Spring MVC系列-(5) AOP

Spring.png

5 AOP

5.1 什麼是AOP

AOP(Aspect-Oriented Programming,面向切面編程),可以說是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。

OOP引入封裝、繼承和多態性等概念來建立一種對象層次結構,用以模擬公共行爲的一個集合。當我們需要爲分散的對象引入公共行爲的時候,OOP則顯得無能爲力。也就是說,OOP允許你定義從上到下的關係,但並不適合定義從左到右的關係。例如日誌功能。日誌代碼往往水平地散佈在所有對象層次中,而與它所散佈到的對象的核心功能毫無關係。對於其他類型的代碼,如安全性、異常處理和透明的持續性也是如此。這種散佈在各處的無關的代碼被稱爲橫切(cross-cutting)代碼,在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。

而AOP技術則恰恰相反,它利用一種稱爲“橫切”的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其名爲“Aspect”,即切面。所謂“切面”,簡單地說,就是將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。

實現AOP的技術,主要分爲兩大類:一是採用動態代理技術,利用攔截方法的方式,對該方法進行裝飾,以取代原有對象行爲的執行;二是採用靜態織入的方

5.2 AOP術語

1. 連接點(Join point)

連接點是在應用執行過程中能夠插入切面的一個點。這個點可以是類的某個方法調用前、調用後、方法拋出異常後等。

2. 通知(Advice)

在特定的連接點,AOP框架執行的動作。

Spring AOP 提供了5種類型的通知:

  • 前置通知(Before):在目標方法被調用之前調用通知功能。
  • 後置通知(After):在目標方法完成之後調用通知,無論該方法是否發生異常。
  • 後置返回通知(After-returning):在目標方法成功執行之後調用通知。
  • 後置異常通知(After-throwing):在目標方法拋出異常後調用通知。
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行爲。

3. 切點(Poincut)

具體定位的連接點:上面也說了,每個方法都可以稱之爲連接點,我們具體定位到某一個方法就成爲切點。

切點與連接點:切點和連接點不是一對一的關係,一個切點匹配多個連接點,切點通過 org.springframework.aop.Pointcut 接口進行描述,它使用類和方法作爲連接點的查詢條件。每個類都擁有多個連接點,例如 ArithmethicCalculator類的所有方法實際上都是連接點。

4. 切面(Aspect)

切面由切點和通知組成,它既包括了橫切邏輯的定義、也包括了連接點的定義。

5. 織入(Weaving)

織入描述的是把切面應用到目標對象來創建新的代理對象的過程。 Spring AOP 的切面是在運行時被織入,原理是使用了動態代理技術。Spring支持兩種方式生成代理對象:JDK動態代理和CGLib,默認的策略是如果目標類是接口,則使用JDK動態代理技術,否則使用Cglib來生成代理。

6. 引入(Introduction)

添加方法或字段到被通知的類。 Spring允許引入新的接口到任何被通知的對象。例如,你可以使用一個引入使任何對象實現 IsModified接口,來簡化緩存。Spring中要使用Introduction, 可有通過DelegatingIntroductionInterceptor來實現通知,通過DefaultIntroductionAdvisor來配置Advice和代理類要實現的接口。

5.3 AOP使用

首先新建業務邏輯類,該類實現了基本的除法操作:

public class Calculator {
	//業務邏輯方法
	public int div(int i, int j)  {
		System.out.println("--------");
		return i/j;
	}	
}

現在需要實現:在div()方法運行之前, 記錄一下日誌, 運行後也記錄一下,運行出異常,也打印一下。

因此可以使用AOP來完成日誌的功能,新建日誌切面類:

在定義切面類的時候,需要注意如下幾點:

  • 在類上加上@Aspect聲明爲切面類。
  • 可以使用PointCut將相同的切點進行統一定義,其他地方直接引用即可。
@Aspect
public class LogAspects {
	@Pointcut("execution(public int com.enjoy.cap10.aop.Calculator.*(..))")
	public void pointCut(){};
	
