聊聊Spring的AOP實現原理

本學習筆記將盡可能的將AOP的知識講解的通俗易懂,先從一個典型的問題出發,引入AOP這個概念,介紹AOP的基本概念,再到Spring中的AOP的實現方案,最后進行一個簡單的總結歸納。本學習筆記中不考慮cglib、也不會太關注Spring AOP如何使用,而是盡可能的簡單的說清楚AOP的工作原理。

筆記中貼出的源代碼均是Spring 5.1.7-RELEASE 版本

問題提出

如下代碼塊,現在需要統計這個方法執行的耗時情況

public void runTask() {
    doSomething();
}

一次性的解決肯定非常簡單,直接添加一個時間記錄即可,如下代碼塊

public void runTask() {
    long start = System.currentTimeMillis();
    doSomething();
    System.out.println(System.currentTimeMillis() - start);
}
  • 改寫原方法:就如上述直接添加時間點記錄,針對一兩個簡單的需求這種方案是最快最高效的,但是弊端也是非常明顯的。直接把非業務功能和業務功能耦合在一起、需要改動太大的業務功能、不能靈活修改,如果下一次需要把時間記錄去掉,換成統計次數調用,那么所有的地方都得改動,成本非常大,稍有不慎就容易出錯
  • 適配包裝:即把原對象通過組合的方式包裝到一個代理對象中,類似于適配器模式,如下圖
image

?? 這不是說真的就按照適配器模式去開發,而是采取類似的套路。新弄一個類然后新弄一個對應的方法,在新創建的方法里面再具體調用目標對象的方法。AOP也就是為了解決這類問題所提出的一種解決方案。

AOP 的基本概念

AOP(Aspect Oriented Programming)是基于切面編程的,可無侵入的在原本功能的切面層添加自定義代碼,一般用于日志收集、權限認證等場景。

在了解AOP包含的組件之前,如果是你去設計實現一套解決方案會如何設計呢?

思考幾分鐘得處一些必備點~

需要知道在什么地方進行切面操作
需要知道切面操作的具體內容
如果有多個切面操作,應該得有一個先后執行的順序

事實上AOP也確實是按照這個類似的思路去實現的,先來了解下AOP包含的幾個概念

  • Jointpoint(連接點):具體的切面點點抽象概念,可以是在字段、方法上,Spring中具體表現形式是PointCut(切入點),僅作用在方法上。
  • Advice(通知): 在連接點進行的具體操作,如何進行增強處理的,分為前置、后置、異常、最終、環繞五種情況。
  • 目標對象:被AOP框架進行增強處理的對象,也被稱為被增強的對象。
  • AOP代理:AOP框架創建的對象,簡單的說,代理就是對目標對象的加強。Spring中的AOP代理可以是JDK動態代理,也可以是CGLIB代理。
  • Weaving(織入):將增強處理添加到目標對象中,創建一個被增強的對象的過程

總結為一句話就是:在目標對象(target object)的某些方法(jointpoint)添加不同種類的操作(通知、增強操處理),最后通過某些方法(weaving、織入操作)實現一個新的代理目標對象。

動態代理

在繼續學習之前有必要介紹一下動態代理。動態代理(Dynamic Proxy)是采用Java的反射技術,在運行時按照某一接口要求創建一個包裝了目標對象的新的代理對象,并通過代理對象實現對目標對象的控制操作。

使用動態代理需InvocationHandler + Proxy,可看如下代碼塊

public class HelloInvocationHandle implements InvocationHandler {
    private Object object;
    public HelloInvocationHandle(Object o) {
        this.object = o;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("method: " + method.getName() + " is invoked");
        System.out.println("proxy: " + proxy.getClass().getName());
        Object result = method.invoke(object, args);
        // 反射方法調用
        return result;
    }
}
// HelloWorld 是一個接口,此處沒有貼出來
Class<?> proxyClass = Proxy.getProxyClass(HelloWorld.class.getClassLoader(), HelloWorld.class);
Constructor cc = proxyClass.getConstructor(InvocationHandler.class);
InvocationHandler ihs = new HelloInvocationHandle(new HelloWorldImpl());
HelloWorld helloWorld = (HelloWorld) cc.newInstance(ihs);

套路就是先獲取Proxy生成的class,然后獲取去其中使用了InvocationHandler作為參數的構造器,使用反射newInstance 實現代理對象helloWorld的生成,當然Proxy也提供了更加方便的方法給我們使用

