统一记录日志
最近业务有需求要对所有的用户操作进行日志记录,方便管理员查询不同权限级别的用户对系统的操作记录,现有的日志只是记录了异常信息、业务出错、重要功能的执行进行了记录,并不能满足需求要求,最直接的解决方法是在每个接口上去添加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 }
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