统一记录日志

最近业务有需求要对所有的用户操作进行日志记录,方便管理员查询不同权限级别的用户对系统的操作记录,现有的日志只是记录了异常信息、业务出错、重要功能的执行进行了记录,并不能满足需求要求,最直接的解决方法是在每个接口上去添加log.info之类的代码,但是这种方式对业务代码的切入性太强,记录日志的代码和业务代码耦合性太强,对于代码的可读性和可维护性来说是一个灾难。那么通过在接口上添加一个注解的方式来实现则要优雅的多。

实现原理就是利用了Spring的切面技术AOP,在接口执行的切面上获取接口方法的参数和执行结果,将要记录的信息记录到数据库(或者是日志文件或者是其他方式)。

1、定义注解:

 1 @Target(ElementType.METHOD)
 2  @Retention(RetentionPolicy.RUNTIME)
 3  @Inherited
 4  @Documented
 5  public @interface LogBook {
 6  7      /**
 8       * 模块
 9       *
10       * @return
11       */
12      String module();
13 14      /**
15       * 跟踪标识(业务标识)
16       *
17       * @return
18       */
19      String traceId();
20 21      /**
22       * 跟踪标签(业务标识标签)
23       *
24       * @return
25       */
26      String traceTag() default "";
27 28      /**
29       * 操作内容
30       *
31       * @return
32       */
33      String[] content();
34 35      /**
36       * 操作类型
37       * 为null时,框架根据request-method进行匹配
38       *
39       * @return
40       */
41      String operateType() default "";
42 43      /**
44       * 操作员 
45       *
46       * @return
47       */
48      String operator() default "_header";
49 50      /**
51       * 启停
52       *
53       * @return
54       */
55      boolean enable() default true;
56  }

2、定义切面类:

定义切面类使用Aop的注解@Aspect来定义,对制定的注解进行环切,定义如下:

  1 @Aspect
  2  public class GenericRestLogBookAspect {
  3   4      //切面 包含注解
  5      @Pointcut("@annotation(com.xxx.annotation.LogBook)")
  6      public void intercept() {
  7   8      }
  9  10  11      /**
 12       * 对方法进行环切
 13       *
 14       * @param joinPoint
 15       * @return
 16       * @throws Throwable
 17       */
 18      @Around(value = "intercept()")
 19      public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
 20  21  22          /**
 23           * 当前注解实例
 24           */
 25          MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
 26          LogBook annotation = AnnotationUtils.findAnnotation(methodSignature.getMethod(), LogBook.class);
 27          if (annotation == null || !annotation.enable()) {
 28              return joinPoint.proceed();
 29          }
 30  31          /**
 32           * 基础参数
 33           */
 34          Object[] args = joinPoint.getArgs();
 35          HttpServletRequest request = this.getRequest(args);
 36  37          LogBookRecord init = new LogBookRecord();
 38          init.setCreateDate(new Date());         //标准时间
 39  40          EvaluationContext evaluationContext = expressionParser.initContext(joinPoint);
 41  42  43          /**
 44           * 执行
 45           */
 46          Object out = null;
 47          Exception err = null;
 48          try {
 49              //前置
 50              this.beforeResolving(request, evaluationContext, annotation, args, init);
 51              //业务执行
 52              out = joinPoint.proceed();
 53          } catch (Exception e) {
 54              err = e;
 55              throw e;
 56          } finally {
 57              //后置
 58              this.afterResolving(request, evaluationContext, annotation, args, init, out, err);
 59              //释放
 60              expressionParser.removeContext();
 61          }
 62          return out;
 63      }
 64  65      /**
 66       * 前置处理
 67       *
 68       * @param handler
 69       * @param request
 70       * @param evaluationContext
 71       * @param annotation
 72       * @param args
 73       * @param init
 74       */
 75      private void beforeResolving(HttpServletRequest request,
 76                                   EvaluationContext evaluationContext,
 77                                   LogBook annotation,
 78                                   Object[] args,
 79                                   LogBookRecord init) {       
 80          //这里的resolving方法从request中解析出相关参数信息到LogBookRecord对象中
 81          resolving(request, evaluationContext, annotation, args, init);
 82      }
 83  84  85      /**
 86       * 后置处理
 87       *
 88       * @param handler
 89       * @param request
 90       * @param evaluationContext
 91       * @param annotation
 92       * @param args
 93       * @param record
 94       * @param result
 95       * @param err
 96       */
 97      private void afterResolving(HttpServletRequest request,
 98                                  EvaluationContext evaluationContext,
 99                                  LogBook annotation,
100                                  Object[] args,
101                                  LogBookRecord record,
102                                  Object result, Exception err) {        
103          /**
104           * 异步处理
105           */
106          try {
107              //重新构建 EvaluationContext
108              EvaluationContext evaluationAfter = expressionParser.setContextResult(evaluationContext, result, err);
109              threadPool.getTaskExecutor().execute(() -> {
110                  //解析 - 根据业务接口返回结果信息解析到LogBookRecord中
111                  List<LogBookRecord> records = resolvingAfter(request, evaluationAfter, annotation, args, result, err, record);
112                  //存储(可以存储到数据库,也可以存储到日志文件或者其他地方)
113                  persistHandler.saveBatch(records);
114              });
115          } catch (Exception e) {
116              logger.error(e.getMessage(), e);
117          }
118      }
119 120 121      /**
122       * 默认从参数中获取
123       *
124       * @param args
125       * @return
126       */
127      private HttpServletRequest getRequest(Object[] args) {
128          Optional<HttpServletRequest> ops = Stream.of(args).filter(e -> e.getClass().isAssignableFrom(HttpServletRequest.class)).map(e -> (HttpServletRequest) e).findFirst();
129          return ops.orElseGet(() -> ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
130      }
131  }

3、定义开关属性:

通过这个开关属性,控制开启还是关闭日志记录。

 1 @Configuration
 2  @ConfigurationProperties(prefix = "xxx.logbook")
 3  public class LogBookProperties implements Serializable {
 4  5      private boolean enable;
 6  7      public boolean isEnable() {
 8          return enable;
 9      }
10 11      public void setEnable(boolean enable) {
12          this.enable = enable;
13      }
14  }

4、定义配置类:

在配置类中定义切面类的Bean,并通过开关属性进行开关控制;

 1  @Configuration
 2  @ConditionalOnProperty(name = "xxx.logbook.enable", havingValue = "true")
 3  @ComponentScan("com.xxx.logbook")
 4  public class LogBookAutoConfiguration {    
 5  6      /**
 7       * 切面
 8       *
 9       * @param factory
10       * @param expressionParser
11       * @return
12       */
13      @Bean
14      public GenericRestLogBookAspect logBookAspect(LogBookExpressionParser expressionParser,
15                                                    LogBookPersistHandler persistHandler, LogBookThreadPool threadPool) {
16          return new GenericRestLogBookAspect(expressionParser, persistHandler, threadPool);
17      }
18  }

5、应用示例:

1 @LogBook(module = "custom", 
2          traceId = "{{#dto.id}}",
3          content = {"用户 :提交了新数据:{{#dto.filed1}} {{#dto.filed2}} {{#dto.filed3}} "}, 
4          operateType = OperateTypes.ADD)

6、参考资料:

https://blog.csdn.net/Cr1556648487/article/details/126777903

https://blog.csdn.net/yyhgo_/article/details/128724938

https://blog.csdn.net/weixin_38860401/article/details/124908507

posted on 2023-09-13 14:35  小夏coding  阅读(251)  评论(0编辑  收藏  举报

导航