final InvocationHandler in = new HelloInvocationHandle(new HelloWorldImpl());
HelloWorld helloWorld = (HelloWorld) Proxy.newProxyInstance(
    HelloWorld.class.getClassLoader(),    // 被代理對象的類加載器
    HelloWorld.class.getInterfaces(),    // 被代理對象的接口(數組,可保護多個)
    in);   // InvocationHandler實例對象

此外可以使用ProxyGenerator.generateProxyClass方法去獲取到動態生成的代理類實際內容,具體如下

image

繼承了Proxy類,而且其構造函數傳入的確實是一個InvocationHandler實例

image

這個方法最后調用相當于InvocationHandler.invoke,經過代理類的保證調用鏈路就到了HelloInvocationHandle類的invoke方法中,再利用反射調用被代理對象的方法。

接下來就來學習和了解下Spring AOP中最關鍵的兩個類ProxyFactory、ProxyFactoryBean學習AOP的實現原理

ProxyFactory

ProxyFactory或許會比較陌生,可是無論是使用注解的方式還是XML的方式百轉千回后還是會去創建ProxyFactory對象,所以忽略Spring前面一系列的操作,利用ProxyFactory做為入口,直面感受和學習AOP代理對象是如何生成的,demo如下

// 本代碼來自官方單元測試NameMatchMethodPointcutTests類中的代碼
// 在引入spring aop的模塊環境下可直接運行
public void setup() {
    ProxyFactory pf = new ProxyFactory(new SerializablePerson());  // 1
    nop = new SerializableNopInterceptor();   // 2
    pc = new NameMatchMethodPointcut();  // 3
    pf.addAdvisor(new DefaultPointcutAdvisor(pc, nop));  // 4
    proxied = (Person) pf.getProxy(); // 5
    // proxied就是生成的AOP代理對象
}

上面的5個步驟每一個都很關鍵,現在就逐一進行解釋

1、ProxyFactory實例化后傳入的被代理對象,會被存儲到TargetSource對象中,可通過getTarget方法獲取到具體的被代理對象,為什么會存儲到TargetSource對象中后面會說明,再一個就是獲取代理對象可能存在的接口情況也被存儲到interfaces列表中。

2、實例化一個SerializableNopInterceptor對象,這是一個實現了MethodInterceptor接口的類,里面的invoke方法是提供給外界觸發該增強操作的入口,類圖如下:

image

還記得上面介紹AOP的基本概念時說的Advice通知么?其實這就是一個增強器,包含了我們需要增強的功能,日志的收集、權限認證的具體代碼就是寫在這些增強器中的。通過調用invoke實現相關的非業務功能。后面會具體說到是誰觸發了invoke方法調用。

3、實例化了一個NameMatchMethodPointcut對象,一個非常簡單的基于名字匹配的切入點,通俗的說就是通過名字判斷是否需要添加通知

image

其實現了Pointcut接口,并且Pointcut接口包含了ClassFilter getClassFilter();MethodMatcher getMethodMatcher();通過這個名字也能看的出來一個是類過濾器,一個是方法過濾器,兩者共同作用就可以判斷添加增強器的位置。

曾經使用過Spring AOP的小伙伴們是否記得自己的代碼里寫過如下類似的注解代碼

@Pointcut("@annotation(XXXXAnnotation)")   // 匹配的是 方法添加XXXXAnnotation注解
@Pointcut("execution(public void com.XXXXX.controller.*.*(..))") 
// 匹配的是 public類型 返回void并且是com.XXXXX.controller文件夾下面的所有類方法

從類名稱NameMatchMethodPointcut判斷是通過方法名稱匹配的,可是Pointcut接口卻告訴我們是有類匹配和方法匹配兩種,那意味著NameMatchMethodPointcut肯定有默認了類過濾的操作,看下StaticMethodMatcherPointcut類,代碼如下

public abstract class StaticMethodMatcherPointcut extends StaticMethodMatcher implements Pointcut {
    private ClassFilter classFilter = ClassFilter.TRUE;
    // 直接就定義好了類過濾對象ClassFilter.TRUE,也就是下面的TrueClassFilter對象
    public void setClassFilter(ClassFilter classFilter) {
        this.classFilter = classFilter;
    }

final class TrueClassFilter implements ClassFilter, Serializable {
     // 這還是經典的單例寫法
    public static final TrueClassFilter INSTANCE = new TrueClassFilter();
    private TrueClassFilter() {
    }

