【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日志并进行多维度分析。
相关文章
|
8天前
|
存储 安全 Java
Spring Boot 3 集成Spring AOP实现系统日志记录
本文介绍了如何在Spring Boot 3中集成Spring AOP实现系统日志记录功能。通过定义`SysLog`注解和配置相应的AOP切面,可以在方法执行前后自动记录日志信息,包括操作的开始时间、结束时间、请求参数、返回结果、异常信息等,并将这些信息保存到数据库中。此外,还使用了`ThreadLocal`变量来存储每个线程独立的日志数据,确保线程安全。文中还展示了项目实战中的部分代码片段,以及基于Spring Boot 3 + Vue 3构建的快速开发框架的简介与内置功能列表。此框架结合了当前主流技术栈,提供了用户管理、权限控制、接口文档自动生成等多项实用特性。
42 8
|
2月前
|
测试技术 开发工具 git
写了BUG还想跑——闲鱼异常日志问题自动追踪-定位-分发机制
为了高效地发现、定位和解决预发问题,闲鱼团队研发了一套异常日志问题自动追踪-定位-分发机制。这套机制通过自动化手段,实现了异常日志的定时扫描、精准定位和自动分发,显著降低了开发和测试的成本,提高了问题解决的效率。
134 15
写了BUG还想跑——闲鱼异常日志问题自动追踪-定位-分发机制
|
1月前
|
运维 监控 Cloud Native
一行代码都不改,Golang 应用链路指标日志全知道
本文将通过阿里云开源的 Golang Agent,帮助用户实现“一行代码都不改”就能获取到应用产生的各种观测数据,同时提升运维团队和研发团队的幸福感。
162 10
|
1月前
|
存储 监控 安全
什么是事件日志管理系统?事件日志管理系统有哪些用处?
事件日志管理系统是IT安全的重要工具,用于集中收集、分析和解释来自组织IT基础设施各组件的事件日志,如防火墙、路由器、交换机等,帮助提升网络安全、实现主动威胁检测和促进合规性。系统支持多种日志类型,包括Windows事件日志、Syslog日志和应用程序日志,通过实时监测、告警及可视化分析,为企业提供强大的安全保障。然而,实施过程中也面临数据量大、日志管理和分析复杂等挑战。EventLog Analyzer作为一款高效工具,不仅提供实时监测与告警、可视化分析和报告功能,还支持多种合规性报告,帮助企业克服挑战,提升网络安全水平。
|
3月前
|
Web App开发 存储 监控
iLogtail 开源两周年:UC 工程师分享日志查询服务建设实践案例
本文为 iLogtail 开源两周年的实践案例分享,讨论了 iLogtail 作为日志采集工具的优势,包括它在性能上超越 Filebeat 的能力,并通过一系列优化解决了在生产环境中替换 Filebeat 和 Logstash 时遇到的挑战。
157 13
|
2月前
|
存储 Linux Docker
centos系统清理docker日志文件
通过以上方法,可以有效清理和管理CentOS系统中的Docker日志文件,防止日志文件占用过多磁盘空间。选择合适的方法取决于具体的应用场景和需求,可以结合手动清理、logrotate和调整日志驱动等多种方式,确保系统的高效运行。
225 2
|
3月前
|
XML JSON 监控
告别简陋:Java日志系统的最佳实践
【10月更文挑战第19天】 在Java开发中,`System.out.println()` 是最基本的输出方法,但它在实际项目中往往被认为是不专业和不足够的。本文将探讨为什么在现代Java应用中应该避免使用 `System.out.println()`,并介绍几种更先进的日志解决方案。
85 1
|
3月前
|
监控 网络协议 安全
Linux系统日志管理
Linux系统日志管理
75 3
|
3月前
|
监控 应用服务中间件 网络安全
#637481#基于django和neo4j的日志分析系统
#637481#基于django和neo4j的日志分析系统
50 4
|
3月前
|
开发工具 git
git显示开发日志+WinSW——将.exe文件注册为服务的一个工具+图床PicGo+kubeconfig 多个集群配置 如何切换
git显示开发日志+WinSW——将.exe文件注册为服务的一个工具+图床PicGo+kubeconfig 多个集群配置 如何切换
52 1