• 【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(){}
    
    • 1
    • 2
    • 3
    切入点表达式(非注解定位靶向)
    • 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?)
    
    • 1

    其中后面跟着“?”的是可选项,括号中各个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 * *(..))
    
    • 1
    任何一个以“set”开始的方法的执行
    execution(* set*(..))
    
    • 1
    com.xyz.service.XXXService 接口的任意方法的执行
    execution(* com.xyz.service.XXXService.*(..))
    
    • 1
    定义在com.xyz.service包里的任意方法的执行
    execution(* com.xyz.service.*.*(..))
    
    • 1
    定义在service包和所有子包里的任意类的任意方法的执行
    execution(* com.xyz.service..*.*(..))
    
    • 1

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

    定义在com.xx.test包和所有子包里的test类的任意方法的执行:
    execution(* com.xx.test..test.*(..))")
    
    • 1
    com.xx.test包里的任意类:
    within(com.xx.test.*)
    
    • 1
    pointcutexp包和所有子包里的任意类:
    within(com.xx.test..*)
    
    • 1
    实现了TestService接口的所有类,如果TestService不是接口,限定TestService单个类:
    this(com.xx.TestService)
    
    • 1
    切入点表达式(注解定位靶向)
    • @within:用于匹配所以持有指定注解类型内的方法;
    • @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
    • @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;
    • @annotation:用于匹配当前执行方法持有指定注解的方法;
    案例解决介绍
    带有@Transactional标注的所有类的任意方法:
    • @within和@target针对类的注解
    @within(org.springframework.transaction.annotation.Transactional)
    @target(org.springframework.transaction.annotation.Transactional)
    
    • 1
    • 2
    带有@Transactional标注的任意方法:
    • @annotation是针对方法的注解
    @annotation(org.springframework.transaction.annotation.Transactional)
    
    • 1
    总结一下对应的注解类信息
    • @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 {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    指定 @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
    @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);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    定义线程装饰器

    此处我采用的是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();
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    定义线程池
    @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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这样就是先了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();
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    清除子线程的,避免内存溢出,就和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()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    自定义线程池进行封装包装操作(任务调度线程池)
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    同理,即使你使用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;
                }
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

  • 相关阅读:
    Ubuntu安装中文man手册
    【路径规划】如何给路径增加运动对象
    ICCV2023论文阅读速览自适应Adaptation28篇
    Redis 持久化
    Qt---文件系统
    CSS - 浮动布局(float)
    前端面试题(附答案)持续更新中……
    【单片机】LED模块和独立按键的使用
    ICSFUZZ:操纵I/O、二进制代码重用以及插桩,来Fuzzing工业控制应用程序
    粒子群算法和鲸鱼算法的比较(Matlab代码实现)
  • 原文地址:https://blog.csdn.net/l569590478/article/details/127944409