一、前言
本文主要解析应用中比较实在的一个log4j的链路应用,在经过一段时间的应用后发现还是很稳的,就记录一下这个MDC 代表Mapped Diagnostic Context(映射式诊断上下文)的情况。
Tisp: 它是一个线程安全的存放诊断日志的容器
Tisp: 它是一个线程安全的存放诊断日志的容器
Tisp: 它是一个线程安全的存放诊断日志的容器
二、接口相关内容
相关slf4j 官方的接口文档: https://www.slf4j.org/api/org/slf4j/MDC.html
1、常用直接调用接口
clear() :移除所有MDC
get (String key) :获取当前线程MDC中指定key的值
getContext() :获取当前线程MDC的MDC
put(String key, Object o) :往当前线程的MDC中存入指定的键值对
remove(String key) :删除当前线程MDC中指定的键值对
2、调试用接口
- pushByKey(String key, String value) :将指定的键值对推入 MDC 上下文信息的栈
可以在之后 在之后通过 popByKey
恢复原始值
- popByKey(String key) :从 MDC 上下文信息的栈中弹出指定键的值
如果在没有先前推送的情况下调用 popByKey
,则会将键从 MDC 中移除。
3、配置用接口
getCopyOfContextMap() : 一个包含当前线程的 MDC 上下文信息的 不可修改的 映射副本
getMDCAdapter() :获取当前线程的 MDC 适配器,可进行 MDC 上下文信息的设置和清除
setContextMap(Map<String,String> contextMap) : 设置整个 MDC 上下文信息的映射
三、应用配置
下属内容中
TraceUtils
仅为生成traceId
TraceConstant
对应枚举类
logging:
pattern:
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{traceId} - %msg%n"
logging.pattern.console=%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{traceId} - %msg%n
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="logDirectory" value="${LOG_DIRECTORY:-logs}"/>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<layout>
<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{traceId} - %msg%n</Pattern>
</layout>
</appender>
<appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logDirectory}/app.log</file>
<!-- 其他 RollingFileAppender 配置省略 -->
</appender>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="File"/>
</root>
</configuration>
1、异步任务线程配置
针对独立线程修饰
TaskDecorator
接口允许在任务执行之前和之后对执行线程进行修改或装饰
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 获取当前线程的 MDC 上下文信息
//final var contextMap = MDC.getCopyOfContextMap();
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// 设置新的执行线程的 MDC 上下文信息
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
// 执行任务
runnable.run();
} finally {
// 清除 MDC 上下文信息
MDC.clear();
}
};
}
}
针对线程工厂修饰
利用
ThreadFactory
创建线程针对内容修饰MDC
public static class MdcThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
// 在创建线程时设置 MDC 上下文信息
return new Thread(() -> {
// 获取当前线程的 MDC 上下文信息
final Map<String, String> contextMap = MDC.getCopyOfContextMap();
try {
// 在新线程中设置 MDC 上下文信息
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
// 执行任务
r.run();
} finally {
// 清除 MDC 上下文信息
MDC.clear();
}
});
}
}
1.1、设置注解线程池的配置
1、利用配置异步方法的执行器
AsyncConfigurer
来配置注解线程池2、
setTaskDecorator
和setThreadFactory
会以最后一个配置的为准
@Component
public class TraceAsyncConfigurer implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-pool-");
//①用来直接修改装饰器
executor.setTaskDecorator(new MdcTaskDecorator());
//② 也可以直接用ThreadFactory
executor.setThreadFactory(new MdcThreadFactory());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
}
1.2、设置直接使用的线程配置
在工厂类直接配置MDC即可
ThreadPoolExecutor executor = new ThreadPoolExecutor(8,
16,
15000,
TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
new MdcThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
2、针对web上下文配置
利用
HandlerInterceptor
直接拦截
public class TraceHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader(TraceConstant.TRACE_ID);
if (!StringUtils.hasText(traceId)) {
traceId = TraceUtils.traceIdGenerator();
}
MDC.put(TraceConstant.TRACE_ID, traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
MDC.remove(TraceConstant.TRACE_ID);
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
3、Feign客户端的配置
RequestInterceptor
是Feign客户端库中的一个接口,用于拦截Feign客户端发出的HTTP请求,利用这个特性包装
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
private final Logger logger = LoggerFactory.getLogger(FeignRequestInterceptor.class);
public FeignRequestInterceptor(){
logger.info("Initializing feign trace interceptor");
}
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(TraceConstant.TRACE_ID, TraceContextUtils.getTraceId());
}
}
4、利用javaagent的思路配置
这里可以看做是相对无侵入式的一种模式把,偷懒是要慢慢来
其实这里存在 静态注入 和 动态注入 ,但是走静态把
动态注入的方式: 实际上需要改造一下代码,使得可以后续动态java-agent内容。
**静态注入的方式:**只需要变更jvm,增加
-javaagent: /路径
即可
4.1、基础环境内容
1、全限定类名: org.examlpe.MDCAgent
2、 测试端点包名 :org.example.controller
3、 打包后路径地址 :/opt/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar
<!-- https://mvnrepository.com/artifact/org.javassist/javassist
当然这里需要引入一个依赖方便下面对类的魔改
-->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.1-GA</version>
</dependency>
构建打包插件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<descriptorRefs>
<!-- 这个为最终输出的名称-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!-- 设置manifest配置文件-->
<manifestEntries>
<!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
<Premain-Class>org.example.MDCAgent</Premain-Class>
<!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
<Agent-Class>org.example.MDCAgent</Agent-Class>
<!--Can-Redefine-Classes: 是否可进行类定义。-->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!--Can-Retransform-Classes: 是否可进行类转换。-->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<!--绑定到package生命周期阶段上-->
<phase>package</phase>
<goals>
<!--绑定到package生命周期阶段上-->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
4.2、利用 premain 预加载
利用
premain
的特性提前于主方法前加载Instrumentation
使得我们可以方便操作类的一些内容(悄悄改)
public class MDCAgent {
private static final Logger logger = LoggerFactory.getLogger(MDCAgent.class);
/**
* @todo 这里实际上我们只需要针对控制层进行补充即可
*/
private static final String TRANSFORM_PREIFX = "org/example/controller";
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("premain start!");
addTransformer(instrumentation);
System.out.println("premain end!");
}
private static void addTransformer(Instrumentation instrumentation) {
....
}
}
4.3、利用 Instrumentation 对类加载前魔改
1、 Modifier.isNative(method.getModifiers()) 这部分是搜罗的经验之谈
2、主要目的是针对
测试端点包名
内容进行注入
instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String className, Class<?> c, ProtectionDomain pd, byte[] b) {
try {
//判断是否为目标包下的类
if (className != null && className.startsWith(TRANSFORM_PREIFX)) {
// 类属于目标包名下的处理逻辑
System.out.println("Class " + className + " belongs to target package.");
// 其他的字节码转换逻辑
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get(className.replace("/", "."));
for (CtMethod method : clazz.getMethods()) {
/*
* Modifier.isNative(methods[i].getModifiers())过滤本地方法,否则会报
* javassist.CannotCompileException: no method body at javassist.CtBehavior.addLocalVariable()
*/
if (Modifier.isNative(method.getModifiers())) {
continue;
}
String traceId = MDC.get(TraceConstant.TRACE_ID);
if (!StringUtils.hasText(traceId)) {
traceId = TraceUtils.traceIdGenerator();
}
MDC.put(TraceConstant.TRACE_ID, traceId);
// 这里正常不需要,仅用于查看效果
// method.insertBefore("System.out.println(\"" + clazz.getSimpleName() + "."
// + method.getName() + "-" + traceId + " start.\");");
//
// method.insertAfter("System.out.println(\"" + clazz.getSimpleName() + "."
// + method.getName() + "-" + MDC.get("haha") + " end.\");", false);
}
return clazz.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
}, true);
}
4.4、构建测试
目标vm参数 :-javaagent: /opt/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar
@GetMapping("ping")
public String ping() throws InterruptedException {
return "pong";
}
//样例测试结果
//DemoController.ping1-ab83c3c3-08aa-4e9d-9d6f-513967eb28ff start.
//DemoController.ping1-ab83c3c3-08aa-4e9d-9d6f-513967eb28ff end.