写点什么

【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「SpringAOP 整合篇」

作者:洛神灬殇
  • 2022-11-20
    江苏
  • 本文字数:6544 字

    阅读完需:约 21 分钟

【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「SpringAOP 整合篇」

承接前文

针对于上一篇【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「Logback-MDC 篇」的功能开发指南之后,相信你对于 Sl4fj 以及 Log4j 整个生态体系的功能已经有了一定的大致的了解了,接下来我们需要进行介绍关于实现如何将 MDC 的编程模式改为声明模式的技术体系,首先再我们的基础机制而言,采用的是 Spring 的 AOP 体系,所以我们先来解决说明一下 Spring 的 AOP 技术体系。

Spring-AOP 注解概述

  • Spring 的 AOP 功能除了在配置文件中配置一大堆的配置,比如:切入点表达式通知等等以外,使用注解的方式更为方便快捷,特别是 Spring boot 出现以后,基本不再使用原先的 beans.xml 等配置文件了,而都推荐注解编程。

  • 对于习惯了 Spring 全家桶编程的人来说,并不是需要直接引入 aspectjweaver 依赖,因为 spring-boot-starter-aop 组件默认已经引用了 aspectjweaver 来实现 AOP 功能。换句话说 Spring 的 AOP 功能就是依赖的 aspectjweaver !

AOP 的基本概念

AOP Proxy:AOP 框架创建的对象,代理就是目标对象的加强。Spring 中的 AOP 代理可以使 JDK 动态代理,也可以是 CGLIB 代理,前者基于接口,后者基于子类。

AOP 的注解定义

Aspect(切面)标注在类、接口(包括注解类型)或枚举上

@Aspect(切面): 切面声明,标注在类、接口(包括注解类型)或枚举上,JointPoint(连接点):  程序执行过程中明确的点,一般是方法的调用

Advice(通知):  AOP 在特定的切入点上执行的增强处理
  • @Before:  标识一个前置增强方法,相当于 BeforeAdvice 的功能

  • 前置通知, 在目标方法(切入点)执行之前执行。

  • value 属性绑定通知的切入点表达式,可以关联切入点声明,也可以直接设置切入点表达式

  • 如果在此回调方法中抛出异常,则目标方法不会再执行,会继续执行后置通知 -> 异常通知。

  • @After:  final 增强,不管是抛出异常或者正常退出都会执行,后置通知, 在目标方法(切入点)执行之后执行

  • 后置通知, 在目标方法(切入点)执行之后执行

  • @Around: 环绕增强,相当于 MethodInterceptor

  • 环绕通知:目标方法执行前后分别执行一些代码,类似拦截器,可以控制目标方法是否继续执行。

  • 通常用于统计方法耗时,参数校验等等操作。

  • 环绕通知早于前置通知,晚于返回通知。

  • @AfterReturning:  后置增强,似于 AfterReturningAdvice, 方法正常退出时执行

  • 返回通知, 在目标方法(切入点)返回结果之后执行,在 @After 的后面执行

  • pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 ""

  • @AfterThrowing:  异常抛出增强,相当于 ThrowsAdvice

  • 异常通知, 在方法抛出异常之后执行, 意味着跳过返回通知

  • pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 ""

  • 如果目标方法自己 try-catch 了异常,而没有继续往外抛,则不会进入此回调函数




正常运作流程



异常运作流程


Pointcut(切入点)

@Pointcut(切入点):   带有通知的连接点,在程序中主要体现为书写切入点表达式,切入点声明,即切入到哪些目标类的目标方法。value 属性指定切入点表达式,默认为 "",用于被通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式


Pointcut 表示式(expression)和签名(signature)


@Pointcut("execution(* com.savage.aop.MessageSender.*(..))")//Point签名private void pointCutRange(){}
复制代码


切入点表达式(非注解定位靶向)


  • execution:用于匹配方法执行的连接点;

  • within:用于匹配指定类型内的方法执行;

  • this:用于匹配当前 AOP 代理对象类型的执行方法;注意是 AOP 代理对象的类型匹配,这样就可能包括引入接口也类型匹配;

  • target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;

  • args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;


execution 表达式格式


切入点表达式通过 execution 函数匹配连接点,语法:execution([方法修饰符] 返回类型 包名.类名.方法名(参数类型) [异常类型])


execution 的表达式的解析器机制


execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
复制代码


