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

news2025/1/13 13:14:06

承接前文

针对于上一篇【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
@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;
            }
        });
    }

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/19427.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

家庭实验室系列文章-如何迁移树莓派系统到更大的 SD 卡?

前言 其实这个专题很久很久之前就想写了&#xff0c;但是一直因为各种原因拖着没动笔。 因为没有资格&#xff0c;也没有钱在一线城市买房 (&#x1f602;&#x1f602;&#x1f602;); 但是在要结婚之前&#xff0c;婚房又是刚需。 我和太太最终一起在一线城市周边的某二线城…

【面试题】详解Cookie、localStorage、sessionStorage区别

【面试题】详解Cookie、localStorage、sessionStorage区别 三者基本概念 Cookie localStorage sessionStorage 安全性的考虑 Cookie、localStorage、sessionStorage、indexedDB对比 应用场景 Token一般放在哪里&#xff1f;&#xff1f;&#xff1f; 放在Cookie 放…

【openGauss】在WPS表格里制作连接到openGauss的实时刷新报表

前言 其实我的数据库启蒙&#xff0c;是在一家甲方公司。 当时一进这家公司&#xff0c;就见到了通过连接数据库自动刷新的excel表。当时学会了这招就一发不可收拾&#xff0c;制作出各种自动刷新的报表。 想象一下&#xff0c;有些高管不喜欢打开各种复杂的业务系统或者报表系…

【JS基础】在js中如何简单的使用正则表达式

文章目录前言创建正则字符类型的匹配方法searchreplacematch正则的匹配方法test转义特殊符号冲突正则创建问题记一些规则符号修饰符原子表[]和原子组()配合转义字符其他字符量词一些实用的正则前言 关于正则表达式的介绍&#xff0c;推荐看这篇文章正则表达式30分钟入门教程&a…

[附源码]java毕业设计商务酒店管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

鲁棒无范围定位算法 (RRGA)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

LVS-NAT集群搭建

目录 一、环境准备 1、准备三台centos服务器 2、实验拓扑 3、NAT模式介绍 二、LVS-NAT模式部署 1、给lvs服务器安装LVS 2、新建LVS集群 3、添加Real Server服务器节点 4、开启路由转发 5、给后端web服务器配置网关 6、效果测试 一、环境准备 1、准备三台centos服务器…

【C++右值引用】左右值的交叉引用的具体情景,右值详讲

目录 1.右值和左值 2.左值引用和右值引用 3.左右值的交叉引用的具体情景 3.4当不接受返回值就没有办法优化 1.右值和左值 左值与右值是C语言中的概念&#xff0c;但C标准并没有给出严格的区分方式&#xff0c;一般认为&#xff1a;可以放在左边的&#xff0c;或者能 够取地…

基于stm32单片机的光照检测智能台灯

资料编号&#xff1a;101 下面是相关功能视频演示&#xff1a; 101-基于stm32单片机的光照检测智能台灯照明灯Proteus仿真&#xff08;仿真源码全套资料&#xff09;功能介绍&#xff1a; 1、设置为自动模式下&#xff1a;可以检测光照强度&#xff0c;当光照强度<100Lux的…

QT QThread 多线程操作

在QT中&#xff0c;QT应用程序所在的线程为主线程&#xff0c;也称为“GUI线程”&#xff0c;QT GUI必须运行在此线程上&#xff1b;而非主线程称为“工作者线程”&#xff0c;主要处理从主线程中卸下的一些工作&#xff0c;例如数据的同步访问等。需要明确的是&#xff0c;同一…

SSH框架过时了吗?那就最后分享一份阿里架构师整合的SSH框架实战心得吧!

记得当年 java 的企业级框架还是 ssh 的天下&#xff08;spring&#xff0c;struts和hibernate&#xff09;&#xff0c;但是现在感觉 spring 已经完全把那两个框架甩在后边了。用 spring 的人越来越多&#xff0c;用 struts 的人比原来少多了&#xff0c;用 hibernate 的就更少…

BI-SQL丨SNAPSHOT

快照&#xff08;SNAPSHOT&#xff09; 我们在做BI项目的过程&#xff0c;一旦数据涉及到数据库&#xff0c;那么需要考量到的点就比较多。 1.如果数仓是在项目过程中搭建的&#xff0c;那么需要考虑高可用、灾备机制以及安全性问题&#xff1b; 2.如果我们只是需要连接数据库…

Vue3留言墙项目——头部和底部静态页面搭建

文章目录创建项目头部底部创建项目 Vue中使用scss 头部 头部当中有两个按钮&#xff0c;然后根据设计稿可知&#xff0c;本留言墙中有4个按钮&#xff0c;所以可以自己封装一个按钮组件 按钮组件的博客 components/TopNav.vue <template><div class"topNav…

Vue封装一个按钮组件(不使用框架)

做留言墙项目&#xff0c;根据设计稿&#xff0c;发现有四种按钮&#xff0c;这里不使用框架&#xff0c;自己写一个按钮组件 在components下新建MyButton/MyButton.vue <template><button :class"my-btn btn-${type}"><slot></slot></b…

PLC学习笔记(一):概述

如今&#xff0c;电气装置的控制愈发复杂&#xff0c;仅仅依靠低压电器构建逻辑控制电路显得捉襟见肘&#xff0c;而将逻辑控制电路软件化是在满足控制需求前提下降低成本、提高可靠性的重要途经。 那么&#xff0c;我们是选择单片机还是PLC呢&#xff1f;若选择使用单片机&…

嵌入式开发学习之--初识stm32函数库

提示&#xff1a;本篇文章主要以了解为主。 文章目录前言一、库目录及文件简介二、常用资料总结前言 上一篇说到&#xff0c;其实我们不必去直接操作寄存器&#xff0c;也不必自己去写库函数&#xff0c;stm32官方函数库已经满足我们绝大部分的需求了&#xff0c;这一篇文章&a…

逻辑漏洞挖掘

逻辑漏洞# 逻辑漏洞是指由于程序逻辑输入管控不严或者逻辑太复杂&#xff0c;导致程序不能够正常处理或处理错误&#xff0c;逻辑漏洞根据功能需求的不同产生的漏洞方式也不同。一般出现在网站程序的登录注册、密码找回、验证方式、信息查看、交易支付金额等地方。 这类漏洞不…

【第十五章 java反射机制,获取Class类的实例,创建运行时类的对象,调用运行时类中指定的结构】

第十五章 java反射机制&#xff0c;获取Class类的实例&#xff0c;创建运行时类的对象&#xff0c;调用运行时类中指定的结构 1.java反射机制概述 加载完类之后&#xff0c;在堆内存的方法区中就产生了一个Class类型的对象&#xff08;一个类只有一个Class对象&#xff09;&am…

[计算机毕业设计]改进粒子群算法的监测资源调度

前言 &#x1f4c5;大四是整个大学期间最忙碌的时光,一边要忙着准备考研,考公,考教资或者实习为毕业后面临的就业升学做准备,一边要为毕业设计耗费大量精力。近几年各个学校要求的毕设项目越来越难,有不少课题是研究生级别难度的,对本科同学来说是充满挑战。为帮助大家顺利通过…

【学海】中位数(Median)的介绍以及如何在不同条件下计算中位数

一、什么是中位数 中位数是指将数据按大小顺序排列起来&#xff0c;形成一个数列&#xff0c;居于数列中间位置的那个数据。中位数用Me表示。 从中位数的定义可知&#xff0c;所研究的数据中有一半小于中位数&#xff0c;一半大于中位数。中位数的作用与算术平均数相近&#…