核心内容摘要
当小熊遇上“黄油”:一场颠覆你想象的温暖触感革命
为了更方便地排查问题电商交易系统的日志中需要记录用户id和订单id等字段。
然而每次打印日志都需要手动设置用户id这一过程非常繁琐需要想个办法优化下。
log.warn(user:{}, orderId:{} 订单提单成功,userId, orderId); log.warn(user:{}, orderId:{} 订单支付成功,userId, orderId); log.warn(user:{}, orderId:{} 订单收到履约请求,userId, orderId); log.warn(user:{}, orderId:{} 订单履约成功,userId, orderId);01目标打印日志时自动填充用户id和订单Id等通参无需手动指定02实现思路日志模板中声明占位符 userIdorderId在业务入口将userId放入到线程ThreadLocal本地变量中。
使用SpringAop 注解 自动将第二步的用户信息放到线程上下文03配置日志变量%X{}可以自定义占位符例如本例中 使用userId:%X{userId} orderId:%X{orderId}定义了userId和orderId两个占位符。
?xml version
0 encodingUTF-8? Configurationstatusinfo Appenders ConsolenameconsoleAppendertargetSYSTEM_OUT PatternLayoutpattern%d{DEFAULT} [%t] %-5p - userId:%X{userId} orderId:%X{orderId} %m%n%excharsetUTF-8/ /Console /Appenders Loggers !-- Root Logger -- AsyncRootlevelinfoincludeLocationtrue appender-refrefconsoleAppender/ /AsyncRoot /Loggers /Configuration04基于MDC 的上下文Map为了给每个请求添加唯一标识用户可将上下文信息放入MDC(Mapped Diagnostic Context)。
slfj 提供了MDC 类可以将变量设置在线程上下文中日志框架会自动将线程上下文中的变量放置到日志占位符中。
Slf4j 作为java日志标准log4j和logback都实现了slfj 日志标准。
MDC是基于每个线程进行管理的允许每个服务器线程具有不同的MDC标记。
MDC类中的put()和get()等操作仅影响当前线程的MDC。
其他线程中的MDC不会受到影响所以可以理解MDC是基于ThreadLocal的Map。
例如下面这种方式打印日志的效果是这样的。
MDC.put(userId, userId); MDC.put(orderId, orderId); log.warn(订单履约完成);当使用log.warn(订单履约完成)方式打印日志时代码中会自动包含userId和 订单Id。
21:35:38,284 [main] WARN - userId:32894934895 orderId:8497587947594859232 订单履约完成接下来声明一个注解加切面自动将用户和订单信息放到日志占位符中。
05注解 SpringAop通过注解的方式在方法执行之前自动将UserId注入到MDC中。
其中的难点在于如何获取到UserId。
我的思路是方法的入参中肯定包含了UserId。
可以在注解中声明UserId的获取路径在切面中获取到UserId并将其注入到MDC中。
1 定义注解Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) publicinterface UserLog { String userId()default ; String orderId()default ; }使用时要求输入userId属性的路径。
例如UserOrder中包含userId和orderId属性则像如下方式声明。
UserLog(userId userId, orderId orderId) publicvoidorderPerform(UserOrder order){ log.warn(订单履约完成); } Data publicstaticclassUserOrder{ String userId; String orderId; }
2 定义切面声明注解的Aop切面在方法执行前将UserId从入参中取出来放到MDC中。
全部代码如下Aspect Component publicclassUserLogAspect{ Pointcut(annotation(UserLog) execution(public * *(..))) publicvoidpointcut(){ } Around(value pointcut()) public Object around(ProceedingJoinPoint joinPoint)throws Throwable { //无参方法不处理 Method method ((MethodSignature) joinPoint.getSignature()).getMethod(); Object[] args joinPoint.getArgs(); //获取注解 UserLog userLogAnnotation method.getAnnotation(UserLog.class); if (userLogAnnotation ! null args ! null args.length
{ //使用工具类获取userId。
String userId String.valueOf(PropertyUtils.getProperty(args[0], userLogAnnotation.userId())); String orderId String.valueOf(PropertyUtils.getProperty(args[0], userLogAnnotation.orderId())); // 放到MDC中 MDC.put(userId, userId); MDC.put(orderId, orderId); } try { Object response joinPoint.proceed(); return response; } catch (Exception e) { throw e; } finally { //清理MDC MDC.clear(); } } }
3 关键代码解读
5.
1 获取UserLog注解UserLog userLogAnnotation method.getAnnotation(UserLog.class);
5.
2 使用PropertyUtils.getProperty 获取userIdPropertyUtils.getProperty(args[0], userLogAnnotation.userId())要注意 PropertyUtils 是commons-beanutils提供的工具类可以指定属性的路径自动提取属性值。
如果存在多层关系可以使用级联取属性值。
例如 info.userId则从对象的info属性中取userId属性。
dependency groupIdcommons-beanutils/groupId artifactIdcommons-beanutils/artifactId version
1.
4/version /dependency
5.
3 使用MDC设置变量和清除变量。
MDC.put(userId, userId); MDC.clear();06验证使用效果
1 声明业务ServiceService publicclassOrderService{ publicstaticfinal Logger log LoggerFactory.getLogger(OrderService.class); UserLog(userId userId, orderId orderId) publicvoidorderPerform(UserOrder order){ log.warn(订单履约完成); } Data publicstaticclassUserOrder{ String userId; String orderId; } }
2 测试日志打印Test publicvoidtestUserLog(){ OrderService.UserOrder order new OrderService.UserOrder(); order.setUserId(
; order.setOrderId(
; orderService.orderPerform(order); }
3 日志效果07
总结不同的业务场景有不同的日志需求一般情况下为了排查问题方便需要唯一标识把一系列请求串联起来使用 UserLog 注解Aop 自动将这部分默认参数放到日志中可以简化业务日志打印极大地提高了生产力。
另外大家可以自行扩展能力例如自动打印出入参日志自动上报监控打点等等。
各位朋友以上工具的关键代码不超过30行快点试试吧。