    @Override
    public boolean matches(Class<?> clazz) {
           // 重點在這,默認全部返還true
        return true;
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

現在知道了NameMatchMethodPointcut是調用了TrueClassFilter單例,所以每一次通過類過濾時,都會返回true,也就是都命中,從而實現了忽略類匹配的操作機制。

到現在2實現了一個通知(增強器)、3實現了一個切入點,那么現在應該需要把2和3組合起來實現切點增強,繼續看4

4、pf.addAdvisor(new DefaultPointcutAdvisor(pc, nop));,實例化了DefaultPointcutAdvisor對象,參數傳入了實例化好的通知和切入點,形成了一個Advisor添加到了ProxyFacotry的advisors列表中

在實際的spring服務中,可能存在多個通知點和切入點,需要通過各種匹配的規則組合成一系列的Advisor對象,然后添加到對應的ProxyFacotry對象中,以便后面的織入

5、實例化代理對象,pf.getProxy()方法寫的是createAopProxy().getProxy();。大致的可以看出來是先創建一個AopProxy對象,然后調用其getProxy()方法返回。先來看看如何創建AopProxy的

// ProxyCreatorSupport 類
private AopProxyFactory aopProxyFactory;
public ProxyCreatorSupport() {
    this.aopProxyFactory = new DefaultAopProxyFactory();  // 1
}

// ProxyFactory 類
public AopProxyFactory getAopProxyFactory() {
    return this.aopProxyFactory;
}
protected final synchronized AopProxy createAopProxy() {
    if (!this.active) {
        activate();
    }
    return getAopProxyFactory().createAopProxy(this);  // 2
}

原來aopProxyFactory默認就是DefaultAopProxyFactory對象,通過其createAopProxy方法返回一個AopProxy對象,并且這傳遞的參數是this,有必要貼一下ProxyFactory的UML圖

image

如圈住的地方是一個AdvisedSupport類,也包含了當前代理類的一些信息。來到DefaultAopProxyFactory類

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                    "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        return new JdkDynamicAopProxy(config);
    }
}

參數傳遞的是AdvisedSupport對象,而ProxyFactory又是繼承AdvisedSupport的,所以上面的this參數是正常的。通過對optimize、proxyTargetClas、是否存在對象接口三個條件判斷選擇是生成JdkDynamicAopProxy還是ObjenesisCglibAopProxy。這里也就是AOP判斷使用動態代理還是CGLIB的地方

在xml配置中添加了proxy-target-class屬性也就是上面說的config.isProxyTargetClass()判斷操作,當設置為true時,就會使用CGLIB

繼續深入,進入到JdkDynamicAopProxy的getProxy()方法中

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
    if (logger.isTraceEnabled()) {
        logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
    }
    Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
    // 明確各種需要實現功能的接口
    findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
    return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

看到這是不是很熟悉,就是我們上面所說的動態代理Proxy.newProxyInstance方法完成代理類的實例化
到這里整個的代理對象就生成了,其實梳理一遍整個流程還是比較清晰的

代理對象調用

動態代理對象生成后調用的入口都是InvocationHandler對象的invoke方法,而且生成代理類的InvocationHandler對象參數傳入就是JdkDynamicAopProxy本身

image

原來JdkDynamicAopProxy也實現了InvocationHandler接口,那么其invoke方法應該包含了具體的調用邏輯

// 精簡了很多代碼,但是并不影響主流程的學習
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    MethodInvocation invocation;
    Object oldProxy = null;
    boolean setProxyContext = false;
    TargetSource targetSource = this.advised.targetSource;
    Object target = null;
    try {
        target = targetSource.getTarget();
        Class<?> targetClass = (target != null ? target.getClass() : null);
        
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
        // 1 獲取增強器執行鏈
        if (chain.isEmpty()) {
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            // 2 無增強器調用鏈,直接通過反射調用target 被代理對象的對應method方法
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        }
        else {
              // 3 生成了新的MethodInvocation對象,開始執行增強器調用執行鏈
            invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            retVal = invocation.proceed();
        }
        return retVal;
    }
}

1、獲取增強器執行鏈,具體實現在DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice方法中