	//@before代表在目標方法執行前切入, 並指定在哪個方法前切入
	@Before("pointCut()")
	public void logStart(JoinPoint joinPoint){
		System.out.println(joinPoint.getSignature().getName()+"除法運行....參數列表是:{"+Arrays.asList(joinPoint.getArgs())+"}");
	}
	@After("pointCut()")
	public void logEnd(JoinPoint joinPoint){
		System.out.println(joinPoint.getSignature().getName()+"除法結束......");
		
	}
	@AfterReturning(value="pointCut()",returning="result")
	public void logReturn(Object result){
		System.out.println("除法正常返回......運行結果是:{"+result+"}");
	}
	@AfterThrowing(value="pointCut()",throwing="exception")
	public void logException(Exception exception){
		System.out.println("運行異常......異常信息是:{"+exception+"}");
	}
	
	/*@Around("pointCut()")
	public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
		System.out.println("@Arount:執行目標方法之前...");
		Object obj = proceedingJoinPoint.proceed();//相當於開始調div地
		System.out.println("@Arount:執行目標方法之後...");
		return obj;
	}*/
}

有了以上操作, 我們還需要將切面類和被切面的類, 都加入到容器中,注意需要加上@EnableAspectJAutoProxy開啓AOP。

/*
 * 日誌切面類的方法需要動態感知到div()方法運行, 
 *  通知方法:
 *     前置通知:logStart(); 在我們執行div()除法之前運行(@Before)
 *     後置通知:logEnd();在我們目標方法div運行結束之後 ,不管有沒有異常(@After)
 *     返回通知:logReturn();在我們的目標方法div正常返回值後運行(@AfterReturning)
 *     異常通知:logException();在我們的目標方法div出現異常後運行(@AfterThrowing)
 *     環繞通知:動態代理, 需要手動執行joinPoint.procced()(其實就是執行我們的目標方法div,), 執行之前div()相當於前置通知, 執行之後就相當於我們後置通知(@Around)
 */
@Configuration
@EnableAspectJAutoProxy
public class Cap10MainConfig {
	@Bean
	public Calculator calculator(){
		return new Calculator();
	}

	@Bean
	public LogAspects logAspects(){
		return new LogAspects();
	}
}

使用JoinPoint可以拿到相關的內容, 比如方法名, 參數

Pictu222re1.png

那麼方法正常返回, 怎麼拿方法的返回值呢?

Pictu4444re1.png

那麼如果是異常呢?定義

Pictur55551.png

下面是測試程序,注意需要使用從IOC容器中取出Bean,否則直接new對象進行操作,AOP、無法生效。

Pict111111ure1.png

從下面的運行結果可以看到,AOP生效,日誌功能正常:

22.png

小結: AOP看起來很麻煩, 只要3步就可以了:
1, 將業務邏輯組件和切面類都加入到容器中, 告訴spring哪個是切面類(@Aspect)
2, 在切面類上的每個通知方法上標註通知註解, 告訴Spring何時運行(寫好切入點表達式,參照官方文檔)
3, 開啓基於註解的AOP模式 @EableXXXX

5.4 Java動態代理

Spring AOP的實現是基於動態代理,在介紹具體實現細節之前,本節先介紹動態代理的原理。

動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制。在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在兩者之間起到中介的作用(可類比房屋中介,房東委託中介銷售房屋、簽訂合同等)。 所謂動態代理,就是實現階段不用關心代理誰,而是在運行階段才指定代理哪個一個對象(不確定性)。如果是自己寫代理類的方式就是靜態代理(確定性)。

很多場景都是利用類似機制做到的,比如用來包裝 RPC 調用、面向切面的編程(AOP)。

(動態)代理模式主要涉及三個要素:

  • 抽象類接口
  • 被代理類(具體實現抽象接口的類)
  • 動態代理類:實際調用被代理類的方法和屬性的類

實現方式: 實現動態代理的方式很多,比如 JDK 自身提供的動態代理,就是主要利用了反射機制。還有其他的實現方式,比如利用字節碼操作機制,類似 ASM、CGLIB(基於 ASM)、Javassist 等。 舉例,常可採用的JDK提供的動態代理接口InvocationHandler來實現動態代理類。其中invoke方法是該接口定義必須實現的,它完成對真實方法的調用。通過InvocationHandler接口,所有方法都由該Handler來進行處理,即所有被代理的方法都由InvocationHandler接管實際的處理任務。此外,我們常可以在invoke方法實現中增加自定義的邏輯實現,實現對被代理類的業務邏輯無侵入。

