本文正在参与「金石方案 . 分割6万现金大奖」

接受前文

针对于上一篇【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 了反常,而没有持续往外抛,则不会进入此回调函数

正常运作流程

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

反常运作流程

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

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
@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完成多线程调用,其中收集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;
            }
        });
    }

小伙伴们能够动手试试看!

本文正在参与「金石方案 . 分割6万现金大奖」