java 注解结合 spring aop 实现日志traceId唯一标识

摘要:
实现思想是生成唯一标识符traceId。例如,可以使用UUID等来确保唯一性。复杂性增加。我们需要为每个日志输出这个额外的traceId。作为一个懒惰的人,我们不愿意写这段代码。最常见的方法是将属性(如traceId)从最外层传递到底层。隐藏日志的traceId信息,这对于开发来说是不可见的。
MDC 的必要性

日志框架

日志框架成熟的也比较多:

slf4j

log4j

logback

log4j2

我们没有必要重复造轮子,一般是建议和 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 等来替代。

更多生成唯一标识的方法,参考:

分布式id

这个 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 类会看不出来,当然可以通过获取方法来解决

优点:实现简单,便于后期拓展和替换。

https://github.com/houbb/auto-log

开源工具

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 的属性说明如下:

属性类型默认值说明
parambooleantrue是否打印入参
resultbooleantrue是否打印出参
costTimebooleanfalse是否打印耗时
exceptionbooleantrue是否打印异常
slowThresholdMillslong-1当这个值大于等于 0 时,且耗时超过配置值,会输出慢日志
descriptionstring""方法描述,默认选择方法名称

@TraceId

@TraceId 放在需要设置 traceId 的方法上,比如 Controller 层,mq 的消费者,rpc 请求的接受者等。

属性类型默认值说明
idClass默认为 uuidtraceId 的实现策略
putIfAbsentbooleanfalse是否在当前线程没有值的时候才设置值
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");
}

开源地址

Github: https://github.com/houbb/auto-log

Gitee: https://gitee.com/houbinbin/auto-log

深入学习

拓展阅读

分布式 id 生成

日志自动输出

参考资料

SLF4j traceID

基于SLF4J MDC机制实现日志的链路追踪

免责声明:文章转载自《java 注解结合 spring aop 实现日志traceId唯一标识》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇基于 Docker 的几种常用 CentOS7 镜像开关电源纹波来源及处理方式下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

十、future其他成员函数、shared_future、atomic(原子操作)

一、 1 int mythread(){ 2 cout<<"thread"<<endl; 3 std::chrono::milliseconds dura(5000);//5秒钟 4 std::this_thread::sleep_for(dura);//休息5秒钟 5 return 5;...

重新理解:ASP.NET 异步编程

相关博文: 异步编程 In .NET(回味无穷!!!) ASP.NET sync over async(异步中同步,什么鬼?) 本来这篇博文想探讨下异步中的异常操作,但自己在做异步测试的时候,又对 ASP.NET 异步有了新的认识,可以说自己之前对异步的理解还是有些问题,先列一下这篇博文的三个解惑点: async await 到底是什么鬼??? 异...

tomcat占用cpu比较多

在Linux中当Tomcat启动后,我们只是去查看应用是否能够正常访问来判断Tomcat启动是否正常。一般情况下这样看是没有问题的,但是有时候我们会发现当Tomcat使用了一段时间后,开始出现CPU或者内存飙升。此时,查看Tomcat日志也许无法看到有效信息,因此有必要了解一下Linux和jdk给我们提供的相关工具。 1、查看内存和CPU使用较高的进程信息...

Linux-pthread如何设置线程的优先级

设置线程优先级的函数: int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param) 它主要用于设置线程的调用策略和优先级。 参数说明: 1.  target_thread是使用pthread_create所获得的线程ID。  ...

【转】Javascript异步编程之setTimeout与setInterval

Javascript异步编程之setTimeout与setInterval 转自:http://www.tuicool.com/articles/Ebueua 在谈到异步编程时,本人最主要会从以下三个方面来总结异步编程(注意:特别解释:是总结,本人也是菜鸟,所以总结不好的,请各位大牛多多原谅!) 1. setTimeout与setInterval详细分析基...

C语言之生产者与消费者模型

多线程并发应用程序有一个经典的模型,即生产者/消费者模型。系统中,产生消息的是生产者,处理消息的是消费者,消费者和生产者通过一个缓冲区进行消息传递。生产者产生消息后提交到缓冲区,然后通知消费者可以从中取出消息进行处理。消费者处理完信息后,通知生产者可以继续提供消息。 要实现这个模型,关键在于消费者和生产者这两个线程进行同步。也就是说:只有缓冲区中有消息时,...