反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至可以運行時修改類定義。

JDK動態代理

代理模式的形式如下圖所示:

Screen Shot 2020-02-12 at 8.28.31 PM.png

代理模式最大的特點就是代理類和實際業務類實現同一個接口(或繼承同一父類),代理對象持有一個實際對象的引用,外部調用時操作的是代理對象,而在代理對象的內部實現中又會去調用實際對象的操作。Java動態代理其實內部也是通過Java反射機制來實現的,即已知的一個對象,然後在運行時動態調用其方法,這樣在調用前後作一些相應的處理。

下面舉例說明:

1. 靜態代理

若代理類在程序運行前就已經存在,那麼這種代理方式被成爲靜態代理 ,這種情況下的代理類通常都是我們在Java代碼中定義的。 通常情況下, 靜態代理中的代理類和委託類會實現同一接口或是派生自相同的父類。

public interface Sell {
    void sell();
    void ad();
}

Vendor的定義如下:

public class Vendor implements Sell{
    @Override
    public void sell() {
        System.out.println("In sell method");
    }
    @Override
    public void ad() {
        System.out.println("ad method");
    }
}

BusinessAgent的定義如下:

/**
 * 靜態代理,通過聚合來實現,讓代理類有一個委託類的引用即可。
 *
 */
public class BusinessAgent implements Sell{
    private Sell vendor;
    public BusinessAgent(Sell vendor) {
        this.vendor = vendor;
    }
    @Override
    public void sell() {
        // 一些業務邏輯
        System.out.println("before sell");
        vendor.sell();
        System.out.println("after sell");
    }
    @Override
    public void ad() {
        // 一些業務邏輯
        System.out.println("before ad");
        vendor.ad();
        System.out.println("after ad");
    }
}

由上面的代碼可以看到, 通過靜態代理,一方面無需修改Vendor的代碼就可以加入一些業務處理邏輯;另一方面,實現了客戶端與委託類的解耦。但這種靜態代理的侷限在於,必須在運行前編寫好代理類,如果委託類的方法較多,在添加業務邏輯時的工作量較大,需要對每個方法單獨添加。

2. 動態代理

代理類在程序運行時創建的代理方式被成爲 動態代理。 也就是說,這種情況下,代理類並不是在Java代碼中定義的,而是在運行時根據我們在Java代碼中的“指示”動態生成的。相比於靜態代理, 動態代理的優勢在於可以很方便的對代理類的函數進行統一的處理,而不用修改每個代理類的函數。

同樣還是上面的例子,需要在委託類的每個方法前後加入一些處理邏輯,在動態代理的實現中,首先需要定義一個位於代理類與委託類之間的中介類,這個中介類被要求實現InvocationHandler接口,這個接口的定義如下:

/**
 * 調用處理程序
 * 代理類對象作爲proxy參數傳入,參數method標識了我們具體調用的是代理類的哪個方法,args爲這個方法的參數
 */
public interface InvocationHandler { 
    Object invoke(Object proxy, Method method, Object[] args); 
} 

中介類必須實現InvocationHandler接口,作爲調用處理器”攔截“對代理類方法的調用。

public class DynamicProxy implements InvocationHandler {
    // obj爲委託對象
    private Object object;
    public DynamicProxy(Object object) {
        this.object = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before");
        Object result = method.invoke(object, args);
        System.out.println("after");
        return result;
    }
}

在使用時需要動態生成代理類,具體如下:

public class Main {
    public static void main(String[] args) {
        /* Static proxy */
        Vendor vendor = new Vendor();
        BusinessAgent businessAgent = new BusinessAgent(vendor);
        businessAgent.sell();
        businessAgent.ad();
        /* Dynamic proxy */
        DynamicProxy inter = new DynamicProxy(new Vendor());
        //加上這句將會產生一個$Proxy0.class文件,這個文件即爲動態生成的代理類文件
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        // 獲取代理實例sell
        /**
         * public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
         *                                      throws IllegalArgumentException 
         * loader:定義了代理類的ClassLoder; 
         * interfaces:代理類實現的接口列表;
         * h:調用處理器,也就是我們上面定義的實現了InvocationHandler接口的類實例.
         */
        Sell sell = (Sell)(Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[] {Sell.class}, inter));
        // 通過代理類對象調用代理方法,實際上會轉到invoke方法調用
        sell.sell();
        sell.ad();
    }
}

