解決Spring AOP攔截抽象類(父類)中方法失效問題
背景
最近工作中需要對組內各個系統依賴的第三方接口進行監控報警,對於下遊出現問題的接口能夠及時感知.首先我們寫瞭一個Spring AOP註解,用於收集調用第三方時返回的信息.而我們調用第三方的類抽象出一個父類.並在父類的方法中加入我們的自定義註解用於監控日志並打印日志.
很多子類繼承瞭這個父類並使用父類中的方法.如:
當調用子類的doSomething方法時問題出現瞭,發現Spring AOP沒有攔截doPost()方法.而將註解加在子類方法上時,Spring AOP可以攔截子類的方法,但這不是我們想要的結果.而當我們將父類通過@Autowired方式註入到子類中代替使用繼承的方式調用父類中方法時Spring AOP可以攔截父類中的方法.至此發現問題出現在繼承上面.
原因分析
Spring AOP攔截器的實現原理就是利用動態代理技術實現面向切面編程,Spring 的代理實現有兩種:一是基於 JDK Dynamic Proxy 技術而實現的;二是基於 CGLIB 技術而實現的。如果目標對象實現瞭接口,在默認情況下Spring會采用JDK的動態代理實現AOP,在本例目標對象沒有實現接口,因此使用的CGLIB實現動態代理對SuperClass對象進行代理,然後增強doPost()方法.下面的代碼展示瞭為什麼Spring AOP沒有增強doPost()方法.
圖2等價於圖3,即使用super關鍵字調用doPost()方法,這就表明我們使用的SuperClass的實例調用的doPost()方法,在我們在使用Spring AOP的時候,我們從IOC容器中獲取的Bean對象其實都是代理對象,而不是那些Bean對象本身.因此AOP不能使用代理對象調用這些父類的方法.
解決方案
知道瞭問題原因,解決問題就比較容易瞭,由於我們使用的super關鍵字調用父類的方法行不通,那麼我們就強制使用代理對象調用父類方法.
好瞭,我們運行程序,發現不但沒有攔截方法而且還報錯瞭.
java.lang.IllegalStateException: Cannot find current proxy: Set ‘exposeProxy’ property on Advised to ‘true’ to make it available.
異常信息非常明確,找不到當前的代理,需要在暴露出代理,我們看下AopContext這個類的源碼,看看到底哪裡出錯瞭,看到瞭我們輸出錯誤信息的地方.
package org.springframework.aop.framework; import org.springframework.core.NamedThreadLocal; import org.springframework.lang.Nullable; public final class AopContext { private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal("Current AOP proxy"); private AopContext() { } public static Object currentProxy() throws IllegalStateException { Object proxy = currentProxy.get(); if (proxy == null) { throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available."); } else { return proxy; } } @Nullable static Object setCurrentProxy(@Nullable Object proxy) { Object old = currentProxy.get(); if (proxy != null) { currentProxy.set(proxy); } else { currentProxy.remove(); } return old; } }
說名setCurrentProxy方法沒有被調用,通過查找發現有兩個類調用瞭該方法,分別為CglibAopProxy和JdkDynamicAopProxy,是不是很熟悉,這兩個就是Spring aop的代理方式,由於我們討論的目標對象不是基於接口的,因此本文使用的代理都是基於CglibAopProxy,我們找到該類中調用setCurrentProxy方法的地方,程序中判斷this.advised.exposeProxy是否為true,如果為true,設置當前代理,而通過調試這個字段為false
if (this.advised.exposeProxy) { oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; }
那麼我們就在需要通知的地方,即你需要攔截方法的類上加上如下註解.
@EnableAspectJAutoProxy(exposeProxy = true)
這次在調用,發現已經可以攔截註解標註的方法瞭.
後記
解決這個問題的方式有很多,可以子類不繼承父類,而是改為@Autowired方式,然後每一個調用的地方需要加上父類的對象.
最終我們在程序中的方案是加瞭一個父類的代理類,用於強制使用代理對象調用父類的方法.而原來父類的子類繼承代理類即可.
import org.springframework.aop.framework.AopContext; import org.springframework.stereotype.Component; import java.util.Map; @Component public class ProxyAgent extends BaseAgent{ private BaseAgent getProxyObject() { return ((BaseAgent)AopContext.currentProxy()); } protected String doGet(String url, Map<String, Object> headers, Map<String, Object> params, Object... uriVariables) { return getProxyObject().doGetBase(url, headers, params, uriVariables); } protected String doPost(String url, Map<String, Object> headers, Object body) { return getProxyObject().doPostBase(url, headers, body); } protected String doPostForm(String url, Map<String, Object> params) { return doPostForm(url, null, params); } protected String doPostForm(String url, Map<String, Object> headers, Map<String, Object> params) { return getProxyObject().doPostFormBase(url, headers, params); } protected String doPostFormWithContentHeader(String url, Map<String, Object> headers, Map<String, Object> params, byte[] boundary) { return getProxyObject().doPostFormWithContentHeaderBase(url, headers, params, boundary); } protected String doPostFormUpload(String url, Map<String, Object> headers, Map<String, Object> params) { return getProxyObject().doPostFormUploadBase(url, headers, params); } }
同理,調用內部方法使用this關鍵字時同樣會出現這個問題,同樣采用強制使用代理對象即可.
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 解決Spring AOP 同類調用失效問題
- Spring aop 如何通過獲取代理對象實現事務切換
- 解決spring AOP中自身方法調用無法應用代理的問題
- SpringBoot中創建的AOP不生效的原因及解決
- Spring AOP 對象內部方法間的嵌套調用方式