支持SpEL表达式的自定义日志注解@SysLog介绍

发布时间 2023-09-19 11:48:18作者: 角刀牛Java

目录
  • 序言
  • 预期
  • 思路
  • 过程
  • 结果

序言

之前封装过一个日志注解,打印方法执行信息,功能较为单一不够灵活,近来兴趣来了,想重构下,使其支持表达式语法,以应对灵活的日志打印需求。

该注解是方法层面的日志打印,如需更细的粒度,还请手撸log.xxx()。

预期

通过自定义注解,灵活的语法表达式,拦截自定义注解下的方法并打印日志

日志要支持以下内容:

  • 方法执行时间
  • 利用已知信息(入参、配置、方法),书写灵活的日志SpEL表达式
  • 打印方法返回结果
  • 按照指定日志类型打印日志

思路

定义自定义注解

拦截自定义注解方法完成以下动作

  • a. 计算方法执行时间
  • b. 解析特定类型的表达式(这里不仅限于SpEL表达式)
  • c. 获取返回结果
  • d. 按照日志类型进行打印

特定类型表达式方案

  • a. 属性解析表达式(如:mybatis对属性的解析,xxx${yyy.aaa}zzz或xxx#{yyy.bbb}zzz书写方式 )
  • b. SpEL表达式(如:${xxx}、#{‘xxx’+#yyy.ppp+aaa.mmm()})

问题:选属性解析表达式、还是SpEL表达式

属性解析表达式:

  • a. 优点:直观、配置简单
  • b. 缺点:需要自行处理属性为待解析对象(容易翻车)

SpEL表达式:

  • a. 优点:解析强大,性能优良
  • b. 缺点:配置复杂不直观

过程

定义自定义注解@SysLog

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface SysLog {
  5. /**
  6. * 日志描述
  7. *
  8. * @return 返回日志描述信息
  9. */
  10. String value();
  11. /**
  12. * 日志等级(info、debug、trace、warn、error)
  13. *
  14. * @return 返回日志等级
  15. */
  16. String level() default "info";
  17. /**
  18. * 打印方法返回结果
  19. *
  20. * @return 返回打印方法返回结果
  21. */
  22. boolean printResult() default false;
  23. }

该类包含以下信息:

  • 日志信息(支持动态表达式)
  • 日志级别(info、debug、trace、warn、error)
  • 是否打印方法返回的结果

走过的弯路1(PropertyParser)

采用MyBatis对XML解析的方式进行解析,需要把拦截到的入参Bean内的属性转换为Properties的方式进行parse,遇到复杂对象就容易出错,属性无法进行动态解析,具体就不详细描述了,感兴趣的可以看下这个类org.apache.ibatis.parsing.PropertyParser

走过的弯路2(ParserContext)

比使用MyBatis更加友好一丢丢,使用Spring自带的ParserContext设定解析规则,结合解析类ExpressionParser进行解析,也没有解决上面遇到的问题,不用引用其它jar包或手撸解析规则,具体就不详细描述了,感兴趣的可以看下这个类

  1. org.springframework.expression.ParserContext

最后的定型方案:

切面拦截方法前后的入参、出参、异常,

SpEL表达式解析,根据表达式去动态解析,语法比预想中强大;

为了确认性能损耗,最后还做了个性能压测

自定义注解切面类SysLogAspect(最终选型SpEL表达式方式)

  1. /**
  2. * SysLog方法拦截打印日志类
  3. *
  4. * @author lipengfei
  5. * @version 1.0
  6. * @since 2019/3/29 10:49 AM
  7. */
  8. @Aspect
  9. public class SysLogAspect {
  10. private static final Logger log = LoggerFactory.getLogger(SysLogAspect.class);
  11. private static final DefaultParameterNameDiscoverer DEFAULT_PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
  12. private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
  13. private static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext();
  14. private static final ThreadLocal<StandardEvaluationContext> StandardEvaluationContextThreadLocal = new ThreadLocal<>();
  15. /**
  16. * 开始时间
  17. */
  18. private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
  19. @Pointcut("@annotation(net.zongfei.core.log.SysLog)")
  20. public void sysLogPointCut() {
  21. }
  22. /**
  23. * 处理完请求后执行
  24. *
  25. * @param joinPoint 切点
  26. */
  27. @SuppressWarnings("unused")
  28. @Before("sysLogPointCut()")
  29. public void doBeforeReturning(JoinPoint joinPoint) {
  30. // 设置请求开始时间
  31. START_TIME.set(System.currentTimeMillis());
  32. }
  33. /**
  34. * 处理完请求后执行
  35. *
  36. * @param joinPoint 切点
  37. */
  38. @AfterReturning(
  39. pointcut = "sysLogPointCut()",
  40. returning = "result"
  41. )
  42. public void doAfterReturning(JoinPoint joinPoint, Object result) {
  43. printLog(joinPoint, result, null);
  44. }
  45. /**
  46. * 拦截异常操作
  47. *
  48. * @param joinPoint 切点
  49. * @param e 异常
  50. */
  51. @AfterThrowing(
  52. pointcut = "sysLogPointCut()",
  53. throwing = "e"
  54. )
  55. public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
  56. printLog(joinPoint, null, e);
  57. }
  58. /**
  59. * 打印日志
  60. *
  61. * @param point 切点
  62. * @param result 返回结果
  63. * @param e 异常
  64. */
  65. protected void printLog(JoinPoint point, Object result, Exception e) {
  66. MethodSignature signature = (MethodSignature) point.getSignature();
  67. String className = ClassUtils.getUserClass(point.getTarget()).getName();
  68. String methodName = point.getSignature().getName();
  69. Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
  70. Method method;
  71. try {
  72. method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
  73. } catch (NoSuchMethodException ex) {
  74. ex.printStackTrace();
  75. return;
  76. }
  77. // 获取注解相关信息
  78. SysLog sysLog = method.getAnnotation(SysLog.class);
  79. String logExpression = sysLog.value();
  80. String logLevel = sysLog.level();
  81. boolean printResult = sysLog.printResult();
  82. // 解析日志中的表达式
  83. Object[] args = point.getArgs();
  84. String[] parameterNames = DEFAULT_PARAMETER_NAME_DISCOVERER.getParameterNames(method);
  85. Map<String, Object> params = new HashMap<>();
  86. if (parameterNames != null) {
  87. for (int i = 0; i < parameterNames.length; i++) {
  88. params.put(parameterNames[i], args[i]);
  89. }
  90. }
  91. // 解析表达式
  92. String logInfo = parseExpression(logExpression, params);
  93. Long costTime = null;
  94. // 请求开始时间
  95. Long startTime = START_TIME.get();
  96. if (startTime != null) {
  97. // 请求耗时
  98. costTime = System.currentTimeMillis() - startTime;
  99. // 清空开始时间
  100. START_TIME.remove();
  101. }
  102. // 如果发生异常,强制打印错误级别日志
  103. if(e != null) {
  104. log.error("{}#{}(): {}, exception: {}, costTime: {}ms", className, methodName, logInfo, e.getMessage(), costTime);
  105. return;
  106. }
  107. // 以下为打印对应级别的日志
  108. if("info".equalsIgnoreCase(logLevel)){
  109. if (printResult) {
  110. log.info("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime);
  111. } else {
  112. log.info("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime);
  113. }
  114. } else if("debug".equalsIgnoreCase(logLevel)){
  115. if (printResult) {
  116. log.debug("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime);
  117. } else {
  118. log.debug("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime);
  119. }
  120. } else if("trace".equalsIgnoreCase(logLevel)){
  121. if (printResult) {
  122. log.trace("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime);
  123. } else {
  124. log.trace("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime);
  125. }
  126. } else if("warn".equalsIgnoreCase(logLevel)){
  127. if (printResult) {
  128. log.warn("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime);
  129. } else {
  130. log.warn("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime);
  131. }
  132. } else if("error".equalsIgnoreCase(logLevel)){
  133. if (printResult) {
  134. log.error("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime);
  135. } else {
  136. log.error("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime);
  137. }
  138. }
  139. }
  140. private String parseExpression(String template, Map<String, Object> params) {
  141. // 将ioc容器设置到上下文中
  142. ApplicationContext applicationContext = SpringContextUtil.getContext();
  143. // 线程初始化StandardEvaluationContext
  144. StandardEvaluationContext standardEvaluationContext = StandardEvaluationContextThreadLocal.get();
  145. if(standardEvaluationContext == null){
  146. standardEvaluationContext = new StandardEvaluationContext(applicationContext);
  147. standardEvaluationContext.addPropertyAccessor(new BeanFactoryAccessor());
  148. StandardEvaluationContextThreadLocal.set(standardEvaluationContext);
  149. }
  150. // 将自定义参数添加到上下文
  151. standardEvaluationContext.setVariables(params);
  152. // 解析表达式
  153. Expression expression = EXPRESSION_PARSER.parseExpression(template, TEMPLATE_PARSER_CONTEXT);
  154. return expression.getValue(standardEvaluationContext, String.class);
  155. }
  156. }

该类按照上面思路中的逻辑进行开发,没有特别复杂的逻辑

为了提高性能和线程安全,对一些类加了static和ThreadLocal

结果

使用方式:

  1. @SysLog(value = “#{‘用户登录'}”)
  2. @SysLog(value = “#{'用户登录: method: ' + #loginRequest.username}”, printResult = true)
  3. @SysLog(value = “#{'用户登录: method: ' + #loginRequest.username + authBizService.test()}”, printResult = true)

更多书写方式参考SpEL表达式即可

  1. /**
  2. * 用户登录接口
  3. *
  4. * @param loginRequest 用户登录输入参数类
  5. * @return 返回用户登录结果输出类
  6. */
  7. @ApiOperation("用户登录接口")
  8. @PostMapping(value = "/login")
  9. @SysLog(value = "#{'用户登录: username: ' + #loginRequest.username + authBizService.test()}", level = "debug", printResult = true)
  10. @Access(type = AccessType.LOGIN, description = "用户登录")
  11. public LoginResponse login(
  12. @ApiParam(value = "用户登录参数") @RequestBody @Valid LoginRequest loginRequest
  13. ) {
  14. // 业务代码
  15. }

结果打印:

2021-09-01 22:04:05.713 ERROR 98511 CRM [2cab21fdd2469b2e--2cab21fdd2469b2e] [nio-8000-exec-2] n.z.m.a.SysLogAspect                     : net.zongfei.crm.api.AuthController#login(): 用户登录: username: lipengfei90@live.cn method: this is test method(), exception: [用户模块] - 用户名或密码错误, costTime: 261ms

压测下来性能损耗较低(可忽略不计)

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

 

 

 

原文:https://www.zhangshengrong.com/p/9MNlDWW6NJ/