针对于上一篇【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「Logback-MDC篇」的功能开发指南之后,相信你对于Sl4fj以及Log4j整个生态体系的功能已经有了一定的大致的了解了,接下来我们需要进行介绍关于实现如何将MDC的编程模式改为声明模式的技术体系,首先再我们的基础机制而言,采用的是Spring的AOP体系,所以我们先来解决说明一下Spring的AOP技术体系。
Spring的AOP功能除了在配置文件中配置一大堆的配置,比如:切入点、表达式、通知等等以外,使用注解的方式更为方便快捷,特别是 Spring boot 出现以后,基本不再使用原先的 beans.xml 等配置文件了,而都推荐注解编程。
对于习惯了Spring全家桶编程的人来说,并不是需要直接引入 aspectjweaver 依赖,因为 spring-boot-starter-aop 组件默认已经引用了 aspectjweaver 来实现 AOP 功能。换句话说 Spring 的 AOP 功能就是依赖的 aspectjweaver !
AOP Proxy:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
@Aspect(切面): 切面声明,标注在类、接口(包括注解类型)或枚举上,JointPoint(连接点): 程序执行过程中明确的点,一般是方法的调用


@Pointcut(切入点): 带有通知的连接点,在程序中主要体现为书写切入点表达式,切入点声明,即切入到哪些目标类的目标方法。value 属性指定切入点表达式,默认为 “”,用于被通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
@Pointcut("execution(* com.savage.aop.MessageSender.*(..))")
//Point签名
private void pointCutRange(){}
切入点表达式通过 execution 函数匹配连接点,语法:execution([方法修饰符] 返回类型 包名.类名.方法名(参数类型) [异常类型])
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
其中后面跟着“?”的是可选项,括号中各个pattern分别表示:
execution(public * *(..))
execution(* set*(..))
execution(* com.xyz.service.XXXService.*(..))
execution(* com.xyz.service.*.*(..))
execution(* com.xyz.service..*.*(..))
第一个表示匹配任意的方法返回值, …(两个点)表示零个或多个,第一个…表示service包及其子包,第二个表示所有类, 第三个*表示所有方法,第二个…表示方法的任意参数个数
execution(* com.xx.test..test.*(..))")
within(com.xx.test.*)
within(com.xx.test..*)
this(com.xx.TestService)
@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)
@annotation(org.springframework.transaction.annotation.Transactional)
优先级高的切面类里的增强处理的优先级总是比优先级低的切面类中的增强处理的优先级高。Spring提供了如下两种解决方案指定不同切面类里的增强处理的优先级:
让切面类实现org.springframework.core.Ordered接口:实现该接口的int getOrder()方法,该方法返回值越小,优先级越高
直接使用@Order注解来修饰一个切面类:使用这个注解时可以配置一个int类型的value属性,该属性值越小,优先级越高
但是,同一个切面类里的两个相同类型的增强处理在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这两个增强处理,没有办法指定它们的织入顺序。即使给这两个 advice 添加了 @Order 这个注解,也不行!
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface TraceIdInjector {}
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.slf4j.MDC;
@Component
@Aspectj
public class TraceIdInterceptor {
protected final static String traceId = "traceId";
@Pointcut("execution(@annotation(com.xx.xx.TraceIdInjector)")
public void pointCutRange() { }
@Around(value = "pointCutRange()")
public Object invoke(ProceedingJoinPoint point) throws Throwable {
Object result;
try {
buildTraceId();
result = point.proceed(point.getArgs());
} catch (Throwable throwable) {
throw throwable;
} finally {
removeTraceId();
}
return result;
}
/**
* 设置traceId
*/
public static void buildTraceId() {
try {
MDC.put(traceId, UUID.randomUUID().toString().replace("-", ""));
} catch (Exception e) {
log.error("set traceId no exception", e);
}
}
/**
* remove traceId
*/
public static void removeTraceId() {
try {
MDC.remove(traceId);
} catch (Exception e) {
log.error("remove traceId no exception", e);
}
}
}
此处我采用的是log back,如果是log4j或者log4j2还是有一些区别的,比如说MDC.getCopyOfContextMap()。
public class MDCTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 此时获取的是父线程的上下文数据
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
// 内部为子线程的领域范围,所以将父线程的上下文保存到子线程上下文中,而且每次submit/execute调用都会更新为最新的上 // 下文对象
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
// 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因
MDC.clear();
}
};
}
}
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(5);
//配置最大线程数
executor.setMaxPoolSize(10);
//配置队列大小
executor.setQueueCapacity(100);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("mdc-trace-");
// 异步MDC
executor.setTaskDecorator(new MDCTaskDecorator());
//执行初始化
executor.initialize();
return executor;
}
这样就是先了traceId传递到线程池中了。
与上面的不同我们如果用的不是spring的线程池那么无法实现TaskDecorator接口,那么就无法实现他的功能了,此时我们就会定义我们自身的线程装配器。
public class MDCTaskDecorator {
public static <T> Callable<T> buildCallable(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (CollectionUtils.isEmpty(context)) {
MDC.clear();
} else {
//MDC.put("trace_id", IdUtil.objectId());
MDC.setContextMap(context);
}
try {
return callable.call();
} finally {
// 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因
MDC.clear();
}
};
}
public static Runnable buildRunnable(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (CollectionUtils.isEmpty(context)) {
MDC.clear();
} else {
//MDC.put("trace_id", IdUtil.objectId());
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
// 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因
MDC.clear();
}
};
}
}
清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因
主线程中,如果使用了线程池,会导致线程池中丢失MDC信息;解决办法:需要我们自己重写线程池,在调用线程跳动run之前,获取到主线程的MDC信息,重新put到子线程中的。
public class ThreadPoolMDCExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable task) {
super.execute(MDCTaskDecorator.buildRunnable(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(MDCTaskDecorator.buildRunnable(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(MDCTaskDecorator.buildCallable(task, MDC.getCopyOfContextMap()));
}
}
public class ThreadPoolMDCScheduler extends ThreadPoolTaskScheduler {
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
return super.scheduleWithFixedDelay(MDCTaskDecorator.buildRunnable(task), startTime, delay);
}
@Override
public ScheduledFuture<?> schedule(Runnable task, Date startTime) {
return super.schedule(MDCTaskDecorator.buildRunnable(task), startTime);
}
}
同理,即使你使用ExecutorCompletionService实现多线程调用,也是相同的方案和思路机制。
使用CompletableFuture实现多线程调用,其中收集CompletableFuture运行结果,也可以手动使用相似的思路进行填充上下文信息数据,但是别忘记了清理clear就好。
private CompletableFuture<Result> test() {
Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
return CompletableFuture.supplyAsync(() -> {
MDC.setContextMap(copyOfContextMap);
//执行业务操作
MDC.clear();
return new Result();
}, threadPoolExecutor).exceptionally(new Function<Throwable, Result>() {
@Override
public Result apply(Throwable throwable) {
log.error("线程[{}]发生了异常[{}], 继续执行其他线程", Thread.currentThread().getName(), throwable.getMessage());
MDC.clear();
return null;
}
});
}
小伙伴们可以动手试试看!