如何用AspectJ在Spring组件中实现嵌套日志功能?
问题场景
在基于Java的Spring Boot Gradle项目中,原本通过Lombok的@Cleanup手动初始化实现AutoCloseable的CustomLogger实例处理日志,但这种方式存在代码重复且触发Sonar问题。于是改用自定义@Loggable注解结合Spring AOP实现日志切面,代码如下:
日志切面代码
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component @Slf4j public class LoggingAspect { private ThreadLocal<CustomLogger> businessLoggingThreadLocal = new ThreadLocal<>(); @Before("@annotation(Loggable)") public void logMethodStart(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); CustomLogger businessLogging = new CustomLogger(log, methodName, args); businessLoggingThreadLocal.set(businessLogging); businessLogging.logMethodStart(); } @AfterReturning(pointcut = "@annotation(Loggable)", returning = "result") public void logMethodEnd(JoinPoint joinPoint, Object result) { CustomLogger businessLogging = businessLoggingThreadLocal.get(); if (businessLogging != null) { businessLogging.close(); businessLoggingThreadLocal.remove(); } } @AfterThrowing(pointcut = "@annotation(Loggable)", throwing = "exception") public void logMethodException(JoinPoint joinPoint, Throwable exception) { CustomLogger businessLogging = businessLoggingThreadLocal.get(); if (businessLogging != null) { businessLogging.close(); businessLoggingThreadLocal.remove(); } } }
自定义注解代码
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 Loggable {}
但出现问题:同一Spring组件内的方法调用无法触发日志切面,跨组件调用则正常。例如:
@Service public class ExampleService { @Loggable public void someMethod(){ // 业务逻辑 anOtherMethod(); } @Loggable public void anOtherMethod(){ // 业务逻辑 } }
预期日志输出:
Method started: someMethod() Method started: anOtherMethod() Method ended: anOtherMethod() Method ended: someMethod()
实际日志输出:
Method started: someMethod() Method ended: someMethod()
使用的Gradle插件为id("io.freefair.aspectj") version "8.0.1",依赖为implementation("org.aspectj:aspectjrt:1.9.7")。
原因分析
Spring AOP默认基于动态代理实现:调用Spring组件方法时,实际调用的是代理对象的方法,代理会触发切面逻辑。但组件内部方法调用是直接调用目标对象的方法,不走代理,因此切面不会生效。
解决方案
方案1:使用AspectJ编译时织入(CTW)实现全方法拦截
AspectJ是功能更强大的AOP框架,通过直接修改字节码实现切面织入,不受Spring动态代理限制,能拦截组件内部方法调用,完美解决嵌套日志问题。
Gradle配置调整
修改build.gradle,启用AspectJ编译时织入:
plugins { id 'java' id 'org.springframework.boot' version '3.2.0' id 'io.spring.dependency-management' version '1.1.4' id "io.freefair.aspectj" version "8.0.1" } aspectj { compileWeave = true // 开启编译时织入 } dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.aspectj:aspectjrt:1.9.7' aspect 'org.aspectj:aspectjweaver:1.9.7' // 编译时织入必须依赖 // 其他项目依赖... }
代码无需修改
原有的LoggingAspect和@Loggable注解代码保持不变,AspectJ会在编译阶段将切面逻辑织入所有标注@Loggable的方法中,包括组件内部的方法调用。
方案2:自注入代理对象(临时 workaround)
如果不想引入AspectJ,可以在组件内注入自身的代理实例,调用内部方法时通过代理对象触发切面:
@Service public class ExampleService { @Autowired private ExampleService self; // 注入自身的代理对象 @Loggable public void someMethod(){ // 业务逻辑 self.anOtherMethod(); // 通过代理调用内部方法 } @Loggable public void anOtherMethod(){ // 业务逻辑 } }
这种方式有代码侵入性,仅适合临时解决个别场景,不推荐大规模使用。
方案3:重构代码拆分组件
将需要日志的方法拆分到不同的Spring组件中,通过跨组件调用触发Spring AOP切面。但该方案仅适用于业务逻辑适合拆分的场景,通用性不强。
总结
- 最优方案:使用AspectJ编译时织入,彻底解决同组件内嵌套日志问题,无代码侵入性,适配所有场景。
- 临时方案:自注入代理对象,适合小范围临时解决,但代码侵入性高。
内容的提问来源于stack exchange,提问作者Norbert Maszlag

