Spring AOP与自定义注解实现的操作日志记录功能

论坛 期权论坛 脚本     
已经匿名di用户   2022-5-29 19:35   1202   0

做项目时,有一个记录操作日志的需求,比如某个用户进行了查询、删除、修改等操作,需要把这个操作所对应的各种信息记录下来,为了实现这个需求,采用Spring AOP的切面可以实现。

我采用的是@AfterReturning和自定义注解来实现的,AOP还有其他几个通知,我就不写了。自定义注解是为了描述这个方法具体是什么操作。

自定义注解:

@Target({ElementType.PARAMETER, ElementType.METHOD}) 
@Retention(RetentionPolicy.RUNTIME) 
@Documented 
public @interface Log {
  /** 是否记录日志  true记录   false不记录**/ 
  public boolean record() default true; 
    
  /** 要执行的具体操作比如:添加用户 **/ 
  public String operationName() default "";
}

这个注解类,标注在Controller方法上,在给operationName参数定义一个名字,就可以知道这个方法具体是哪个操作了,后续AOP记录时,获取该注解的operationName值,就能获取到对应的方法描述。

注意:eclipse不能导包,需要手动

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.ElementType;

日志POJO类(set get省略),用于保存日志信息,方便把这些信息插入数据库或者打印到日志文件:

private Integer id;//主键
 private String userId;//登录用户账号
 private String nickName;//登录用户昵称
 private String operationName;//模块
 private String ip;//ip地址
 private Integer port;//端口
 private String url;//url路径
 private String method;//提交方式
 private String cls;//类
 private String classMethod;//调用方法
 private String para;//传递参数
 //@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",locale="zh",timezone="GMT+8") //参数注解,Date对象返回时,格式化日期 yyyy-MM-dd  HH:mm:ss a
 private String date;//操作时间
 private Long time;//耗时
 private Integer success;//是否成功  1成功。0失败

重点来啦,AOP切面类:

@Aspect     // 表示一个切面bean
@Component  // bean容器的组件注解。虽然放在contrller包里,但它不是控制器。如果注入service,但我们又没有放在service包里
@Order(3)   // 有多个日志时,ORDER可以定义切面的执行顺序(数字越大,前置越后执行,后置越前执行)
public class WebLogAspect {
 //定义日志记录器--获取sl4j包下提供的logger
    Logger logger = LoggerFactory.getLogger(this.getClass());
    ThreadLocal<Long> startTime = new ThreadLocal<>();  //线程副本类去记录各个线程的开始时间
    @Autowired
    private LogPojoServiceImpl logPojoServiceImpl;
    //在com.zy.controller包下,任意返回值(第一个*)、任意类(第二个*)、任意方法(第三个*)上插入切点
    @Pointcut("execution(public * com.zy.controller.*.*(..))")
    public void weblog() {
 
    }
 
    //方法的返回值注入给ret
    @AfterReturning(returning = "ret", pointcut = "weblog()")
    public void doafter(JoinPoint joinPoint,Object ret) {
     try {
      //获得注解
         Log controllerLog = getAnnotationLog(joinPoint);
            logger.info("后置返回通知:");                     //info ,debug ,warn ,erro四种级别,这里我们注入info级别
            startTime.set(System.currentTimeMillis());
     
            //获取servlet请求对象---因为这不是控制器,这里不能注入HttpServletRequest,但springMVC本身提供ServletRequestAttributes可以拿到
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            User userObj= (User) request.getSession().getAttribute("loginUser");//获取登录用户
            String userId=userObj.getUserId();//获取账号
            String nickName=userObj.getNickName();//获取昵称
            String ip=request.getRemoteAddr();// 获取ip
            int port=request.getRemotePort();//获取端口
            String url=request.getRequestURL().toString(); //想那个url发的请求
            String method=request.getMethod();//提交方式
            String cls=joinPoint.getSignature().getDeclaringTypeName();//类
            String classMethod=joinPoint.getSignature().getName();//方法
            String para=Arrays.toString(joinPoint.getArgs());// 方法本传了哪些参数
            String date=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//操作时间
            long time=System.currentTimeMillis()-startTime.get() ;
            logger.info("登录用户昵称:" +  nickName); 
            logger.info("ip:" +   ip);         
            logger.info("端口:" + port);   
            logger.info("URL:" + url);       
            logger.info("提交方式:" + method);
            logger.info("类:" + cls);
            logger.info("调用方法:" + classMethod);                     // 
            logger.info("接收参数:" + para);     // 方法本传了哪些参数
            logger.info("操作时间:" + date);
            //logger.info("返回值:" + ret);       // 响应的内容---方法的返回值responseEntity
            logger.info("耗时:" + time);
            if(controllerLog!=null) { 
             String operationName=controllerLog.operationName();//获取模块名称
              logger.info("模块:" + operationName);     
              //是否需要记录日志
               boolean isRecord=controllerLog.record();
               LogPojo logPojo=new LogPojo();
              if(isRecord) {
               logPojo.setNickName(nickName);
               logPojo.setUserId(userId);
               logPojo.setOperationName(operationName);
               logPojo.setIp(ip);
               logPojo.setPort(port);
               logPojo.setUrl(url);
               logPojo.setMethod(method);
               logPojo.setCls(cls);
               logPojo.setClassMethod(classMethod);
               logPojo.setPara(para);
               logPojo.setDate(date);
               logPojo.setTime(time);
               logPojo.setSuccess(1);
               logPojoServiceImpl.insertLogPojo(logPojo);//把日志数据保存进数据库
               logger.info(isRecord+"该日志需要记录-------------------------------");
               
              }else {
               logger.info(isRecord+"该日志不需要记录-------------------------------"); 
              }
            }
            logger.info("---------------------------------------------------------------------");
  } catch (Exception e) {
   // 记录本地异常日志
   logger.error("==通知异常==");
   logger.error("异常信息:{}", e.getMessage());
      e.printStackTrace();
  } 
    }
    
    /**
     * 是否存在注解,如果存在就获取
     */
    private static Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
 Signature signature = joinPoint.getSignature();
 MethodSignature methodSignature = (MethodSignature) signature;
 Method method = methodSignature.getMethod();
 if (method != null) {
     return method.getAnnotation(Log.class);
 }
 return null;
    }
}

这里定义了一个获取注解的方法,就是为了获取Controller 层的具体操作。

然后看Controller上的方法,这样用自定义Log注解标注后:

    //查询数据
 @GetMapping("/queryUserAll")
 @ResponseBody
 @Log(operationName="查询用户")
 public Result<User> queryUserAll() {
  return userManageServiceImpl.queryUserAll();
 }
 //添加
 @PostMapping("/addUser")
 @ResponseBody
 @Log(operationName="添加用户")
 public Result<User> addUser(@RequestBody User user) {
  System.out.println(user);
  return userManageServiceImpl.addUser(user);
 }
 //修改
 @PostMapping("/updateUser")
 @ResponseBody
 @Log(operationName="修改用户")
 public Result<User> updateUser(@RequestBody User user) {
  return userManageServiceImpl.updateUser(user);
 }

最后打印出来的结果如下,我把这些日志信息写入了数据库

数据库中保存的数据如下:

为啥我不用前置通知呢?应为在登录时,session还未保存用户数据,所以获取不到登录时的用户名,所以我干脆就直接使用后置返回通知了。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:81
帖子:4969
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP