Spring AOP 原理剖析,這一篇就夠

Spring AOP 學習記錄

AOP(面向切面編程)方面的知識又是看了忘忘了看,今天有空記錄下AOP的知識點。主要分爲以下幾方面:

​ 1、AOP相關術語

​ 2、基礎知識及示例

​ 3、增強分類

1、AOP相關術語

連接點(Joinpoint) 一個類擁有一些邊界性質的特定點,如一個類的各個方法就可稱爲連接點。
切點(Pointcut) 切點就是在衆多連接點中選擇自己感興趣的連接點,如果將連接點比作一個數據庫中的所有記錄,那麼切點就是一個查詢語句的查詢條件。
增強(Advice) 增強是置入目標連接點的一段代碼,增強本身攜帶方位信息,如方法調用前、調用後、調用前後、異常拋出使等。
切面(Aspect) 切面由切點和增強組成,既包含增強邏輯和方位信息,也包含連接點信息。
目標對象(Target) 增強邏輯織入的目標類。
引介(Introduction) 引介是一種特殊的增強,它爲類提供一些屬性和方法。這樣,即是一個業務類原本沒實現某個接口,通過AOP的引介功能也可以動態的爲該類添加接口實現邏輯,讓該類成爲接口的實現類。
織入(Weaving) 織入就是講增強添加到目標類的具體方法上的過程。AOP有三種織入方式①編譯期織入、②類裝載期織入、③動態代理織入(Spring一般採用的方式)
代理(Proxy) 一個類被AOP織入增強後,就產生了一個代理結果類。它融合了增強邏輯,根據代理方式不同,代理類既可能是和原類具有相同接口的類或者是原類的子類。

通過以上概念我們可以想到通過定義切點、給切點織入增強邏輯等步驟,最後就可以完成切面編程。

2、基礎知識及示例
2.1 JDK動態代理

​ Java JDK提供的動態代理技術允許開發者在運行期創建接口的代理類,JDK動態代理主要涉及java.lang.reflect包中的兩個類:Proxy和InvocationHandler。

​ 其中,InvocationHandler是一個接口,可以通過實現該接口實現增強邏輯,並通過反射機制調用目標類代碼,動態的將增強邏輯和業務邏輯編制在一起。Proxy利用InvocationHandler動態創建一個符合某一接口的示例,生成目標類的代理類。下面我使用代碼演示。

// 下面假設我們有以下代碼
public interface GreetService {
	void serviceTo(String name);
}

public class Waiter implements GreetService{

	@Override
	public void serviceTo(String name) {
         // 假設下面爲代碼時間性能監測代碼
		// Long beginTime = System.currentTimeMillis();
		try {
			// 模擬程序在工作
			System.out.println("service to :"+name);
			Thread.sleep(10000L);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// System.out.println("consume time:"+(System.currentTimeMillis()-beginTime));
	}
}


// 我們將上面代碼的性能監控代碼使用JDK動態代理織入
// 首先編寫InvocationHandler實現類
public class WaiterHandler implements InvocationHandler{
	// 目標類
	private Object target;
	public WaiterHandler(Object target) {
		this.target = target;
	}
	// 代理調用方法
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Long beginTime = System.currentTimeMillis();
		// 調用target的相應方法,並傳入參數
		method.invoke(target,args);
		
		System.out.println("consume time : "+(System.currentTimeMillis()-beginTime));
		return null;
	}
}
public class MainTest {

	public static void main(String[] args) {
		// 創建GreetService實例,也就是希望被代理的業務類
		GreetService waiter = new Waiter();
		// 創建InvocationHandler實現類,爲業務類織入增強邏輯
		WaiterHandler waiterHandler = new WaiterHandler(waiter);
		// 使用Proxy生成GreetService代理實例
		GreetService waiterProxy = (GreetService)Proxy.newProxyInstance(waiter.getClass().getClassLoader(), Waiter.class.getInterfaces(),waiterHandler);
		// 調用代理實例方法
		waiterProxy.serviceTo("fuhang");
	}
}
// output:
//  service to :fuhang
//  consume time : 10002
 

​ 上面爲大家演示了一個使用JDK動態代理將性能監控代碼從業務邏輯抽出然後動態織入的示例代碼。

​ 從上面的示例代碼中大家可能發現了幾個問題:

​ 1、Waiter必須實現一個接口

​ 2、newProxyInstance(waiter.getClass().getClassLoader(), Waiter.class.getInterfaces(),waiterHandler);第二個參數必須傳入Waiter實現的接口列表

由以上可以知道JDK創建代理有一個限制,它只能爲接口創建代理實例,難道我們寫一個業務類必須爲其定義一個接口嗎?對於沒有通過接口定義業務方法的類,如何實現爲其動態創建代理實例呢?下面我們看看下一個動態代理技術CGLib。

2.2CGLib動態代理

​ CGLib底層使用了動態字節碼技術,可以爲一個類創建子類,在子類中採用方法攔截技術攔截所有父類方法調用,並順勢織入增強邏輯。下面我們使用CGLib實現上面性能監控織入的邏輯。

// 首先我們創建類CglibProxy實現MethodInterceptor
public class CglibProxy implements MethodInterceptor{

	@Override
	public Object intercept(Object obj, Method arg1, Object[] arg2, MethodProxy methodProxy) throws Throwable {
		Long beginTime = System.currentTimeMillis();
		// 實際方法調用的地方
		methodProxy.invokeSuper(obj, arg2);
		
		System.out.println("consume time : "+(System.currentTimeMillis()-beginTime));
		return null;
	}
}

// 測試類
public class MainTest {

	public static void main(String[] args) {
		// 創建EnHancer對象
		Enhancer enhancer = new Enhancer();
        // 創建我們的CglibProxy代理對象
		CglibProxy cglibProxy = new CglibProxy();
		// 設置父類爲Waiter
		enhancer.setSuperclass(Waiter.class);
        // 設置回調函數爲CglibProxy
		enhancer.setCallback(cglibProxy);
		// 使用enhancer動態字節碼技術創建Waiter的代理類
		Waiter waiter = (Waiter) enhancer.create();
		waiter.serviceTo("fuhang");
	}
}
// output:
//    service to :fuhang
//    consume time : 10044

​ 上面就是通過Cglib動態字節碼技術爲業務類生成子類的方式生成代理類,由此牽扯到Java知識點回顧,final和private修飾符修飾的類不能使用Cglib動態代理技術。

小結

​ SpringAOP技術底層就是使用動態代理技術爲目標bean織入增強邏輯的,但是從以上兩類型代理技術的示例代碼中可以發現以下幾個問題:

​ (1)、目標類被代理後,所有方法都加入了增強邏輯,有些時候這並不是我們想要的。

​ (2)、 通過手動編碼方式指定了增強邏輯

​ (3)、 手工編寫代理實例創建過程,爲不同類創建代理時候需要手工編寫相應的代理實例創建過程代碼。

​ 所以Spring AOP的主要工作就是解決以上三個問題:Spring AOP 通過切點(Pointcut)指定在哪些類的哪些方法上織入增強邏輯,通過增強(Advice)描述具體的織入點(方法調用前、調用後、調用前後),Spring通過Advisor(切面)將切面和增強組織起來,下面Spring就可以利用動態代理技術採用統一的方式爲目標bean織入增強了。

3、增強類型

​ Spring使用增強(Advice)定義增強邏輯,增強不僅包括增強邏輯,還包括方位信息(即方法調用前、調用後、調用前後)

​ Spring支持五種增強類型,解釋如下:

在這裏插入圖片描述

  • 前置增強org.springframework.aop.BeforeAdvice 代表前置增強,Spring只支持方法級的增強,所以目前MethodBeforeAdvice是Spring中可用的前置增強,BeforeAdvice接口是爲了將來擴展定義的。
  • 後置增強 :org.springframework.aop.AfterReturningAdvice,表示在目標方法執行後進行增強。
  • 環繞增強 :org.aopalliance.intercept.MethodInterceptor代表環繞增強,表示在目標方法執行前後實施增強。
  • 異常拋出增強 : org.springframework.aop.ThrowsAdvice代表拋出異常增強,表示在目標方法拋出異常後實施增強。
  • 引介增強 : org.springframework.aop.IntroductionInterceptor代表引介增強,表示在目標類中添加一些新的方法和屬性。
