嚴(yán)重!Spring AOP Bug導(dǎo)致切面重復(fù)執(zhí)行
環(huán)境:Spring6.1.7
1. 問題復(fù)現(xiàn)
為了提高代碼的復(fù)用性和維護(hù)性,我們設(shè)計(jì)了一個(gè)通用的抽象的切面父類。這個(gè)父類封裝了通用的切面邏輯,如日志記錄、性能監(jiān)控、異常處理等。通過繼承這個(gè)抽象父類,具體的業(yè)務(wù)切面可以輕松地?cái)U(kuò)展或定制這些通用功能,而無需從零開始編寫切面邏輯。如下示例:
public abstract class CommonAspect {
@Pointcut("execution(public * com.pack..*.*(..))")
private void commonPointcut() {}
@Before("bean(*Service)")
protected void beforeLog(JoinPoint jp) {
System.out.println("通用日志記錄...") ;
}
@Around("@annotation(monitor)")
protected Object monitorRun(ProceedingJoinPoint pjp, Monitor monitor) throws Throwable {
Object ret = null ;
// TODO
ret = pjp.proceed() ;
// TODO
return ret ;
}
// other
}
具體切面子類
@Component
@Aspect
public class LogAspect extends CommonAspect {
@Override
public void beforeLog(JoinPoint jp) {
System.out.println("重寫日志記錄功能") ;
}
}
當(dāng)項(xiàng)目運(yùn)行時(shí)出現(xiàn)詭異的問題,既然打印了2次日志信息。
// 測試代碼
AnnotationConfigApplicationContext context = ... ;
PersonService ps = context.getBean(PersonService.class) ;
ps.queryById(1L) ;
執(zhí)行結(jié)果
重寫日志記錄功能
重寫日志記錄功能
查詢Person對象
雖然執(zhí)行了我們重寫的方法,但是日志確輸出了2遍。
通過debug分析
容器在啟動初始化解析@Aspect切面時(shí),在獲取切面類中的所有方法時(shí),會得到兩個(gè)方法(父類及子類重寫的)
圖片
在這里的getAdvisorMethods方法返回了3個(gè)方法,其中2個(gè)是父類中的一個(gè)是子類重寫的方法
圖片
那么接下來根據(jù)這2個(gè)方法就會生成對應(yīng)的Advisor對象。
圖片
這也就是為什么重復(fù)的原因了。Spring并沒有判斷我當(dāng)前的這個(gè)通知是否是重寫父類的方法。
注:在Spring中@Aspect定義的切面最終都會轉(zhuǎn)換為Advisor對象,當(dāng)代理類在執(zhí)行時(shí)會遍歷所有符合添加的Advisor然后從中取出對應(yīng)的Advice(MethodInterceptor)對象。
既然知道了問題出現(xiàn)的原因,接下來就進(jìn)行解決該問題。
2. 解決問題
2.1 解決辦法1
我們可以在子類重寫的方法上再加上通知類型,將切入點(diǎn)設(shè)置的不匹配任何方法即可
@Before("execution(public * xxxooo())")
@Override
public void beforeLog(JoinPoint jp) {
System.out.println("重寫日志記錄功能") ;
}
在這里重寫的方法上,將切入點(diǎn)重新寫,該切入點(diǎn)不會匹配任何的方法,這樣修改以后再次執(zhí)行
重寫日志記錄功能
查詢Person對象
正常執(zhí)行,沒有重復(fù)輸出日志。
2.2 解決辦法2
該方法需要將Spring版本升級到6.1.8,在該版本中解決了該問題。來看看在該版本中是如何解決的。
6.1.8版本
核心方法還是在上面debug是看到的getAdvisors方法中
public class ReflectiveAspectJAdvisorFactory {
public List<Advisor> getAdvisors(...) {
List<Advisor> advisors = new ArrayList<>();
for (Method method : getAdvisorMethods(aspectClass)) {
// 在這里加入了判斷(當(dāng)前方法是否與當(dāng)前切面中的方法相同)
// 當(dāng)遍歷到這里的Method是父類中的方法時(shí),這里的getMostSpecificMethod
// 方法就會判斷根據(jù)方法的名稱再從當(dāng)前切面類中獲取方法(那當(dāng)然就不同了)
if (method.equals(ClassUtils.getMostSpecificMethod(method, aspectClass))) {
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName);
if (advisor != null) {
advisors.add(advisor);
}
}
}
}
}
圖片
在6.1.8版本中通過這種方式就排除了父類的方法。
其它版本
圖片
低于6.1.8版本,都沒有相應(yīng)的判斷。