總結: 動態代理的原理就是,首先通過newProxyInstance方法獲取代理類實例,而後我們便可以通過這個代理類實例調用代理類的方法,對代理類的方法的調用實際上都會調用中介類(調用處理器)的invoke方法,在invoke方法中我們調用委託類的相應方法,並且可以添加自己的處理邏輯。

CGLIB動態代理

CGLIB(Code Generation Library)是一個基於ASM的字節碼生成庫,它允許我們在運行時對字節碼進行修改和動態生成。CGLIB通過繼承方式實現代理。

來看示例,假設我們有一個沒有實現任何接口的類HelloConcrete:

public class HelloConcrete {
    public String sayHello(String str) {
        return "HelloConcrete: " + str;
    }
}

因爲沒有實現接口該類無法使用JDK代理,通過CGLIB代理實現如下:

  • 實現一個MethodInterceptor,方法調用會被轉發到該類的intercept()方法。
  • 在需要使用HelloConcrete的時候,通過CGLIB動態代理獲取代理對象。
// CGLIB動態代理
// 1. 首先實現一個MethodInterceptor,方法調用會被轉發到該類的intercept()方法。
class MyMethodInterceptor implements MethodInterceptor{
  ...
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        logger.info("You said: " + Arrays.toString(args));
        return proxy.invokeSuper(obj, args);
    }
}
// 2. 然後在需要使用HelloConcrete的時候,通過CGLIB動態代理獲取代理對象。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());
HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));
// 輸出結果如下
// 日誌信息: You said: [I love you!]
// HelloConcrete: I love you!

通過CGLIB的Enhancer來指定要代理的目標對象、實際處理代理邏輯的對象,最終通過調用create()方法得到代理對象,對這個對象所有非final方法的調用都會轉發給MethodInterceptor.intercept()方法,在intercept()方法裏我們可以加入任何邏輯,比如修改方法參數,加入日誌功能、安全檢查功能等;通過調用MethodProxy.invokeSuper()方法,我們將調用轉發給原始對象,具體到本例,就是HelloConcrete的具體方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很類似,都是方法調用的中轉站。

CGLIB是通過繼承(如上面例子中的enhancer.setSuperclass(HelloConcrete.class))實現代理,由於final類型不能有子類,所以CGLIB不能代理final類型,遇到這種情況會拋出異常。

5.5 AOP原理深入分析

AOP的原理簡單來講,利用動態代理,在IOC容器初始化時,創建Bean的代理類;在代理方法被調用時,代理類會攔截方法的調用,並在之前或者之後插入切面方法,以此實現AOP的目標。

接下來會從以下幾方面深入分析AOP的原理:

  • AnnotationAwareAspectJAutoProxyCreator註冊
  • AnnotationAwareAspectJAutoProxyCreator分析
  • AOP流程分析

AnnotationAwareAspectJAutoProxyCreator註冊

在之前使用AOP時,爲了啓用AOP,需要在配置類中,聲明@EnableAspectJAutoProxy的註解,這個註解的功能就是註冊AnnotationAwareAspectJAutoProxyCreator。下面具體分析這個組件是如何註冊的。

Screen Shot 2020-02-13 at 11.47.39 AM.png@w=250

進入@EnableAspectJAutoProxy的源碼中,可以看到該類引入了AspectJAutoProxyRegistrar

@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    //proxyTargetClass屬性,默認false,採用JDK動態代理織入增強(實現接口的方式);如果設爲true,則採用CGLIB動態代理織入增強
 	boolean proxyTargetClass() default false;
    //通過aop框架暴露該代理對象,aopContext能夠訪問
 	boolean exposeProxy() default false;
}

AspectJAutoProxyRegistrar中, 可以看到實現了ImportBeanDefinitionRegistrar接口,這個接口之前也有介紹,能給容器中自定義註冊組件。

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

	/**
	 * Register, escalate, and configure the AspectJ auto proxy creator based on the value
	 * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing
	 * {@code @Configuration} class.
	 */
	@Override
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy != null) {
			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
			}
		}
	}

}