3.1前置增強

​ 我們還用我們之前的GreetService和Waiter做示例代碼,之前我們的代碼中Waiter.serviceTo(name)方法只是輸出"service to xxx",現在我們想實現Waiter.serviceTo(name)在輸出之前想客戶打招呼,那麼按照前面的理解,我們需要爲其定義一個前置增強,下面我們着手改造吧。

// 我們定義一個打招呼前置類,實現MethodBeforeAdvice
public class GreetBeforeAdvice implements MethodBeforeAdvice{
	@Override
	public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
         // 輸出招呼用語
		System.out.print("hello "+ arg1[0]);
	}
}
public class MainTest {
	public static void main(String[] args) {
		// 創建Waiter和前置增強類
		Waiter waiter = new Waiter();
		GreetBeforeAdvice greetBeforeAdvice = new GreetBeforeAdvice();
		// 創建代理工廠,隨後解釋這個工廠
		ProxyFactory proxyFactory = new ProxyFactory();
		// 設置被代理的目標和要織入的增強
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(greetBeforeAdvice);
		// 通過代理工廠創建代理類
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		// 調用方法
		myWaiter.serviceTo("fuhang");	
	}
}
// output:
// 		hello fuhang service to :fuhang

​ 上面就是前置增強的示例,下面講解下ProxyFactory的細節,先來回一下前面的基礎知識章節的內容,說了兩個動態代理技術,ProxyFactory實際就是用到了JDK或者Cglib動態代理技術。

​ Spring定義了org.springframework.aop.framework.AopProxy接口,最終提供了兩個final實現類,如下圖所示。

在這裏插入圖片描述

​ 上圖中Cglib2Proxy使用的是Cglib動態代理技術,JdkDynamicAopProxy使用的是JDK動態代理技術,如果通過ProxyFactory的setInterfaces(Class [] interfaces)方法指定目標類的接口信息,那麼ProxyFactory底層將使用Jdk動態代理技術,否則不設置就使用Cglib動態代理技術。由此可知我們的示例代碼中用到了Cglib動態代理技術。

3.2後置增強

​ 比如每次服務過後服務員需要詢問顧客"還有什麼需要嗎?"這句話,那麼可以通過一個後置增強來實施這一個要求。示例如下。

// 聲明一個後置增強類並實現AfterReturningAdvice
public class GreetAfterAdvice implements AfterReturningAdvice{

	@Override
	public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable {
		System.out.println("還有什麼需要嗎?");
	}
}

// 測試類
public class MainTest {

	public static void main(String[] args) {
		// 創建Waiter及前置增強、後置增強
		Waiter waiter = new Waiter();
		GreetBeforeAdvice greetBeforeAdvice = new GreetBeforeAdvice();
		GreetAfterAdvice greetAfterAdvice = new GreetAfterAdvice();
		// 代理工廠
		ProxyFactory proxyFactory = new ProxyFactory();
		// 設置目標代理類及前置增強、後置增強
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(greetAfterAdvice);
		proxyFactory.addAdvice(greetBeforeAdvice);
		// 創建代理Waiter
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		// 調用方法
		myWaiter.serviceTo("fuhang");
	}
}
// output:
//     hello fuhang service to :fuhang
//     還有什麼需要嗎?
3.3 環繞增強

​ 使用環繞增強實現上面前置後置增強綜合體,示例代碼如下。

// 聲明環繞增強類並實現MethodInterceptor(aopalliance的包)
public class SurrondAdvice implements MethodInterceptor{

	@Override
	public Object invoke(MethodInvocation arg0) throws Throwable {
         // 獲取目標類參數
		Object [] args = arg0.getArguments();
		System.out.println("hello :"+args[0]);
         // 執行目標類方法
		arg0.proceed();
		System.out.println(" 請問還有什麼需要嗎?");
		return null;
	}
}

// 下面代碼不在囉嗦,直接看輸出
public class MainTest {

	public static void main(String[] args) {
		
		Waiter waiter = new Waiter();
		SurrondAdvice surrondAdvice = new SurrondAdvice();
		
		ProxyFactory proxyFactory = new ProxyFactory();
		
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(surrondAdvice);

		
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		
		myWaiter.serviceTo("fuhang");
		
	}
}
// output:
//  	hello :fuhang
//   	service to :fuhang
//		請問還有什麼需要嗎?
3.4 異常拋出增強