對切點的過濾匹配,也就是上面說的類過濾和方法過濾,調用類過濾matches方法+方法過濾matches方法,返回true添加到返回的容器中。如果是Interceptor對象則直接添加至返回的容器中。最后生成可被調用的增強器執行鏈

2、反射method.invoke 調用操作

3、包裝成了ReflectiveMethodInvocation對象,然后調用其proceed方法

public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
           // 運行到最后了執行被代理對象的方法
        return invokeJoinpoint();
    }

    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    // 從增強器執行鏈獲取一個增強器,索引值currentInterceptorIndex+1
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // 動態參數匹配,匹配后后方可執行
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        }
        else {
            return proceed();
        }
    }
    else {
        // 一般增強器調用invoke
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

上面說的 ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);,調用的是methodinterceptor的invoke方法,這地方也就是ProxyFactory開頭提的增強器調用invoke操作的調用觸發點

來看看@Before注解對應的增強器是如何操作的

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
    return mi.proceed();
}

和我們設想的一樣先執行了增強器的方法,然后循環調用MethodInvocation的proceed的方法,那同理肯定可以猜到@After操作肯定是先執行proceed方法,然后調用相關的增強方法。

到這里整個的AOP過程就算完成了,但是上面還留有一個疑問TargetSource是干什么用的?

上面已經提到targetsource只是包裝了一下具體的被代理類,被包裝成SingletonTargetSource類,每次獲取實際的被代理對象都是通過targetsource.getTarget方法獲取的。那我們就可以自定義targetsource改寫其中的getTarget()方法,從而實現動態控制被代理對象實際對象了。其實熱部署也是采用類似的原理實現的,關于熱部署的更多代碼可以看看官方提供的HotSwappableTargetSourceTests 單元測試代碼。

ProxyFactoryBean

了解完ProxyFactory的整個過程,就很容易理解ProxyFactoryBean了,需知道ProxyFactoryBean = Proxy + FactoryBean,是一種特殊的工廠bean,如下圖是其UML類圖

image

通過FactoryBean很自然的想到起代理類是通過getObject方法完成

public Object getObject() throws BeansException {
    initializeAdvisorChain();
    // 初始化增強器鏈,完成advisor
    if (isSingleton()) {
           // 依舊需要考慮是否為單例bean
        return getSingletonInstance();
    }
    else {
        if (this.targetName == null) {
            logger.info("Using non-singleton proxies with singleton targets is often undesirable. " +
                    "Enable prototype proxies by setting the 'targetName' property.");
        }
        return newPrototypeInstance();
    }
}
    
private synchronized Object getSingletonInstance() {
    if (this.singletonInstance == null) {
        this.targetSource = freshTargetSource();
        if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) {
            Class<?> targetClass = getTargetClass();
            if (targetClass == null) {
                throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy");
            }
            setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
        }
        super.setFrozen(this.freezeProxy);
        this.singletonInstance = getProxy(createAopProxy());
    }
    return this.singletonInstance;
}

protected Object getProxy(AopProxy aopProxy) {
    return aopProxy.getProxy(this.proxyClassLoader);
}

需要關注的是this.singletonInstance = getProxy(createAopProxy());aopProxy.getProxy(this.proxyClassLoader);方法,對比發現其和FactoryBean所生成代理對象的方式是一模一樣的,先生成AopProxy,再調用AopProxy.getProxy方法。只是其中尋找增強器和切點的邏輯存在差異。

總結

寫關于Spring的學習筆記第一次盡可能的跳出Spring框架的思路,Spring AOP提供了純XML、XML+注解、甚至于SpringBoot純注解等多種方案,如果糾結于前期XML的解析、注解的尋找(不是說這些不重要,只是在AOP的學習上不屬于重點),那將會使得整個AOP的學習體驗降到最低。從官方提供的單元測試出發執行單元測試能更加精準。有些功能點雖然被忽略了,但并不影響整體的學習和了解。

從問題的提出,明確了做什么在什么上面做這兩個點,進而引出AOP的概念,認識到通知、連接點等概念,進一步到Spring AOP的切點和增強,組合成為增強器。采取動態代理或者CGLIB的方案實現Weaving織入的過程,進而完成了代理對象的生成。

再提一句:官方的源碼包中提供了豐富的單元測試,可以借助單元測試加深對代碼的理解。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容