1. 程式人生 > >曹工說Spring Boot原始碼(16)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【上】)

曹工說Spring Boot原始碼(16)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【上】)

寫在前面的話

相關背景及資源:

曹工說Spring Boot原始碼(1)-- Bean Definition到底是什麼,附spring思維導圖分享

曹工說Spring Boot原始碼(2)-- Bean Definition到底是什麼,咱們對著介面,逐個方法講解

曹工說Spring Boot原始碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下

曹工說Spring Boot原始碼(4)-- 我是怎麼自定義ApplicationContext,從json檔案讀取bean definition的?

曹工說Spring Boot原始碼(5)-- 怎麼從properties檔案讀取bean

曹工說Spring Boot原始碼(6)-- Spring怎麼從xml檔案裡解析bean的

曹工說Spring Boot原始碼(7)-- Spring解析xml檔案,到底從中得到了什麼(上)

曹工說Spring Boot原始碼(8)-- Spring解析xml檔案,到底從中得到了什麼(util名稱空間)

曹工說Spring Boot原始碼(9)-- Spring解析xml檔案,到底從中得到了什麼(context名稱空間上)

曹工說Spring Boot原始碼(10)-- Spring解析xml檔案,到底從中得到了什麼(context:annotation-config 解析)

曹工說Spring Boot原始碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)

曹工說Spring Boot原始碼(12)-- Spring解析xml檔案,到底從中得到了什麼(context:component-scan完整解析)

曹工說Spring Boot原始碼(13)-- AspectJ的執行時織入(Load-Time-Weaving),基本內容是講清楚了(附原始碼)

曹工說Spring Boot原始碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation整合

曹工說Spring Boot原始碼(15)-- Spring從xml檔案裡到底得到了什麼(context:load-time-weaver 完整解析)

工程程式碼地址 思維導圖地址

工程結構圖:

概要

本篇是spring原始碼的第16篇,前面已經把context名稱空間下,常用的幾個元素講解差不多了,包括:

context:property-placeholder
context:property-override
context:annotation-config
context:component-scan
context:load-time-weaver

接下來,著重講解aop名稱空間。該名稱空間下,有以下幾個元素:

<aop:config></aop:config>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<aop:scoped-proxy></aop:scoped-proxy>

本講講解aop:config,在沒有註解的時代,大家的aop就是這麼配的,如下所示:

<!--目標物件-->
    <bean id="performer" class="foo.Performer"/>

    <!--切面-->
    <bean id="performAspect" class="foo.PerformAspect"/>

    <!--配置切入點-->
    <aop:config>
        <aop:pointcut id="mypointcut" expression="execution(public * foo.Perform.sing(..))"/>

        <aop:aspect ref="performAspect">
            <aop:after method="afterPerform" pointcut-ref="mypointcut"/>
        </aop:aspect>
    </aop:config>

大家可能覺得xml落伍了,沒錯,我也這麼覺得,但我深入瞭解後發現,通過註解配置切面,和通過xml配置切面,最終其實殊途同歸,最終都轉變為了內部結構List<org.springframework.aop.Advisor>,無非是讀取配置的方式不同。

public interface Advisor {
   
   Advice getAdvice();
}

Advice這個東西,通俗來說是"通知",我截取了spring官網說明:

  • Join point: A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
  • Advice: Action taken by an aspect at a particular join point. Different types of advice include “around”, “before” and “after” advice. (Advice types are discussed later.) Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.

大概翻譯就是:

連線點:程式執行過程中的一個執行點,比如方法呼叫,或者異常處理。在spring aop裡,永遠指方法呼叫

通知:切面在一個特定的連線點所採取的動作。advice包含了多種型別,包括"around"、“before”、“after”。很多aop框架,包括spring,將advice建模為一個攔截器,在切點處維護一個攔截器鏈。

Advice,在文件裡,都是說,表示的是在連線點所採取的動作,比如效能檢測、記錄日誌、事務等。

但是,我要說明的是,在原始碼裡,是不太一樣的。針對前面提到的各種型別的advice,"around"、“before”、“after”等,其在spring裡,是使用以下幾個類來表示的。

我們隨便找個org.springframework.aop.aspectj.AspectJAfterAdvice來看看,這個是代表after型別的advice:

public AspectJAfterAdvice(
      Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {

   super(aspectJBeforeAdviceMethod, pointcut, aif);
}

以上是該類,唯一的建構函式,其中將3個引數,直接傳給了父類的建構函式。

    protected final Method aspectJAdviceMethod;

    private final AspectJExpressionPointcut pointcut;

    private final AspectInstanceFactory aspectInstanceFactory;

public AbstractAspectJAdvice(
      Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aspectInstanceFactory) {
   this.aspectJAdviceMethod = aspectJAdviceMethod;
   this.pointcut = pointcut;
   this.aspectInstanceFactory = aspectInstanceFactory;
}

這裡面,三個欄位,其中,aspectJAdviceMethod就是我們的通知方法,其型別是JDK裡的Method,即我們自定義的那些效能、日誌等aop業務邏輯所在之處;pointcut,代表了切點,我們預設寫的表示式是使用AspectJ的語法寫的,想想,是不是不會寫的時候,有時候查著查著,就查到aspectJ的官網去了;aspectInstanceFactory,裡面封裝了切面物件。

這幾個屬性,有啥關係?

切面 = 切點 + 通知,即,在什麼時間,幹什麼事。

那用這幾個屬性,怎麼表達呢? 簡單來說,是不是,在每個方法執行時,匹配是否和pointcut匹配,如果匹配,則執行:

return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);

