AspectJ切面执行两次原因分析

本文探讨了在使用AspectJ进行AOP编程时遇到的拦截器双调用现象,通过代码示例和深入分析揭示了问题根源在于Ajc编译器的bug,并提供了两种有效的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

                       

背景
转眼之间,发现博客已经将近半年没更新了,甚是惭愧。话不多说,正如标题所言,最近在使用AspectJ的时候,发现拦截器(AOP切面)执行了两次了。我们知道,AspectJ是AOP的一种解决方案,本质上是通过代理类在目标方法执行通知(Advice),然后由代理类再去调用目标方法。所以,从这点讲,拦截器应该只会执行一次。但是在测试的时候发现拦截器执行了两次。

问题重现
既然问题已经明了,那么可以通过代码简单重现这个问题,从而更深层次分析到底是什么原因导致的。

定义一个注解:

package com.rhwayfun.aspect;import java.lang.annotation.*;@Target({ElementType.METHOD})@Retention(RetentionPolicy.CLASS)@Documentedpublic @interface StatsService {}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

为该注解定义切面:

package com.rhwayfun.aspect;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.slf4j.Logger;import org.slf4j.LoggerFactory;@Aspectpublic class StatsServiceInterceptor {    private static Logger log = LoggerFactory.getLogger(StatsServiceInterceptor.class);    @Around("@annotation(StatsService)")    public Object invoke(ProceedingJoinPoint pjp) {        try {            log.info("before invoke target.");            return pjp.proceed();        } catch (Throwable e) {            log.error("invoke occurs error:", e);            return null;        } finally {            log.info("after invoke target.");        }    }}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

方法测试:

package com.rhwayfun;import com.rhwayfun.aspect.StatsService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.time.LocalDateTime;public class AspectTest {    private static Logger log = LoggerFactory.getLogger(AspectTest.class);    public static void main(String[] args) {        AspectTest.print();    }    @StatsService    public static void print(){        log.info("Now: {}", LocalDateTime.now());    }}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

输出结果:

AspectJ测试

debug分析
由于是静态织入,所以可以通过反编译工具查看编译后的文件,如下:

public class AspectTest{    private static Logger log;    private static final /* synthetic */ JoinPoint$StaticPart ajc$tjp_0;    private static final /* synthetic */ JoinPoint$StaticPart ajc$tjp_1;    public static void main(final String[] args) {        StatsServiceInterceptor.aspectOf().invoke(((AroundClosure)new AspectTest$AjcClosure1(new Object[] { Factory.makeJP(AspectTest.ajc$tjp_0, (Object)null, (Object)null) })).linkClosureAndJoinPoint(0));    }    @StatsService    public static void print() {        StatsServiceInterceptor.aspectOf().invoke(((AroundClosure)new AspectTest$AjcClosure3(new Object[] { Factory.makeJP(AspectTest.ajc$tjp_1, (Object)null, (Object)null) })).linkClosureAndJoinPoint(65536));    }    static {        ajc$preClinit();        AspectTest.log = LoggerFactory.getLogger((Class)AspectTest.class);    }    private static /* synthetic */ void ajc$preClinit() {        final Factory factory = new Factory("AspectTest.java", (Class)AspectTest.class);        ajc$tjp_0 = factory.makeSJP("method-call", (Signature)factory.makeMethodSig("9", "print", "com.rhwayfun.AspectTest", "", "", "", "void"), 17);        ajc$tjp_1 = factory.makeSJP("method-execution", (Signature)factory.makeMethodSig("9", "print", "com.rhwayfun.AspectTest", "", "", "", "void"), 22);    }}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

请注意两个连接点:ajc$tjp_0ajc$tjp_1,这两个连接点是产生两次调用的关键,问题注解明明是加上print()方法上的,为什么main()方法也被注入了通知呢?正因为main()方法也织入了通知,所以就形成了A call B, B call print()的调用链,有两次method-call,一次method-executionmethod-execution才是我们的目标方法print(),所以我们才看到了两次输出。

 

method-callmethod-execution都是连接点ProceedingJoinPointkind属性

其实,这属于Ajc编译器的一个Bug,详见Ajc-bug

所以,到这一步,问题就很清晰了,因为Ajc编辑器的bug,导致了在main方法中也织入了通知,所以在执行的时候,输出了两次日志。

解决方法

方案一
因为两次调用的kind属性不一样,所以可以通过kind属性来判断时候调用切面。这样显得不优雅,而且如果切面有更多的逻辑的话,需要加各种if-else的判断,所以不推荐。

方法二
更优雅的方案是修改@Around("@annotation(StatsService)")的逻辑,改为@Around("execution(* *(..)) && @annotation(StatsService)")

重新运行上面的测试类,结果如下:

AspectJ测试

           

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.youkuaiyun.com/jiangjunshow

