嚴重!Spring AOP Bug導致切面重復執行
環境:Spring6.1.7
1. 問題復現
為了提高代碼的復用性和維護性,我們設計了一個通用的抽象的切面父類。這個父類封裝了通用的切面邏輯,如日志記錄、性能監控、異常處理等。通過繼承這個抽象父類,具體的業務切面可以輕松地擴展或定制這些通用功能,而無需從零開始編寫切面邏輯。如下示例:
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("重寫日志記錄功能") ;
}
}
當項目運行時出現詭異的問題,既然打印了2次日志信息。
// 測試代碼
AnnotationConfigApplicationContext context = ... ;
PersonService ps = context.getBean(PersonService.class) ;
ps.queryById(1L) ;
執行結果
重寫日志記錄功能
重寫日志記錄功能
查詢Person對象
雖然執行了我們重寫的方法,但是日志確輸出了2遍。
通過debug分析
容器在啟動初始化解析@Aspect切面時,在獲取切面類中的所有方法時,會得到兩個方法(父類及子類重寫的)
圖片
在這里的getAdvisorMethods方法返回了3個方法,其中2個是父類中的一個是子類重寫的方法
圖片
那么接下來根據這2個方法就會生成對應的Advisor對象。
圖片
這也就是為什么重復的原因了。Spring并沒有判斷我當前的這個通知是否是重寫父類的方法。
注:在Spring中@Aspect定義的切面最終都會轉換為Advisor對象,當代理類在執行時會遍歷所有符合添加的Advisor然后從中取出對應的Advice(MethodInterceptor)對象。
既然知道了問題出現的原因,接下來就進行解決該問題。
2. 解決問題
2.1 解決辦法1
我們可以在子類重寫的方法上再加上通知類型,將切入點設置的不匹配任何方法即可
@Before("execution(public * xxxooo())")
@Override
public void beforeLog(JoinPoint jp) {
System.out.println("重寫日志記錄功能") ;
}
在這里重寫的方法上,將切入點重新寫,該切入點不會匹配任何的方法,這樣修改以后再次執行
重寫日志記錄功能
查詢Person對象
正常執行,沒有重復輸出日志。
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)) {
// 在這里加入了判斷(當前方法是否與當前切面中的方法相同)
// 當遍歷到這里的Method是父類中的方法時,這里的getMostSpecificMethod
// 方法就會判斷根據方法的名稱再從當前切面類中獲取方法(那當然就不同了)
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版本,都沒有相應的判斷。