沒錯,上面那個程式碼其實就是spring裡來的,spring在"幹什麼事"這部分,就是這麼做的:

#org.springframework.aop.aspectj.AbstractAspectJAdvice
protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
        Object[] actualArgs = args;
        if (this.aspectJAdviceMethod.getParameterTypes().length == 0) {
            actualArgs = null;
        }
        try {
            ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
            return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
        }
    }

說了半天,主要就是說,spring aop原始碼裡的advice,不只是文件裡提到的advice,而是包含了完整的切點和切面的邏輯,這裡的advice,其實也是狹義的,即spring aop裡的方法級別的advice。

囉嗦了半天,我們馬上進入正題。

使用

原始碼見:spring-aop-xml-demo

目標類和介面如下:

package foo;


public class Performer implements Perform {
    @Override
    public void sing() {
        System.out.println("男孩在唱歌");

    }
}

package foo;

public interface Perform {
    void sing();
}

切面如下:

package foo;

public class PerformAspect {

    public void afterPerform() {
        System.out.println("表演之後要行禮");
    }
}

xml配置檔案如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!--目標物件-->
    <bean id="performer" class="foo.Performer"/>

    <!--切面-->
    <bean id="performAspect" class="foo.PerformAspect"/>

    <!--配置切入點-->
    <aop:config>
        <aop:pointcut id="mypointcut" expression="execution(public * foo.Perform.sing(..))"/>

        <aop:aspect ref="performAspect">
            <aop:after method="afterPerform" pointcut-ref="mypointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

測試程式碼如下:

package foo;

public final class Main {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                "context-namespace-test-aop.xml");
        // json輸出bean definition
        List<BeanDefinition> list =
                ctx.getBeanFactory().getBeanDefinitionList();
        MyFastJson.printJsonStringForBeanDefinitionList(list);
        
        Perform performer = (Perform) ctx.getBean(Perform.class);
        performer.sing();
    }
}

執行結果如下:

男孩在唱歌
表演之後要行禮

簡略原始碼說明

上面那個例子,很簡單就實現了aop,但是spring為此做了很多工作,總結起來,有以下幾步:

步驟1:解析xml檔案,獲取bean definition

bean definition 中bean class 備註
PerformAspect 通知
Performer 要切的目標
AspectJExpressionPointcut 切點,即那一行
org.springframework.aop.aspectj.AspectJPointcutAdvisor advisor,請翻到文章開頭,實際表達一個:切點+切面方法;
AspectJAwareAdvisorAutoProxyCreator 實現了BeanPostProcessor介面,在spring getBean過程中,檢查是否匹配切點,匹配則建立代理,並使用代理物件替換ioc容器中真實的bean

步驟2:AspectJAwareAdvisorAutoProxyCreator 狸貓換太子

這個bean definition,本來也很普通,但讓它變得不普通的是,這個bean class,實現了BeanPostProcessor介面。BeanPostProcessor介面的功能,看下面就明白:

public interface BeanPostProcessor {

   Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
   
   Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

這兩個方法,在spring建立bean時,被呼叫,一個是在初始化之前,一個是初始化之後。

關於生命週期,大家可以看上圖,一定要搞明白,什麼叫例項化,什麼叫初始化,什麼叫屬性注入,我們這裡,

AspectJAwareAdvisorAutoProxyCreator 生效的地方,主要是在 初始化之後。它實現了postProcessAfterInitialization方法,這個方法,其return的結果,就會取代原有的bean,來存放到ioc容器中。

後續,如果有其他bean,依賴這個bean的話,拿到的也是代理之後的了。

大家可以看看其實現:

AspectJAwareAdvisorAutoProxyCreator.java
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.contains(cacheKey)) {
             // 這裡根據原來的bean,建立動態代理,並返回給ioc容器,完成狸貓換太子操作
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

而我們建立的代理物件,其實是會包含要應用的advisor的,大家看下圖,其中specificInterceptors的第二個元素,就是前面我們步驟1解析的那個bean definition。

當然了,大家熟知的,有介面時建立jdk代理,沒介面時建立cglib代理,就是在這個步驟發生的,下一篇會細講。

步驟3:花非花,霧非霧

執行時,看似呼叫target,實際呼叫代理的對應方法:

可以看到,這裡拿到的,已經是代理物件,而不是真實物件了,呼叫代理物件時,就會像tomcat的filter鏈那樣,tomcat是filter鏈進行鏈式處理,直到最後呼叫servlet;這裡是interceptor鏈先挨個呼叫自己在target方法之前要執行的邏輯,然後呼叫target,最後呼叫要在target之後執行的邏輯。

總結

今天這篇算是aop的開胃菜,前面只說了大概的步驟,並沒有講透,詳細的原始碼分析實在不適合揉到一篇來講,所以會分到下一講或兩講。

希望對大家有所幫助,謝謝