分析java 中AspectJ切面執(zhí)行兩次的原因
分析java 中AspectJ切面執(zhí)行兩次的原因
背景
轉(zhuǎn)眼之間,發(fā)現(xiàn)博客已經(jīng)將近半年沒(méi)更新了,甚是慚愧。話不多說(shuō),正如標(biāo)題所言,最近在使用AspectJ的時(shí)候,發(fā)現(xiàn)攔截器(AOP切面)執(zhí)行了兩次了。我們知道,AspectJ是AOP的一種解決方案,本質(zhì)上是通過(guò)代理類在目標(biāo)方法執(zhí)行通知(Advice),然后由代理類再去調(diào)用目標(biāo)方法。所以,從這點(diǎn)講,攔截器應(yīng)該只會(huì)執(zhí)行一次。但是在測(cè)試的時(shí)候發(fā)現(xiàn)攔截器執(zhí)行了兩次。
問(wèn)題重現(xiàn)
既然問(wèn)題已經(jīng)明了,那么可以通過(guò)代碼簡(jiǎn)單重現(xiàn)這個(gè)問(wèn)題,從而更深層次分析到底是什么原因?qū)е碌摹?/p>
定義一個(gè)注解:
package com.rhwayfun.aspect;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface StatsService {
}
為該注解定義切面:
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;
@Aspect
public 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.");
}
}
}
方法測(cè)試:
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());
}
}
輸出結(jié)果:

debug分析
由于是靜態(tài)織入,所以可以通過(guò)反編譯工具查看編譯后的文件,如下:
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);
}
}
請(qǐng)注意兩個(gè)連接點(diǎn):ajc$tjp_0和ajc$tjp_1,這兩個(gè)連接點(diǎn)是產(chǎn)生兩次調(diào)用的關(guān)鍵,問(wèn)題注解明明是加上print()方法上的,為什么main()方法也被注入了通知呢?正因?yàn)閙ain()方法也織入了通知,所以就形成了A call B, B call print()的調(diào)用鏈,有兩次method-call,一次method-execution,method-execution才是我們的目標(biāo)方法print(),所以我們才看到了兩次輸出。
method-call和method-execution都是連接點(diǎn)ProceedingJoinPoint的kind屬性
其實(shí),這屬于Ajc編譯器的一個(gè)Bug,詳見Ajc-bug
所以,到這一步,問(wèn)題就很清晰了,因?yàn)锳jc編輯器的bug,導(dǎo)致了在main方法中也織入了通知,所以在執(zhí)行的時(shí)候,輸出了兩次日志。
解決方法
方案一
因?yàn)閮纱握{(diào)用的kind屬性不一樣,所以可以通過(guò)kind屬性來(lái)判斷時(shí)候調(diào)用切面。這樣顯得不優(yōu)雅,而且如果切面有更多的邏輯的話,需要加各種if-else的判斷,所以不推薦。
方法二
更優(yōu)雅的方案是修改@Around("@annotation(StatsService)")的邏輯,改為@Around("execution(* *(..)) && @annotation(StatsService)")。
重新運(yùn)行上面的測(cè)試類,結(jié)果如下:

如有疑問(wèn)請(qǐng)留言或者到本站社區(qū)交流討論,感謝閱讀,希望能幫助到大家,謝謝大家對(duì)本站的支持!
相關(guān)文章
springboot-jta-atomikos多數(shù)據(jù)源事務(wù)管理實(shí)現(xiàn)
本文主要介紹了springboot-jta-atomikos多數(shù)據(jù)源事務(wù)管理實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Java使用正則表達(dá)式驗(yàn)證用戶名和密碼的方法
這篇文章主要介紹了Java使用正則表達(dá)式驗(yàn)證用戶名和密碼的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09
SpringBoot項(xiàng)目如何打包部署到服務(wù)器
這篇文章主要介紹了SpringBoot項(xiàng)目如何打包部署到服務(wù)器問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
Springboot初始化啟動(dòng)報(bào)錯(cuò)Error?creating?bean?with?name?'da
這篇文章主要為大家介紹了Springboot初始化啟動(dòng)報(bào)Error?creating?bean?with?name?'dataSource'?defined?in?class?path?resource解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
Spring?Boot項(xiàng)目傳參校驗(yàn)的最佳實(shí)踐指南
有參數(shù)傳遞的地方都少不了參數(shù)校驗(yàn),在web開發(fā)中前端的參數(shù)校驗(yàn)是為了用戶體驗(yàn),后端的參數(shù)校驗(yàn)是為了安全,下面這篇文章主要給大家介紹了關(guān)于Spring?Boot項(xiàng)目傳參校驗(yàn)的最佳實(shí)踐,需要的朋友可以參考下2022-04-04
SpringBoot中優(yōu)化if-else語(yǔ)句的七種方法
if-else語(yǔ)句是控制流程的基本工具,但過(guò)度使用會(huì)使代碼變得復(fù)雜且難以維護(hù),在SpringBoot , SpringCloud項(xiàng)目中,優(yōu)化if-else結(jié)構(gòu)變得尤為重要,本文將深入探討七種策略,旨在減少SpringBoot , SpringCloud項(xiàng)目中 if-else的使用,需要的朋友可以參考下2024-07-07