重點關注AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);,這一步將會註冊AnnotationAwareAspectJAutoProxyCreator。下面進入源碼,

Pict111ure1.png

Pictur222e1.png

程序的邏輯很清晰,

  • 如果(registry.containsBeanDefinition(ATUO_PROXY_CREATOR_BEAN_NAME))也就是容器中bean已經有了 internalAutoProxyCreator, 執行內部內容,返回null。
  • 如果沒有,創建AnnotationAwareAspectJAutoProxyCreator信息; 把此bean註冊在registry中.
    做完後, 相當於給容器中註冊internalAutoProxyCreator組件, 該組件類型爲AnnotationAwareAspectJAutoProxyCreator.class。( 注意這裏ATUO_PROXY_CREATOR_BEAN_NAME值爲internalAutoProxyCreator)

綜上分析,@EnableAspectJAutoProxy的功能就是,利用其中的AspectJAutoProxyRegistrar給我們容器中註冊一個AnnotationAwareAspectJAutoProxyCreator組件,這是後續創建增強Bean的基礎。

AnnotationAwareAspectJAutoProxyCreator分析

AnnotationAwareAspectJAutoProxyCreator的類層次結構如下圖所示,

CDA7BFAFE6E8CF7B8DD9E99993FD64D6.png

繼承關係爲:

  • AnnotationAwareAspectJAutoProxyCreator
  • ->AspectJAwareAdvisorAutoProxyCreator
  • --->AbstractAdvisorAutoProxyCreator
  • ----->AbstractAutoProxyCreator implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware

其中的SmartInstantiationAwareBeanPostProcessor是Bean的後置處理器,同時也實現了BeanFactoryAware可以在容器初始化時,將beanFactory傳進來進行相關操作。

由上述分析可知,AnnotationAwareAspectJAutoProxyCreator既具有BeanPostProcessor特點, 也實現了BeanFactoryAware接口,方便操作BeanFactory。

AOP實現流程

上面已經介紹了AOP過程中,核心的AnnotationAwareAspectJAutoProxyCreator組件,接下來對整個AOP的流程進行梳理,主要分爲如下4個步驟:

  1. 註冊AnnotationAwareAspectJAutoProxyCreator的BeanDefinition

  2. 創建AnnotationAwareAspectJAutoProxyCreator,並加入到BeanFactory

  3. 利用AnnotationAwareAspectJAutoProxyCreator攔截Bean的初始化,創建增強的Bean

  4. 增強Bean的調用過程

IOC容器初始化的入口是如下的refresh()函數,上面1,2,3步驟,分別發生在如下標出的3個函數中,下面分別對這三個函數進行詳細介紹。

Screen Shot 2020-02-15 at 10.56.17 AM.png

1. 註冊AnnotationAwareAspectJAutoProxyCreator的BeanDefinition

這一步主要是通過invokeBeanFactoryPostProcessors(beanFactory)函數,添加AnnotationAwareAspectJAutoProxyCreator的定義,最終調用的函數如下:

Screen Shot 2020-02-15 at 12.46.45 PM.png

註冊的組件類型爲AnnotationAwareAspectJAutoProxyCreator.class,組件名稱ATUO_PROXY_CREATOR_BEAN_NAME值爲internalAutoProxyCreator。

下面是調用棧:

Screen Shot 2020-02-15 at 12.54.46 PM.png

2. 創建AnnotationAwareAspectJAutoProxyCreator,並加入到BeanFactory

這一步入口是registerBeanPostProcessors(beanFactory),進入該函數後,會跳轉到如下的核心函數中進行beanPostProcess的實例化。注意到之前提到過,AnnotationAwareAspectJAutoProxyCreator實現了BeanPostProcess接口,所以可以將其當成一個正常的後置處理器來進行實例化。

Screen Shot 2020-02-15 at 1.10.12 PM.png

從下面的debug信息可以看到,在這一步中,容器需要實例化4個後置處理器,其中最後一個就是我們關注的AnnotationAwareAspectJAutoProxyCreator

