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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【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 了异常,而没有继续往外抛,则不会进入此回调函数

正常运作流程

image.png

异常运作流程

image.png



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;
            }
        });
    }




相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
Java UED Sentinel
微服务守护神:Spring Cloud Sentinel,让你的系统在流量洪峰中稳如磐石!
【8月更文挑战第29天】Spring Cloud Sentinel结合了阿里巴巴Sentinel的流控、降级、熔断和热点规则等特性,为微服务架构下的应用提供了一套完整的流量控制解决方案。它能够有效应对突发流量,保护服务稳定性,避免雪崩效应,确保系统在高并发下健康运行。通过简单的配置和注解即可实现高效流量控制,适用于高并发场景、依赖服务不稳定及资源保护等多种情况,显著提升系统健壮性和用户体验。
83 1
|
1月前
|
Web App开发 存储 监控
iLogtail 开源两周年:UC 工程师分享日志查询服务建设实践案例
本文为 iLogtail 开源两周年的实践案例分享,讨论了 iLogtail 作为日志采集工具的优势,包括它在性能上超越 Filebeat 的能力,并通过一系列优化解决了在生产环境中替换 Filebeat 和 Logstash 时遇到的挑战。
|
14天前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
29 0
|
1月前
|
开发工具 git
git显示开发日志+WinSW——将.exe文件注册为服务的一个工具+图床PicGo+kubeconfig 多个集群配置 如何切换
git显示开发日志+WinSW——将.exe文件注册为服务的一个工具+图床PicGo+kubeconfig 多个集群配置 如何切换
38 1
|
1月前
|
存储 缓存 网络协议
搭建dns服务常见报错--查看/etc/named.conf没有错误日志信息却显示出错(/etc/named.conf:49: missing ‘;‘ before ‘include‘)及dns介绍
搭建dns服务常见报错--查看/etc/named.conf没有错误日志信息却显示出错(/etc/named.conf:49: missing ‘;‘ before ‘include‘)及dns介绍
110 0
|
2月前
|
SQL 人工智能 运维
在阿里云日志服务轻松落地您的AI模型服务——让您的数据更容易产生洞见和实现价值
您有大量的数据,数据的存储和管理消耗您大量的成本,您知道这些数据隐藏着巨大的价值,但是您总觉得还没有把数据的价值变现出来,对吗?来吧,我们用一系列的案例帮您轻松落地AI模型服务,实现数据价值的变现......
191 3
|
3月前
|
消息中间件 Java RocketMQ
微服务架构师的福音:深度解析Spring Cloud RocketMQ,打造高可靠消息驱动系统的不二之选!
【8月更文挑战第29天】Spring Cloud RocketMQ结合了Spring Cloud生态与RocketMQ消息中间件的优势,简化了RocketMQ在微服务中的集成,使开发者能更专注业务逻辑。通过配置依赖和连接信息,可轻松搭建消息生产和消费流程,支持消息过滤、转换及分布式事务等功能,确保微服务间解耦的同时,提升了系统的稳定性和效率。掌握其应用,有助于构建复杂分布式系统。
65 0
|
3月前
|
存储 Java Spring
【Azure Spring Cloud】Azure Spring Cloud服务,如何获取应用程序日志文件呢?
【Azure Spring Cloud】Azure Spring Cloud服务,如何获取应用程序日志文件呢?
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
162 2