<think>首先,用户在询问Spring中用于标记切面注解。用户提供了两个引用:[^1]和[^2],这些是来自系统引用的内容。引用[^1]提到@EnableAspectJAutoProxy注解,引用[^2]讨论SpringBoot中的切面依赖。用户的上一次问题或上下文没有被直接显示,但系统指令说"参考站内引用"和"参考用户上一次的问题和你的回答"。在上一次交互中,可能没有直接的对话历史,所以我需要基于当前查询。用户说:"我想查找用于标记切面的Spring注解",具体是"请问Spring中用于标记切面注解"。所以,焦点是识别Spring中用于定义切面注解。回顾SpringAOP的知识:-SpringAOP中使用@Aspect注解来标记一个切面。-其他注解如@Pointcut、@Before、@After等用于定义切点和通知。-@EnableAspectJAutoProxy注解用于启用AspectJ代理,但它自身不是标记切面的;它启用了AspectJ自动代理,从而允许Spring识别@Aspect注解。所以,直接用于"标记切面注解"应该是@Aspect。现在,查看提供的引用:-引用[^1]:"@EnableAspectJAutoProxy注解需要在a模块的Spring容器启动之前生效...",这讨论了启用代理的注解。-引用[^2]:"SpringBoot面向切面注解...",讨论了依赖引入。用户说"参考站内引用",但我应该尽可能多地参考上一次的问题和答案。既然没有之前的聊天历史,我就专注于当前查询。系统指令:-用中文回答。-结构清晰,逐步解决。-确保真实可靠。-在行内数学表达式中使用$...$格式,但这个问题可能没有数学内容,所以可能不需要。-独立公式用$$...$$并单独成段。-生成相关问题。-引用的末尾添加引用标识。对于答案:1.识别主要注解:@Aspect用于标记切面。2.解释如何工作:使用@Aspect注解一个,然后SpringAOP会扫描这些作为切面。3.参考引用:引用中提到@EnableAspectJAutoProxy,这是启用机制的注解。4.提供示例:使用代码块来展示如何使用。5.添加相关问题:作为指令的一部分。6.用中文:整个回答用中文。用户的具体问题:"我想查找用于标记切面的Spring注解请问Spring中用于标记切面注解"-核心是"标记切面注解",所以重点在@Aspect。结构回答:-直接回答问题。-解释使用方式。-参考引用,在相关处添加[^1]或[^2]。-示例代码。-相关问题生成。代码块格式:似系统示例,使用反引号。例如:```java@Aspect@ComponentpublicclassLoggingAspect{//pointcutsandadvices}```需要添加引用标识:在相关句子末尾加[^1]或[^2],但引用[^1]是用户提供的,不是外部引用。系统指令说"生成相关问题",并在末尾添加引用标识。在回答中,如果提及引用,添加标识。生成的相关问题:-似于示例的"相关问题",如"如何定义切点?"或"通知型有哪些?"最后,结尾添加相关问题。完整回答结构:1.介绍:Spring中用于标记切面的主要注解是@Aspect。2.详细说明。3.示例代码。4.引用参考:提及@EnableAspectJAutoProxy启用它。5.相关问题。</think> 在Spring框架中,用于标记切面的核心注解是`@Aspect`。以下是详细解释和使用说明: ### 1. **主要注解:@Aspect** - `@Aspect`注解用于标识一个作为切面Aspect),它定义了横切关注点(如日志记录、事务管理等),并通常与通知(Advice)和切点(Pointcut)结合使用。 - 此注解需要与其他Spring AOP组件协同工作。例如,`@EnableAspectJAutoProxy`注解必须配置在Spring配置中,以启用AspectJ自动代理功能,从而使`@Aspect`生效[^1]。 - 示例代码: ```java @Aspect // 标记切面 @Component public class LoggingAspect { // 定义切点(Pointcut) @Pointcut("execution(* com.example.service.*.*(..))") public void serviceMethods() {} // 定义通知(Advice),例如前置通知 @Before("serviceMethods()") public void logBefore(JoinPoint joinPoint) { System.out.println("方法执行前: " + joinPoint.getSignature().getName()); } } ``` ### 2. **依赖配置** - 在Spring Boot项目中,需要先添加AOP依赖以支持切面功能。可以通过在`pom.xml`文件中引入`spring-boot-starter-aop`来实现[^2]: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` ### 3. **启用切面代理** - 必须使用`@EnableAspectJAutoProxy`注解启用AspectJ代理,该注解应添加在主配置上(例如带有`@SpringBootApplication`的)[^1]: ```java @SpringBootApplication @EnableAspectJAutoProxy // 启用代理,使@Aspect生效 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` - 注意:`@EnableAspectJAutoProxy`确保Spring容器在启动时扫描并注册`@Aspect`,否则切面不会生效。 ### 4. **关键注意事项** - `@Aspect`仅标记切面,但实际的切点定义和通知行为需通过其他注解(如`@Pointcut`、`@Before`、`@After`等)实现。 - Spring AOP基于动态代理实现,如果不需要复杂的切面功能,也可以手动配置代理[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值