Screen Shot 2020-02-15 at 1.12.50 PM.png

整個初始化後置處理器的流程,可以分爲如下幾步:

1)先獲取ioc容器已經定義了的需要創建對象的所有BeanPostProcessor
3)優先註冊實現了PriorityOrdered接口的BeanPostProcessor;
4)再給容器中註冊實現了Ordered接口的BeanPostProcessor;
5)註冊沒實現優先級接口的BeanPostProcessor;

Screen Shot 2020-02-15 at 1.18.01 PM.png

後置處理器AnnotationAwareAspectJAutoProxyCreator實例化完成之後,在接下來的Bean的實例化過程中,它會去嘗試攔截Bean的初始化,如果有需要,則會創建代理增強後的Bean。

3. 利用AnnotationAwareAspectJAutoProxyCreator攔截Bean的初始化,創建增強的Bean

在之前的例子中,定義瞭如下的切面類,實現了相關的advice方法。

Screen Shot 2020-02-15 at 1.22.17 PM.png

這是Calculate類,就是需要增強的類。

Screen Shot 2020-02-15 at 1.31.49 PM.png@w=300

這一步中主要關注這兩個Bean的實例化。

這一步的入口是refresh函數中的beanFactory.preInstantiateSingletons(),下一步進入到getBean-->doGetBean函數,

Screen Shot 2020-02-15 at 3.10.53 PM.png

Screen Shot 2020-02-15 at 3.12.38 PM.png

接着進入doGetBean-->createBean函數,
Screen Shot 2020-02-15 at 3.17.15 PM.png

Screen Shot 2020-02-15 at 3.17.41 PM.png

接着進入到createBean函數,會調用函數Object bean = resolveBeforeInstantiation(beanName, mbdToUse);試圖直接返回proxy對象。

接下來首先分析這個函數,再分析之後正常的初始化流程。createBean函數是理解整個AOP流程的核心。

Screen Shot 2020-02-15 at 3.26.04 PM.png

進入到函數的實現,可以看到最後會去嘗試調用類型爲InstantiationAwareBeanPostProcessor的後置處理器,由於AnnotationAwareAspectJAutoProxyCreator實現了該接口,所以這個時候會被調用來試圖返回proxy對象,但是通常情況下,增強bean不會在這裏生成。

Screen Shot 2020-02-15 at 3.35.20 PM.png

但並不是說這個AnnotationAwareAspectJAutoProxyCreator就沒有作用,進入到該函數的實現,可以發現在shouldSkip函數中會去找到所有的Advisor,也就是之前例子中的LogAspects類,並把這些Advisor放到BeanFactory中,方便後續創建增強的Bean。

Screen Shot 2020-02-15 at 4.25.23 PM.png

在獲取到所有的Advisor之後,判斷當前bean是否在advisedBeans中(保存了所有需要增強bean)
以及判斷當前bean是否是基礎類型的Advice、Pointcut、Advisor、AopInfrastructureBean,如果是的話就跳過。

回到createBean函數,下面進入到正常的Bean初始化流程,一步步跟進到initializeBean函數中,可以看到在初始化Bean的前後都會調用對應的後置處理器來完成相應的功能,但是AbstractAutoProxyCreator的實現中,在初始化Bean之前,只是直接返回Bean;但是在初始化完Bean之後,會調用對應的後置處理器,也就是在applyBeanPostProcessorsAfterInitialization函數中來創建增強的Bean。

Screen Shot 2020-02-15 at 5.00.58 PM.png

下面對該函數進行仔細分析,

Screen Shot 2020-02-15 at 5.35.30 PM.png

接着分析createProxy函數的實現,下面省略了部分中間調用,在最後的實現中,createAopProxy會根據情況使用jdk代理或者CGLib,從代碼中可以看到,當被代理類是接口或者是proxy類時,就會採用jdk動態代理,反之則採用CGLib。

以後容器中獲取到的就是這個組件的代理對象,執行目標方法的時候,代理對象就會執行通知方法的流程;

Screen Shot 2020-02-15 at 6.09.40 PM.png