其中后面跟着“?”的是可选项,括号中各个 pattern 分别表示:


  • 修饰符匹配(modifier-pattern?)例如:public、private、protected 等当然也可以不写

  • 返回值匹配(ret-type-pattern)可以为*表示任何返回值,全路径的类名等

  • 类路径匹配(declaring-type-pattern?)

  • 方法名匹配(name-pattern)可以指定方法名 或者 代表所有, set 代表以 set 开头的所有方法

  • 参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用"*" 来表示匹配任意类型的参数,".."表示零个或多个任意参数。

  • 如(String)表示匹配一个 String 参数的方法;(*,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是 String 类型

  • 异常类型匹配(throws-pattern?)


execution 的表达式的解析规则机制


  • 返回值类型、包名、类名、方法名可以使用星号*代表任意;

  • 包名与类名之间一个点.代表当前包下的类,两个点..表示当前包及其子包下的类;

  • 参数列表可以使用两个点..表示任意个数,任意类型的参数列表;

  • 切入点表达式的写法比较灵活,比如:* 号表示任意一个,.. 表示任意多个,还可以使用 &&、||、! 进行逻辑运算。

Pointcut 使用详细语法:

任意公共方法的执行


execution(public * *(..))
复制代码


任何一个以“set”开始的方法的执行


execution(* set*(..))
复制代码


com.xyz.service.XXXService 接口的任意方法的执行


execution(* com.xyz.service.XXXService.*(..))
复制代码


定义在 com.xyz.service 包里的任意方法的执行


execution(* com.xyz.service.*.*(..))
复制代码


定义在 service 包和所有子包里的任意类的任意方法的执行


execution(* com.xyz.service..*.*(..))
复制代码


第一个表示匹配任意的方法返回值, ..(两个点)表示零个或多个,第一个..表示 service 包及其子包,第二个表示所有类, 第三个*表示所有方法,第二个..表示方法的任意参数个数


定义在 com.xx.test 包和所有子包里的 test 类的任意方法的执行:


execution(* com.xx.test..test.*(..))")
复制代码


com.xx.test 包里的任意类:


within(com.xx.test.*)
复制代码


pointcutexp 包和所有子包里的任意类:


within(com.xx.test..*)
复制代码
实现了 TestService 接口的所有类,如果 TestService 不是接口,限定 TestService 单个类:
this(com.xx.TestService)
复制代码


切入点表达式(注解定位靶向)


  • @within:用于匹配所以持有指定注解类型内的方法;

  • @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;

  • @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;

  • @annotation:用于匹配当前执行方法持有指定注解的方法;

案例解决介绍

带有 @Transactional 标注的所有类的任意方法:


  • @within 和 @target 针对类的注解


@within(org.springframework.transaction.annotation.Transactional)@target(org.springframework.transaction.annotation.Transactional)
复制代码


带有 @Transactional 标注的任意方法:


  • @annotation 是针对方法的注解


@annotation(org.springframework.transaction.annotation.Transactional)
复制代码


总结一下对应的注解类信息


  • @args(org.springframework.transaction.annotation.Transactional),参数带有 @Transactional 标注的方法


同一个方法被多个 Aspect 类拦截


优先级高的切面类里的增强处理的优先级总是比优先级低的切面类中的增强处理的优先级高。Spring 提供了如下两种解决方案指定不同切面类里的增强处理的优先级


  • 让切面类实现 org.springframework.core.Ordered 接口:实现该接口的 int getOrder()方法,该方法返回值越小,优先级越高

  • 直接使用 @Order 注解来修饰一个切面类:使用这个注解时可以配置一个 int 类型的 value 属性,该属性值越小,优先级越高


但是,同一个切面类里的两个相同类型的增强处理在同一个连接点被织入时,Spring AOP 将以随机的顺序来织入这两个增强处理,没有办法指定它们的织入顺序。即使给这两个 advice 添加了 @Order 这个注解,也不行!

开展实际开发 AOP 切面类机制体系

新增标记注解

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 {}
复制代码

指定 @MDC 切面类

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@Aspectjpublic 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 实现多线程调用,其中收集 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;            }        });    }
复制代码


小伙伴们可以动手试试看!

发布于: 刚刚阅读数: 3
用户头像

洛神灬殇

关注

🏆InfoQ写作平台-签约作者🏆 2020-03-25 加入

【个人简介】酷爱计算机科学、醉心编程技术、喜爱健身运动、热衷悬疑推理的“极客达人” 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、微服务/分布式体系和算法设计等

评论

发布
暂无评论
【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「SpringAOP 整合篇」_log4j_洛神灬殇_InfoQ写作社区