Spring AOP攔截抽象類(父類)中方法失效問題

背景

最近工做中須要對組內各個系統依賴的第三方接口進行監控報警,對於下游出現問題的接口可以及時感知.首先咱們寫了一個Spring AOP註解,用於收集調用第三方時返回的信息.而咱們調用第三方的類抽象出一個父類.並在父類的方法中加入咱們的自定義註解用於監控日誌並打印日誌.

不少子類繼承了這個父類並使用父類中的方法.如:

當調用子類的doSomething方法時問題出現了,發現Spring AOP沒有攔截doPost()方法.而將註解加在子類方法上時,Spring AOP能夠攔截子類的方法,但這不是咱們想要的結果.而當咱們將父類經過@Autowired方式注入到子類中代替使用繼承的方式調用父類中方法時Spring AOP能夠攔截父類中的方法.至此發現問題出如今繼承上面.java

緣由分析

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不能使用代理對象調用這些父類的方法.spring

解決方案

知道了問題緣由,解決問題就比較容易了,因爲咱們使用的super關鍵字調用父類的方法行不通,那麼咱們就強制使用代理對象調用父類方法.

好了,咱們運行程序,發現不但沒有攔截方法並且還報錯了.編程

java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.
  • 1

異常信息很是明確,找不到當前的代理,須要在暴露出代理,咱們看下AopContext這個類的源碼,看看到底哪裏出錯了,看到了咱們輸出錯誤信息的地方.this

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方法沒有被調用,經過查找發現有兩個類調用了該方法,分別爲CglibAopProxyJdkDynamicAopProxy,是否是很熟悉,這兩個就是Spring aop的代理方式,因爲咱們討論的目標對象不是基於接口的,所以本文使用的代理都是基於CglibAopProxy,咱們找到該類中調用setCurrentProxy方法的地方,程序中判斷this.advised.exposeProxy是否爲true,若是爲true,設置當前代理,而經過調試這個字段爲falseurl

if (this.advised.exposeProxy) {
                    oldProxy = AopContext.setCurrentProxy(proxy);
                    setProxyContext = true;
                }
  • 1
  • 2
  • 3
  • 4

那麼咱們就在須要通知的地方,即你須要攔截方法的類上加上以下註解.spa

@EnableAspectJAutoProxy(exposeProxy = true)
  • 1

此次在調用,發現已經能夠攔截註解標註的方法了.代理

後記

解決這個問題的方式有不少,能夠子類不繼承父類,而是改成@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關鍵字時一樣會出現這個問題,一樣採用強制使用代理對象便可.日誌

相關文章
相關標籤/搜索