注意一點:在createAopProxy時,會判斷config.isProxyTargetClass(),這個值默認爲false。但是在兩個地方進行設置,一個是EnableAspectJAutoProxy註解中,另一個地方是在createProxy函數中,evaluateProxyInterfaces會去查找目標類的所有interface,如果可用的話,則將其加到proxyFactory中,否則,調用setProxyTargetClass,設置爲true。在本例子中,calculate類沒有相關接口,所以設置爲true。這也是爲什麼在createAopProxy函數中,會進行判斷,而不是直接返回jdk動態代理的類。

Screen Shot 2020-02-15 at 6.26.00 PM.png

4. 增強Bean的調用過程

上面對AOP流程進行了梳理,通過代碼分析瞭如何代理生成增強的Bean。這部分介紹在調用增強Bean的方法時,proxy對象是如何攔截方法調用的。

當被增強的Bean在執行時,會進入到下面的攔截執行流程,

Screen Shot 2020-02-16 at 5.05.32 PM.png

首先,根據ProxyFactory對象獲取將要執行的目標方法攔截器鏈:List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

Picture1.png

進一步跟進到實現,getInterceptorsAndDynamicInterceptionAdvice的流程大致如下,主要是爲了獲取攔截鏈:

  1. List
  2. 遍歷所有的增強器,將其轉爲List;如果是MethodInterceptor,直接加入到集合中。如果不是,使用AdvisorAdapter將增強器轉爲MethodInterceptor;轉換完成返回MethodInterceptor數組。

在得到攔截鏈之後,如果沒有攔截器鏈,直接執行目標方法;如果有攔截器鏈,把需要執行的目標對象,目標方法,攔截器鏈等信息傳入創建一個 CglibMethodInvocation 對象,並調用 mi.proceed();來獲取執行結果。

注意:攔截器鏈的觸發過程是一個迭代的過程,

  1. 如果沒有攔截器執行執行目標方法,或者攔截器的索引和攔截器數組-1大小一樣(指定到了最後一個攔截器)執行目標方法;
  2. 鏈式獲取每一個攔截器,攔截器執行invoke方法,每一個攔截器等待下一個攔截器執行完成返回以後再來執行;

從下面的調用棧可以看到,所有的攔截器都會等待下一個攔截器調用完成後,再接着執行。

Screen Shot 2020-02-16 at 8.36.58 PM.png

當在執行Before方法時,會先執行完before定義好的方法,然後再去執行正常的方法體:

Screen Shot 2020-02-16 at 8.39.32 PM.png

整個攔截的流程可以總結如下圖所示:

Picture111111.png

下面對整個AOP實現流程進行總結:

  1. @EnableAspectJAutoProxy 開啓AOP功能,會給容器中註冊一個組件 AnnotationAwareAspectJAutoProxyCreator
  2. AnnotationAwareAspectJAutoProxyCreator是一個後置處理器;
  3. 容器的創建流程:
  • registerBeanPostProcessors()註冊後置處理器;創建AnnotationAwareAspectJAutoProxyCreator對象
  • finishBeanFactoryInitialization()初始化剩下的單實例bean

a. 創建業務邏輯組件和切面組件
b. AnnotationAwareAspectJAutoProxyCreator攔截組件的創建過程
c. 組件創建完之後,判斷組件是否需要增強,如果是,則將切面的通知方法,包裝成增強器(Advisor);給業務邏輯組件創建一個代理對象(cglib);

  1. 執行目標方法:
  • 代理對象執行目標方法
  • CglibAopProxy.intercept();

a. 得到目標方法的攔截器鏈(增強器包裝成攔截器MethodInterceptor)
b. 利用攔截器的鏈式機制,依次進入每一個攔截器進行執行;
c. 效果:
正常執行:前置通知-》目標方法-》後置通知-》返回通知
出現異常:前置通知-》目標方法-》後置通知-》異常通知

5.6 Spring AOP VS AspectJ

之前介紹的都是標準的Spring AOP實現,通過在運行時對目標類增強,生成代理類。但是利用AspectJ同樣可以實現增強,只是後者是編譯時增強,而且與Spring框架沒有關係,可以獨立運行。

下面先簡單介紹AspectJ的使用,然後將其與Spring AOP進行對比。

