java 注解结合 spring aop 实现日志traceId唯一标识
MDC 的必要性
日志框架
日志框架成熟的也比较多:
我们没有必要重复造轮子,一般是建议和 slf4j 进行整合,便于后期替换为其他框架。
日志的使用
基本上所有的应用都需要打印日志,但并不是每一个开发都会输出日志。
主要有下面的问题:
(1)日志太少,出问题时无法定位问题
(2)日志太多,查找问题很麻烦,对服务器磁盘也是很大的压力
(3)日志级别控制不合理
(4)没有一个唯一标识贯穿整个调用链路
我们本次主要谈一谈第四个问题。
为什么需要唯一标识
对于最常见的 web 应用,每一次请求都可以认为新开了一个线程。
在并发高一点的情况,我们的日志会出现穿插的情况。就是我们看日志时,发现出现不属于当前请求的日志,看起来就会特别累。所以需要一个过滤条件,可以将请求的整个生命周期连接起来,也就是我们常说的 traceId。
我们看日志的时候,比如 traceId='202009021658001',那么执行如下的命令即可:
grep 202009021658001 app.log
就可以将这个链路对应的日志全部过滤出来。
那么应该如何实现呢?
实现思路
(1)生成一个唯一标识 traceId
这个比较简单,比如 UUID 之类的就行,保证唯一即可。
(2)输出日志时,打印这个 traceId
于是很自然的就会有下面的代码:
logger.info("traceId: {} Controller 层请求参数为: {}", traceId, req);
缺陷
很多项目都是这种实现方式,这种实现方式有几个问题:
(1)需要参数传递
比如从 controller =》biz =》service,就因为一个 traceId,我们所有的方法都需要多一个参数,用来接受这个值。
非常的不优雅
(2)需要输出 traceId
每次都要记得输出这个值,或者就无法关联。
如果有个别方法忘记输出,那我们根据 traceId 查看日志就会变得很奇怪。
(3)复杂度提高
我们每一个日志都需要区输出这个额外的 traceId,作为一个懒人,不乐意区写这个代码。
那么,有什么方法可以解决这个问题吗?
slf4j 的 MDC 就是为了解决这个问题而存在的。
MDC 的应用场景
程序中,日志打印时我们有时需要跟踪整个调用链路。
最常见的做法,就是将一个属性,比如 traceId 从最外层一致往下传递。
导致每个方法都会多出这个参数,却只是为了打印一个标识,很不推荐。
MDC 就是为了这个场景使用的。
简单例子
普通实现版本
在方法调用前后,手动设置。
本文展示 aop 的方式,原理一样,更加灵活方便。代码也更加优雅。
基于 aop 的方式
定义拦截器
import com.baomidou.mybatisplus.toolkit.IdWorker;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
/**
* 日志拦截器
* @author binbin.hou
* @date 2018/12/7
*/
@Component
@Aspect
public class LogAspect {
/**
* 限额限日志次的 trace id
*/
private static final String TRACE_ID = "TRACE_ID";
/**
* 拦截入口下所有的 public方法
*/
@Pointcut("execution(public * com.github.houbb..*(..))")
public void pointCut() {
}
/**
* 拦截处理
*
* @param point point 信息
* @return result
* @throws Throwable if any
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//添加 MDC
MDC.put(TRACE_ID, IdWorker.getIdStr());
Object result = point.proceed();
//移除 MDC
MDC.remove(TRACE_ID);
return result;
}
}
IdWorker.getIdStr() 只是用来生成一个唯一标识,你可以使用 UUID 等来替代。
更多生成唯一标识的方法,参考:
这个 AOP 的切面一般建议放在调用的入口。
(1)controller 层入口
(2)mq 消费入口
(3)外部 rpc 请求入口
定义 logback.xml
定义好了 MDC,接下来我们在日志配置文件中使用即可。
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{TRACE_ID}] [%thread] %logger{50} - %msg%n</pattern>
</encoder>
[%X{TRACE_ID}] 就是我们系统中需要使用的唯一标识,配置好之后日志中就会将这个标识打印出来。
如果不存在,就是直接空字符串,也不影响。
对于已经存在的系统
现象
如果有一个已经存在已久的项目,原始的打印日志,都会从最上层把订单编号一直传递下去,你会怎么做?
也是这样,把一个标识号从最开始一直传递到最底层吗?
当然不是的。
你完全可以做的更好。
原理
我们知道 MDC 的原理就是在当前的线程中放置一个属性,这个属性在同一个线程中是唯一且共享的。
所以不同的线程之间不会相互干扰。
那么我们对于比较旧的系统,可以采取最简单的方式:
提供一个工具类,可以获取当前线程的订单号。当然,你需要在一个地方将这个值设置到当前线程,一般是方法入口的地方。
更好的方式
你可以提供一个打印日志的工具类,复写常见的日志打印方法。
将日志 traceId 信息等隐藏起来,对于开发是不可见的。
实现方式 ThreadLocal
不再赘述,参见 ThreadLocal
基础的工具类
import org.slf4j.MDC;
/**
* 日志工具类
* @author binbin.hou
*/
public final class LogUtil {
private LogUtil(){}
/**
* trace id
*/
private static final String TRACE_ID = "TRACE_ID";
/**
* 设置 traceId
* @param traceId traceId
*/
public static void setTraceId(final String traceId) {
MDC.put(TRACE_ID, traceId);
}
/**
* 移除 traceId
*/
public static void removeTraceId() {
MDC.remove(TRACE_ID);
}
/**
* 获取批次号
* @return 批次号
*/
public static String getTraceId() {
return MDC.get(TRACE_ID);
}
}
对于异步的处理
spring 异步
参见 async 异步
异步的 traceId 处理
在异步的时候,就会另起一个线程。
建议异步的时候,将原来父类线程的唯一标识(traceId) 当做参数传递下去,然后将这个参数设置为子线程的 traceId。
不依赖 MDC
MDC 的限制
MDC 虽然使用起来比较方便,但是毕竟是 slf4j 为我们实现的一个工具。
其原理就是基于 ThreadLocal 保存基于线程隔离标识。
知道这一点,其实我们可以自己实现一个类似 MDC 的功能,满足不同的应用场景。
实现思路
(1)生成日志唯一标识
(2)基于 ThreadLocal 保存唯一的线程标识
(3)基于注解+AOP
@Around("@annotation(trace)")
public Object trace(ProceedingJoinPoint joinPoint, Trace trace) {
// 生成 id
// 设置 id 到当前线程
Object result = joinPoint.proceed();
// 移除 id
return result;
}
(4)如何使用 id
最简单的方式,就是我们创建一个工具类 LogUtil。
对于常见的方法进行重写,然后日志输出统一调用这个方法。
缺点:日志中的输出 class 类会看不出来,当然可以通过获取方法来解决
优点:实现简单,便于后期拓展和替换。
开源工具
auto-log 是一款为 java 设计的自动日志监控框架。
创作目的
经常会写一些工具,有时候手动加一些日志很麻烦,引入 spring 又过于大材小用。
所以希望从从简到繁实现一个工具,便于平时使用。
特性
基于注解+字节码,配置灵活
自动适配常见的日志框架
支持编程式的调用
支持注解式,完美整合 spring
支持整合 spring-boot
支持慢日志阈值指定,耗时,入参,出参,异常信息等常见属性指定
支持 traceId 特性
快速开始
maven 引入
<dependency>
<group>com.github.houbb</group>
<artifact>auto-log-core</artifact>
<version>0.0.8</version>
</dependency>
入门案例
UserService userService = AutoLogHelper.proxy(new UserServiceImpl());
userService.queryLog("1");
- 日志如下
[INFO] [2020-05-29 16:24:06.227] [main] [c.g.h.a.l.c.s.i.AutoLogMethodInterceptor.invoke] - public java.lang.String com.github.houbb.auto.log.test.service.impl.UserServiceImpl.queryLog(java.lang.String) param is [1]
[INFO] [2020-05-29 16:24:06.228] [main] [c.g.h.a.l.c.s.i.AutoLogMethodInterceptor.invoke] - public java.lang.String com.github.houbb.auto.log.test.service.impl.UserServiceImpl.queryLog(java.lang.String) result is result-1
代码
其中方法实现如下:
- UserService.java
public interface UserService {
String queryLog(final String id);
}
- UserServiceImpl.java
直接使用注解 @AutoLog 指定需要打日志的方法即可。
public class UserServiceImpl implements UserService {
@Override
@AutoLog
public String queryLog(String id) {
return "result-"+id;
}
}
TraceId 的例子
代码
UserService service = AutoLogProxy.getProxy(new UserServiceImpl());
service.traceId("1");
其中 traceId 方法如下:
@AutoLog
@TraceId
public String traceId(String id) {
return id+"-1";
}
测试效果
信息: [ba7ddaded5a644e5a58fbd276b6657af] <traceId>入参: [1].
信息: [ba7ddaded5a644e5a58fbd276b6657af] <traceId>出参:1-1.
其中 ba7ddaded5a644e5a58fbd276b6657af 就是对应的 traceId,可以贯穿整个 thread 周期,便于我们日志查看。
注解说明
@AutoLog
核心注解 @AutoLog 的属性说明如下:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| param | boolean | true | 是否打印入参 |
| result | boolean | true | 是否打印出参 |
| costTime | boolean | false | 是否打印耗时 |
| exception | boolean | true | 是否打印异常 |
| slowThresholdMills | long | -1 | 当这个值大于等于 0 时,且耗时超过配置值,会输出慢日志 |
| description | string | "" | 方法描述,默认选择方法名称 |
@TraceId
@TraceId 放在需要设置 traceId 的方法上,比如 Controller 层,mq 的消费者,rpc 请求的接受者等。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| id | Class | 默认为 uuid | traceId 的实现策略 |
| putIfAbsent | boolean | false | 是否在当前线程没有值的时候才设置值 |
spring 整合使用
完整示例参考 SpringServiceTest
注解声明
使用 @EnableAutoLog 启用自动日志输出
@Configurable
@ComponentScan(basePackages = "com.github.houbb.auto.log.test.service")
@EnableAutoLog
public class SpringConfig {
}
测试代码
@ContextConfiguration(classes = SpringConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringServiceTest {
@Autowired
private UserService userService;
@Test
public void queryLogTest() {
userService.queryLog("1");
}
}
- 输出结果
信息: public java.lang.String com.github.houbb.auto.log.test.service.impl.UserServiceImpl.queryLog(java.lang.String) param is [1]
五月 30, 2020 12:17:51 下午 com.github.houbb.auto.log.core.support.interceptor.AutoLogMethodInterceptor info
信息: public java.lang.String com.github.houbb.auto.log.test.service.impl.UserServiceImpl.queryLog(java.lang.String) result is result-1
五月 30, 2020 12:17:51 下午 org.springframework.context.support.GenericApplicationContext doClose
springboot 整合使用
maven 引入
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>auto-log-springboot-starter</artifactId>
<version>0.0.8</version>
</dependency>
只需要引入 jar 即可,其他的什么都不用配置。
使用方式和 spring 一致。
测试
@Autowired
private UserService userService;
@Test
public void queryLogTest() {
userService.query("spring-boot");
}
开源地址

拓展阅读
参考资料
java 注解结合 spring aop 实现日志traceId唯一标识的更多相关文章
- 基于注解的Spring AOP的配置和使用
摘要: 基于注解的Spring AOP的配置和使用 AOP是OOP的延续,是Aspect Oriented Programming的缩写,意思是面向切面编程.可以通过预编译方式和运行期动态代理实现在不 ...
- 基于注解的Spring AOP的配置和使用--转载
AOP是OOP的延续,是Aspect Oriented Programming的缩写,意思是面向切面编程.可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术. ...
- Spring AOP进行日志记录
在java开发中日志的管理有很多种.我一般会使用过滤器,或者是Spring的拦截器进行日志的处理.如果是用过滤器比较简单,只要对所有的.do提交进行拦截,然后获取action的提交路径就可以获取对每个 ...
- Spring AOP进行日志记录,管理
在java开发中日志的管理有很多种.我一般会使用过滤器,或者是Spring的拦截器进行日志的处理.如果是用过滤器比较简单,只要对所有的.do提交进行拦截,然后获取action的提交路径就可以获取对每个 ...
- Java动态代理-->Spring AOP
引述要学习Spring框架的技术内幕,必须事先掌握一些基本的Java知识,正所谓“登高必自卑,涉远必自迩”.以下几项Java知识和Spring框架息息相关,不可不学(我将通过一个系列分别介绍这些Jav ...
- 基于注解的Spring AOP示例
基于注解的Spring AOP示例 目录 在XML配置文件中开启 @AspectJ 支持 声明切面及切入点 声明通知 测试 结语 在XML配置文件中开启 @AspectJ 支持 要使用Spring的A ...
- Spring AOP 完成日志记录
Spring AOP 完成日志记录 http://hotstrong.iteye.com/blog/1330046
- Spring Aop(二)——基于Aspectj注解的Spring Aop简单实现
转发地址:https://www.iteye.com/blog/elim-2394762 2 基于Aspectj注解的Spring Aop简单实现 Spring Aop是基于Aop框架Aspectj实 ...
- 【java自定义注解2】java自定义注解结合Spring AOP
承接上一篇,注解应用于属性,本篇定义了一个用于方法的注解,结合Spring AOP 实现 切面编程. 以下demo演示使用了SpringBoot,与SSM中使用方式大致相同,效果如下: 1.自定义注解 ...
- java框架篇---spring AOP 实现原理
什么是AOP AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善.OOP引入 ...
随机推荐
- Nacos源码 (4) 配置中心
本文阅读nacos-2.0.2的config源码,编写示例,分析推送配置.监听配置的原理. 客户端 创建NacosConfigService对象 Properties properties = new ...
- [转帖]防火墙、DCD与TCP Keep alive
https://www.laoxiong.net/tag/network 在以前我写的一篇文章<Oracle与防火墙>中提到,网络防火墙会切断长时间空闲的TCP连接,这个空闲时间具体多长可 ...
- [转帖]Oracle参数解析(parallel_force_local)
https://www.modb.pro/db/122032 是否需要增加这个参数? 往期专题请查看www.zhaibibei.cn这是一个坚持Oracle,Python,MySQL原创内容的公众号 ...
- Nginx arm编译安装
Nginx arm编译安装 背景 计划编译一套产品. 能够比较方便快捷的进行 nginx的交付. 主要思想是源码编译 不仅能够在arm上面运行 也可以在x86上面编译 考虑性能还有一些扩展性. 高效处 ...
- [转帖]kafka压测多维度分析实战
设置虚拟机不同的带宽来进行模拟压测 ---------kafka数据压测-------------------1.公司生产kafka集群硬盘:单台500G.共3台.日志保留7天. 1. ...
- [转帖][问题已处理]-kubernetes中2次不同的oom处理
https://dandelioncloud.cn/article/details/1598699030236577793 起因: 同事反馈 服务挂了,kuboard上查看是服务挂掉了,livenes ...
- 你对iframe知道多少
iframe 嵌套第三方页面出现的问题 我们需要通过一个接口获取被嵌套的地址. 然后将改地址赋值给iframe的src中,代码如下 <template> <div> <i ...
- vue3跟新视图遇见神奇现象
场景描述 今天遇见一个问题, tableAllFun 函数中写了一个 index=1; 然后在 otherAllFun 函数中去改变这个index=2的值 奇怪的事情发生了 在视图index展示的值是 ...
- vue写组件时的命名规范
1组件命名驼峰 如myBread.vue(组件) 2引入时,接受同样是驼峰 import MyBread from "@/components/cuscom/myBread.vue" ...
- 【小测试】VictoriaMetrics中如何汇总单个time series上的多个data point?
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 问题最终在andy专家的帮助下解决,但是内部的原理还是很迷 ...