coding++:Spring 中的 AOP 原理
為什麼使用 AOP 如下場景:
現在有一個情景:
我們要把大象放進冰箱,步驟為:打開冰箱->放入大象->關閉冰箱
如果再把大象拿出來,步驟為:打開冰箱->拿出大象->關閉冰箱
程式碼如下:
public void put() { System.out.println("打開冰箱..."); System.out.println("放入大象..."); System.out.println("關閉冰箱..."); } public void get() { System.out.println("打開冰箱..."); System.out.println("拿出大象..."); System.out.println("關閉冰箱..."); }
我們需要在每一個拿進拿出操作前後都要進行打開冰箱和關閉冰箱的操作,造成了程式碼重複。
而如果要拿進拿出其他動物,那麼每一個動物的操作都需要加入打開冰箱關閉冰箱的操作,十分繁瑣混亂。
解決方法就是AOP,將這些打開冰箱和關閉冰箱的操作單獨抽取出來,做成一個切面,之後調用任何方法,都插入到方法前後即可。
先來看一些基本概念再來解決這個問題。
基本概念:
AOP 即Aspect Oriented Program,面向切面編程。
使用AOP技術,可以將一些系統性相關的編程工作,獨立提取出來,獨立實現,然後通過切面切入進系統。
從而避免了在業務邏輯的程式碼中混入很多的系統相關的邏輯——比如許可權管理,事物管理,日誌記錄等等。
這些系統性的編程工作都可以獨立編碼實現,然後通過AOP技術切入進系統即可。從而達到了 將不同的關注點分離出來的效果。
切面(Aspect):其實就是共有功能的實現。
如日誌切面、許可權切面、事務切面等。
在實際應用中通常是一個存放共有功能實現的普通Java類,之所以能被AOP容器識別成切面,是在配置中指定的。
通知/增強(Advice):是切面的具體實現。以目標方法為參照點,根據放置的地方不同,可分為前置通知(Before)、後置通知(AfterReturning)、異常通知(AfterThrowing)、最終通知(After)與環繞通知(Around)5種。
在實際應用中通常是切面類中的一個方法,具體屬於哪類通知,同樣是在配置中指定的。
連接點(Joinpoint):就是程式在運行過程中能夠插入切面的地點。
例如,方法調用、異常拋出或欄位修改等,但Spring只支援方法級的連接點。
切入點(Pointcut):用於定義通知應該切入到哪些連接點上。
不同的通知通常需要切入到不同的連接點上,這種精準的匹配是由切入點的正則表達式來定義的。
目標對象(Target):就是那些即將切入切面的對象,也就是那些被通知的對象。
這些對象中已經只剩下乾乾淨淨的核心業務邏輯程式碼了,所有的共有功能程式碼等待AOP容器的切入。
代理對象(Proxy):將通知應用到目標對象之後被動態創建的對象。
可以簡單地理解為,代理對象的功能等於目標對象的核心業務邏輯功能加上共有功能。
代理對象對於使用者而言是透明的,是程式運行過程中的產物。
織入(Weaving):將切面應用到目標對象從而創建一個新的代理對象的過程。
這個過程可以發生在編譯期、類裝載期及運行期,當然不同的發生點有著不同的前提條件。
譬如發生在編譯期的話,就要求有一個支援這種AOP實現的特殊編譯器;發生在類裝載期,就要求有一個支援AOP實現的特殊類裝載器;只有發生在運行期,則可直接通過Java語言的反射機制與動態代理機制來動態實現。
AOP 原理:
AOP 代理可分為靜態代理和動態代理兩大類,
靜態代理:使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時增強;
動態代理:在運行時藉助於 JDK 動態代理、CGLIB(code generate libary)位元組碼生成技術 等在記憶體中「臨時」生成 AOP 動態代理類,因此也被稱為運行時增強
Spring AOP採用的是動態代理,在運行期間對業務方法進行增強,所以不會生成新類。
對於動態代理技術,Spring AOP提供了對JDK動態代理的支援以及CGLib的支援。
前者是基於反射技術的實現,後者是基於繼承的機制實現。
如果目標對象有實現介面,使用jdk代理。
如果目標對象沒有實現介面,則使用Cglib代理。
JDK:動態代理:
JDK動態代理需要獲得被目標類的介面資訊(應用Java的反射),生成一個實現了代理介面的動態代理類(位元組碼),再通過反射機制獲得動態代理類的構造函數,利用構造函數生成動態代理類的實例對象,在調用具體方法前調用
invokeHandler方法來處理。
主要使用到 InvocationHandler 介面和 Proxy.newProxyInstance() 方法。
JDK動態代理要求被代理的類實現一個介面,只有介面中的方法才能夠被代理 。
其方法是將被代理對象注入到一個中間對象,而中間對象實現InvocationHandler介面,在實現該介面時,可以在被代理對象調用它的方法時,在調用的前後插入一些程式碼。
而 Proxy.newProxyInstance() 能夠利用中間對象來生產代理對象。
插入的程式碼就是切面程式碼。所以使用JDK動態代理可以實現AOP。
現在演示一下如何使用JDK動態代理實現開頭的情景
JDK動態代理需要被代理類實現一個介面,先寫一個介面。
public interface AnimalOperation { public void put(); public void get(); }
再寫一個類(要被代理的類),實現這個介面
public class ElephantOperation implements AnimalOperation{ public void put() { System.out.println("放入大象..."); } public void get() { System.out.println("拿出大象..."); } }
然後寫一個類來實現InvocationHandler介面,在該類中對被代理類的方法做增強,並編寫生成代理對象的方法
public class FridgeJDKProxy implements InvocationHandler{ //被代理的對象,之後用反射調用被代理方法的時候需要被代理對象的引用 private Object target; //InvocationHandler介面的方法, // proxy是代理對象,method是被代理的方法,args是被代理方法的參數,返回值是原方法的返回 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { openDoor();//調用被代理方法做一些操作 Object result = method.invoke(target, args);//執行被代理對象的方法,如果方法有返回值則賦值給result closeDoor();//調用被代理方法後做一些操作 return result; } private void openDoor(){ System.out.println("打開冰箱..."); } private void closeDoor(){ System.out.println("關閉冰箱..."); } public Object getProxy(Object target){ this.target=target; return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this); } }
其中Proxy.newProxyInstance()方法需要的參數分別為,類載入器ClassLoader loader,介面數組Class<?>[] interfaces,與 InvocationHandler
測試程式碼為:
public static void main(String args[]) { AnimalOperation elephantOperation =(AnimalOperation) new FridgeJDKProxy().getProxy(new ElephantOperation()); elephantOperation.put(); elephantOperation.get(); }
列印結果:
CGLIB 動態代理:
位元組碼生成技術實現AOP,其實就是繼承被代理對象,然後Override需要被代理的方法,在覆蓋該方法時,自然是可以插入我們自己的程式碼的。
CGLib動態代理需要依賴asm包,把被代理對象類的class文件載入進來,修改其位元組碼生成子類。
因為需要Override被代理對象的方法,所以自然CGLIB技術實現AOP時,就 必須要求需要被代理的方法不能是final方法,因為final方法不能被子類覆蓋 。
現在演示一下如何使用CGLIB動態代理實現開頭的情景
CGLIB動態代理不要求被代理類實現介面,先寫一個被代理類。
public class MonkeyOperation { public void put() { System.out.println("放入猴子..."); } public void get() { System.out.println("拿出猴子..."); } }
在寫一個類實現MethodInterceptor介面,並在介面方法intercept()里對被代理對象的方法做增強,並編寫生成代理對象的方法
public class FridgeCGLibProxy implements MethodInterceptor { public String name="hahaha"; public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { openDoor();//調用被代理方法做一些操作 Object result = methodProxy.invokeSuper(proxy,args);//執行被代理對象的方法,如果方法有返回值則賦值給result closeDoor();//調用被代理方法後做一些操作 return result; } private void openDoor(){ System.out.println("打開冰箱..."); } private void closeDoor(){ System.out.println("關閉冰箱..."); } public Object getProxy(Class cls){//參數為被代理的類對象 Enhancer enhancer = new Enhancer();//創建增強器,用來創建動態代理類 enhancer.setSuperclass(cls);//設置父類,即被代理的類對象 enhancer.setCallback(this);//設置回調,指定為當前對象 return enhancer.create();//返回生成的代理類 } }
測試程式碼:
public static void main(String args[]) { MonkeyOperation monkeyOperation =(MonkeyOperation)new FridgeCGLibProxy().getProxy(MonkeyOperation.class); monkeyOperation.put(); monkeyOperation.get(); }
列印結果:
spring實現AOP,如果被代理對象實現了介面,那麼就使用JDK的動態代理技術,反之則使用CGLIB來實現AOP,所以 Spring默認是使用JDK的動態代理技術實現AOP的 。