AspectJ的使用

  1. 下載AspectJ並安裝:http://www.eclipse.org/aspectj/downloads.php
  2. 實現HelloWord
業務組件  SayHelloService
package com.ywsc.fenfenzhong.aspectj.learn;
public class SayHelloService {
    public void say(){
        System.out.print("Hello  AspectJ");
    }
} 

需要來了,在需要在調用say()方法之後,需要記錄日誌。那就是通過AspectJ的後置增強吧。

LogAspect 日誌記錄組件,實現對SayHelloService 後置增強
public aspect LogAspect{
    pointcut logPointcut():execution(void SayHelloService.say());
    after():logPointcut(){
         System.out.println("記錄日誌 ..."); 
    }
}
  1. 編譯SayHelloService
執行命令   ajc -d . SayHelloService.java LogAspect.java
生成 SayHelloService.class
執行命令    java SayHelloService
輸出  Hello AspectJ  記錄日誌

ajc.exe 可以理解爲 javac.exe 命令,都用於編譯 Java 程序,區別是 ajc.exe 命令可識別 AspectJ 的語法;我們可以將 ajc.exe 當成一個增強版的 javac.exe 命令.執行ajc命令後的 SayHelloService.class 文件不是由原來的 SayHelloService.java 文件編譯得到的,該 SayHelloService.class 裏新增了打印日誌的內容——這表明 AspectJ 在編譯時“自動”編譯得到了一個新類,這個新類增強了原有的 SayHelloService.java 類的功能,因此 AspectJ 通常被稱爲編譯時增強的 AOP 框架。

Spring AOP和AspectJ對比

1. 從目標角度講:

  • Spring AOP側重於在IOC容器中,提供了一個簡單的AOP實現,它並不是一個完整的AOP解決方案,只適用於被IOC容器管理的Bean。
  • AspectJ是原始的AOP方案,目標是提供一套完整的AOP解決方案。相比Spring AOP,魯棒性更強,可以適用於所有的對象,但是也更加複雜。

2. 織入(Weaving)

AspectJ利用了下面3種不同的織入方法:

  1. Compile-time weaving: The AspectJ compiler takes as input both the source code of our aspect and our application and produces a woven class files as output
  2. Post-compile weaving: This is also known as binary weaving. It is used to weave existing class files and JAR files with our aspects
  3. Load-time weaving: This is exactly like the former binary weaving, with a difference that weaving is postponed until a class loader loads the class files to the JVM

相比於AspectJ,Spring AOP利用了運行時織入(runtime weaving)。

通過動態織入,切面方法被動態的織入到程序的運行過程中,通常有JDK動態代理或者CGLIB代理。

  • Spring AOP傾向於使用JDK動態代理,只要目標對象實現了至少一個接口,Spring將會採用JDK動態代理來創建增強的Bean。

  • 如果目標方法沒有實現接口,就會採用CGLIB來實現。

Screen Shot 2020-02-18 at 3.28.16 PM.png

3. 連接點(Join point)

從設計的角度講,Spring AOP通過代理模式來實現, 例如CGLIB創建目標類的子類(如下圖的實例所示),再調父類的目標方法實現AOP。但是,一旦目標父類使用了關鍵字final,子類無法繼承,切入就不能實現。因此不能代理final類型,同樣的,也不能代理static方法,因爲他們不能被重寫。所以通常情況下,Spring AOP只支持方法作爲連接點。

Screen Shot 2020-02-18 at 4.12.05 PM.png

AspectJ沒有這種限制,在編譯期直接將增強方法織入到代碼中,也不需要像Spring AOP那樣繼承目標方法,因此可以支持更多的連接點。

具體比較如下:

Screen Shot 2020-02-18 at 4.19.53 PM.png

總結:

Spring AOP是基於代理的實現方式,在程序運行時創建代理,並通過攔截鏈來執行切面方法。AspectJ在編譯期將切面方法織入到目標類,在運行期沒有其他性能損耗,因此性能上相比Spring AOP會快很多。

下表是一個整體的對比:

Screen Shot 2020-02-18 at 4.24.51 PM.png


參考:


本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處

搜索『後端精進之路』關注公衆號,立刻獲取最新文章和價值2000元的BATJ精品面試課程

後端精進之路.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章