​ 異常拋出增強適合的應用場景是事務管理,當參與事務管理的某個Dao拋出異常時候回滾事務。示例如下。

public class Waiter{

	@Override
	public void serviceTo(String name) {
		throw new RuntimeException("Dao異常");
	}
}

// 異常拋出增強類,在異常拋出之前會調用
public class ExceptionAdvice implements ThrowsAdvice{
	
	public void afterThrowing(Method method,Object [] args,Object target,Exception exception){
		System.out.println("事務回滾");
	}
}

// 測試類
public class MainTest {

	public static void main(String[] args) {
		
		Waiter waiter = new Waiter();
		ExceptionAdvice exceptionAdvice = new ExceptionAdvice();
		
		ProxyFactory proxyFactory = new ProxyFactory();
		
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(exceptionAdvice);

		
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		
		myWaiter.serviceTo("fuhang");
		
	}
}
// output:
// 		事務回滾
//		Exception in thread "main" java.lang.RuntimeException: Dao異常

​ ThrowingAdvice是一個標籤接口,其中沒有聲明任何方法,在運行期Spring通過反射機制自動判斷增強方法,增強方法簽名必須是以下格式:

void afterThrowing([Method method,Object [] args,Object obj],Throwable)

方法前三個參數可選,要麼全部傳入,要麼全部不傳。最後一個必須傳入。最後一個參數是Throwable的子類就行。

3.5 引介增強

​ 引介增強比較特殊,他不是在方法周圍增加增強邏輯,而是爲目標類動態添加新的方法和屬性。所以引介增強的連接點定位是類而不是方法上。下面我們通過引介增強爲目標類添加一個接口實現,完成監控開關功能。

// 聲明一個附加接口
public interface Monitor {
	void setMonitorActive(boolean active);
}

// 目標類
public class Waiter{
	public void serviceTo(String name) {
		System.out.println("hello :"+name);
	}
}

// 創建一個類繼承DelegatingIntroductionInterceptor並實現將來要附加到目標類的接口
public class PerformanceMonitor extends DelegatingIntroductionInterceptor implements Monitor{
	// 相當於最終給類添加的屬性
	ThreadLocal<Boolean> isOpen = new ThreadLocal<>();

	@Override
	public void setMonitorActive(boolean active) {
		isOpen.set(active);
	}

	@Override
	public Object invoke(MethodInvocation arg0) throws Throwable {
		if(isOpen.get()!=null && isOpen.get() ){
			Long beginTime = System.currentTimeMillis();
			super.invoke(arg0);
			System.out.println("consume time :"+(System.currentTimeMillis()-beginTime));
		}else{
			super.invoke(arg0);
		}
		return null;
	}

}

// 測試類
public class MainTest {

	public static void main(String[] args) {
		
		Waiter waiter = new Waiter();
		PerformanceMonitor performanceMonitor = new PerformanceMonitor();
		
		ProxyFactory proxyFactory = new ProxyFactory();
		// 下面必須設置setOptimize(true),開啓Cglib代理
		proxyFactory.setInterfaces(Monitor.class);
		proxyFactory.setTarget(waiter);
		proxyFactory.addAdvice(performanceMonitor);
		proxyFactory.setOptimize(true);

		// 第一次調用方法
		Waiter myWaiter = (Waiter) proxyFactory.getProxy();
		myWaiter.serviceTo("fuhang");
		
        // 轉爲Monitor後設置監控開關後再次調用方法
		Monitor monitor = (Monitor)myWaiter;
		monitor.setMonitorActive(true);
		
		myWaiter.serviceTo("zhangsan");
		
	}
}
// output:
// 		hello :fuhang
//		hello :zhangsan
//		consume time :0

總結

​ 以上就是本次學習的主要內容,後面如何將上面手動創建的方法轉換爲Spring xml配置在本篇不做記錄(因爲不難,也沒啥技術含量)。希望通過本次記錄能學到Spring Aop核心知識。

參考資料
《精通Spring4.x企業應用開發實戰》

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