1. 程式人生 > >Spring系列之AOP

Spring系列之AOP

-a cte implement 結合 動態擴展 分離 可操作性 技術 其中

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

而AOP技術則恰恰相反,它利用一種稱為“橫切”的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行為封裝到一個可重用模塊,並將其名為"Aspect",即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻為業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重復代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關系,如果說“對象”是一個空心的圓柱體,其中封裝的是對象的屬性和行為;那麽面向方面編程的方法,就仿佛一把利刃,將這些空心圓柱體剖開,以獲得其內部的消息。而剖開的切面,也就是所謂的“方面”了。然後它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。

使用“橫切”技術,AOP把軟件系統分為兩個部分:核心關註點和橫切關註點。業務處理的主要流程是核心關註點,與之關系不大的部分是橫切關註點。橫切關註點的一個特點是,他們經常發生在核心關註點的多處,而各處都基本相似。比如權限認證、日誌、事務處理。Aop 的作用在於分離系統中的各種關註點,將核心關註點和橫切關註點分離開來。正如Avanade公司的高級方案構架師Adam Magee所說,AOP的核心思想就是“將應用程序中的商業邏輯同對其提供支持的通用服務進行分離。”

實現AOP的技術,主要分為兩大類:一是采用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行為的執行;二是采用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。

二、AOP使用場景
AOP用來封裝橫切關註點,具體可以在下面的場景中使用:
Authentication 權限
Caching 緩存
Context passing 內容傳遞
Error handling 錯誤處理
Lazy loading 懶加載
Debugging  調試
logging, tracing, profiling and monitoring 記錄跟蹤 優化 校準
Performance optimization 性能優化
Persistence  持久化
Resource pooling 資源池
Synchronization 同步
Transactions 事務

三、AOP相關概念

1.連接點(Joinpoint)
程序執行的某個特定位置:如類開始初始化之前、類初始化之後、類某個方法調用前、調用後等;一個類或一段程序代碼擁有一些具有邊界性質的特定點,這些代碼中的特定點就成為“連接點”,Spring僅支持方法的連接點,即僅能在方法調用前、方法調用後以及方法調用前後的這些程序執行點織入增強。比如:黑客攻擊系統需要找到突破口,沒有突破口就沒有辦法攻擊,從某種程度上來說,AOP就是一個黑客,連接點就是AOP向目標類攻擊的候選點。

連接點有兩個信息確定:第一是用方法表示的程序執行點;第二是用相對點表示的方位;如在Test.foo()方法執行前的連接點,執行點為Test.foo,方位為該方法執行前的位置。Spring使用切點對執行點進行定位,而方位則在增強類型中定義。

2.切點(Pointcut)
每個程序類都擁有許多連接點,如一個擁有兩個方法的類,這兩個方法都是連接點,即連接點是程序類中客觀存在的事物。但在為數眾多的連接點中,如何定位到某個連接點上呢?AOP通過切點定位特定連接點。通過數據庫查詢的概念來理解切點和連接點:連接點相當於數據庫表中的記錄,而切點相當於查詢條件。連接點和切點不是一一對應的關系,一個切點可以匹配多個連接點。

在Spring中,切點通過org.springframework.aop.Pointcut接口進行描述,它使用類和方法作為連接點的查詢條件,Spring AOP的規則解析引擎負責解析切點所設定的查詢條件,找到對應的連接點;其實確切的說應該是執行點而非連接點,因為連接點是方法執行前、執行後等包括方位信息的具體程序執行點,而切點只定位到某個方法上,所以如果希望定位到具體連接點上,還需要提供方位信息。

3.增強(Advice)
增強是織入到目標類連接點上的一段程序代碼(好比AOP以黑客的身份往業務類中裝入木馬),增強還擁有一個和連接點相關的信息,這便是執行點的方位。結合執行點方位信息和切點信息,我們就可以找到特定的連接點了,所以Spring提供的增強接口都是帶方位名的:BefortAdvice、AfterReturningAdvice、ThrowsAdvice等。(有些將Advice翻譯為通知,但通知就是把某個消息傳達給被通知者,並沒有為被通知者做任何事情,而Spring的Advice必須嵌入到某個類的連接點上,並完成了一段附加的應用邏輯;)

4.目標對象(Target)
增強邏輯的織入目標類,如果沒有AOP,目標業務類需要自己實現所有邏輯,在AOP的幫助下,目標類只實現那些非橫切邏輯的程序邏輯,而其他監測代碼則可以使用AOP動態織入到特定的連接點上。

5.引介(Introduction)
引介是一種特殊的增強,它為類添加一些屬性和方法,這樣即使一個業務類原本沒有實現某個接口,通過AOP的引介功能,我們可以動態的為該業務類添加接口的實現邏輯,讓這個業務類成為這個接口的實現類。

6.織入(Weaving)
織入是將增強添加到目標類具體連接點上的過程,AOP就像一臺織布機,將目標類、增強或者引介編織到一起,AOP有三種織入的方式:
a.編譯期間織入,這要求使用特殊的java編譯器;
b.類裝載期織入,這要求使用特殊的類裝載器;
c.動態代理織入,在運行期為目標類添加增強生成子類的方式。
Spring采用動態代理織入,而AspectJ采用編譯器織入和類裝載期織入。

7.代理(Proxy)
一個類被AOP織入增強後,就產生出了一個結果類,它是融合了原類和增強邏輯的代理類。

8.切面(Aspect)
切面由切點和增強組成,它既包括了橫切邏輯的定義,也包括了連接點的定義,Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯織入到切面所指定的連接點中。

總結:AOP的工作重點就是如何將增強應用於目標對象的連接點上,這裏首先包括兩個工作:第一,如何通過切點和增強定位到連接點;第二,如何在增強中編寫切面的代碼。

四、實現原理

1.代理模式

代理的概念:簡單的理解就是通過為某一個對象創建一個代理對象,我們不直接引用原本的對象,而是由創建的代理對象來控制對原對象的引用。

代理模式是常用的java設計模式,他的特征是代理類與委托類(或目標類)有同樣的接口,代理類主要負責為委托類預處理消息、過濾消息、把消息轉發給委托類,以及事後處理消息等。代理類與委托類之間通常會存在關聯關系,一個代理類的對象與一個委托類的對象關聯,代理類的對象本身並不真正實現服務,而是通過調用委托類的對象的相關方法,來提供特定的服務。
按照代理的創建時期,代理類可以分為兩種。
靜態代理:由程序員創建或特定工具自動生成源代碼,再對其編譯。在程序運行前,代理類的.class文件就已經存在了。
動態代理:在程序運行時,運用反射機制動態創建而成,無需手動編寫代碼。動態代理不僅簡化了編程工作,而且提高了軟件系統的可擴展性,因為Java反射機制可以生成任意類型的動態代理類。

代理原理:代理對象內部含有對真實對象的引用,從而可以操作真實對象,同時代理對象提供與真實對象相同的接口以便在任何時刻都能代替真實對象。同時,代理對象可以在執行真實對象操作時,附加其他的操作,相當於對真實對象進行封裝。

2.AOP的實現原理

AOP的實現關鍵在於AOP框架自動創建的AOP代理。AOP代理主要分為兩大類:
.靜態代理:使用AOP框架提供的命令進行編譯,從而在編譯階段就可以生成AOP代理類,因此也稱為編譯時增強;靜態代理一Aspectj為代表。
.動態代理:在運行時借助於JDK動態代理,CGLIB等在內存中臨時生成AOP動態代理類,因此也被稱為運行時增強,Spring AOP用的就是動態代理。

AOP分為靜態AOP和動態AOP。靜態AOP是指AspectJ實現的AOP,他是將切面代碼直接編譯到Java類文件中。動態AOP是指將切面代碼進行動態織入實現的AOP。Spring的AOP為動態AOP,實現的技術為:JDK提供的動態代理技術 CGLIB(動態字節碼增強技術)盡管實現技術不一樣,但都是基於代理模式,都是生成一個代理對象。

(1)JDK動態代理

a. JDK動態代理是面向接口的,必須提供一個委托類和代理類都要實現的接口,只有接口中的方法才能夠被代理。
b. JDK動態代理的實現主要使用java.lang.reflect包裏的Proxy類和InvocationHandler接口。

InvocationHandler接口:

來看看java的API幫助文檔是怎麽樣描述InvocationHandler接口的:

InvocationHandler is the interface implemented by the invocation handler of a proxy instance. 

Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and 
dispatched to the invoke method of its invocation handler.

說明:每一個動態代理類都必須要實現InvocationHandler這個接口,並且每個代理類的實例都關聯到了一個handler,當我們通過代理對象調用一個方法的時候,這個方法的調用就會被轉發為由InvocationHandler這個接口的 invoke 方法來進行調用。同時在invoke的方法裏 我們可以對被代理對象的方法調用做增強處理(如添加事務、日誌、權限驗證等操作)。我們來看看InvocationHandler這個接口的唯一一個方法 invoke 方法:

public interface InvocationHandler { 
     public Object invoke(Object proxy,Method method,Object[] args) throws Throwable; 
}

參數說明:
Object proxy:指被代理的對象。
Method method:要調用的方法。(指代的是我們所要調用代理對象的某個方法的Method對象)
Object[] args:方法調用時所需要的參數。(指代的是調用真實對象某個方法時接受的參數)

可以將InvocationHandler接口的子類想象成一個代理的最終操作類,替換掉ProxySubject。

Proxy類:
Proxy類是專門完成代理的操作類,可以通過此類為一個或多個接口動態地生成實現類,此類提供了如下的操作方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException 

參數說明:
ClassLoader loader:類加載器
Class<?>[] interfaces:得到全部的接口
InvocationHandler h:得到InvocationHandler接口的子類實例

JDK動態代理示例:

定義一個業務接口IUserService,如下:

技術分享
package com.spring.aop;

public interface IUserService {
    //添加用戶
    public void addUser();
    //刪除用戶
    public void deleteUser();
}
技術分享

一個簡單的實現類UserServiceImpl,如下:

技術分享
package com.spring.aop;

public class UserServiceImpl implements IUserService{
    
    public void addUser(){
        System.out.println("新增了一個用戶!");
    }
    
    public void deleteUser(){
        System.out.println("刪除了一個用戶!");
    }
}
技術分享

現在我們要實現的是,在addUser和deleteUser之前和之後分別動態植入處理。
JDK動態代理主要用到java.lang.reflect包中的兩個類:Proxy和InvocationHandler。
InvocationHandler是一個接口,通過實現該接口定義橫切邏輯,並通過反射機制調用目標類的代碼,動態的將橫切邏輯和業務邏輯編織在一起。
Proxy利用InvocationHandler動態創建一個符合某一接口的實例,生成目標類的代理對象。
如下,我們創建一個InvocationHandler實例DynamicProxy:(當執行動態代理對象裏的目標方法時,實際上會替換成調用DynamicProxy的invoke方法)

技術分享
package com.spring.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DynamicProxy implements InvocationHandler{
    
    //被代理對象(就是要給這個目標類創建代理對象)
    private Object target;
    
    //傳遞代理目標的實例,因為代理處理器需要,也可以用set等方法。
    public DynamicProxy(Object target){
        this.target=target;
    }
    
    /**
     * 覆蓋java.lang.reflect.InvocationHandler的方法invoke()進行織入(增強)的操作。
     * 這個方法是給代理對象調用的,留心的是內部的method調用的對象是目標對象,可別寫錯。
     * 參數說明:
     * proxy是生成的代理對象,method是代理的方法,args是方法接收的參數
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        //目標方法之前執行
        System.out.println("do sth Before...");
        //通過反射機制來調用目標類方法
        Object result = method.invoke(target, args);
        //目標方法之後執行
        System.out.println("do sth After...\n");
        return result;
    }
}
技術分享

下面是測試:

技術分享
package com.spring.aop;

//用java.lang.reflect.Proxy.newProxyInstance()方法創建動態實例來調用代理實例的方法
import java.lang.reflect.Proxy;

public class DynamicTest {
    
    public static void main(String[] args){
        //希望被代理的目標業務類
        IUserService target = new UserServiceImpl();
        //將目標類和橫切類編織在一起
        DynamicProxy handler= new DynamicProxy(target);
        //創建代理實例,它可以看作是要代理的目標業務類的加多了橫切代碼(方法)的一個子類
        //創建代理實例(使用Proxy類和自定義的調用處理邏輯(handler)來生成一個代理對象)
        IUserService proxy = (IUserService)Proxy.newProxyInstance(
                target.getClass().getClassLoader(),//目標類的類加載器
                target.getClass().getInterfaces(), //目標類的接口
                handler); //橫切類
        proxy.addUser();
        proxy.deleteUser();
    }
}
技術分享

說明:上面的代碼完成業務類代碼和橫切代碼的編制工作,並生成了代理實例,newProxyInstance方法的第一個參數為類加載器,第二個參數為目標類所實現的一組接口,第三個參數是整合了業務邏輯和橫切邏輯的編織器對象。

每一個動態代理實例的調用都要通過InvocationHandler接口的handler(調用處理器)來調用,動態代理不做任何執行操作,只是在創建動態代理時,把要實現的接口和handler關聯,動態代理要幫助被代理執行的任務,要轉交給handler來執行。其實就是調用invoke方法。(可以看到執行代理實例的addUser()和deleteUser()方法時執行的是DynamicProxy的invoke()方法。)

運行結果:
技術分享

基本流程:用Proxy類創建目標類的動態代理,創建時需要指定一個自己實現InvocationHandler接口的回調類的對象,這個回調類中有一個invoke()用於攔截對目標類各個方法的調用。創建好代理後就可以直接在代理上調用目標對象的各個方法。

實現動態代理步驟:
A. 創建一個實現接口InvocationHandler的類,他必須實現invoke方法。
B.創建被代理的類以及接口。
C.通過Proxy的靜態方法newProxyInstance(ClassLoader loader, Class<?>[]interfaces, InvocationHandler handler)創建一個代理。
D.通過代理調用方法。

使用JDK動態代理有一個很大的限制,就是它要求目標類必須實現了對應方法的接口,它只能為接口創建代理實例。我們在上文測試類中的Proxy的newProxyInstance方法中可以看到,該方法第二個參數便是目標類的接口。如果該類沒有實現接口,這就要靠cglib動態代理了。

(2)CGLIB動態代理

CGLib采用非常底層的字節碼技術,可以為一個類創建一個子類,並在子類中采用方法攔截的技術攔截所有父類方法的調用,並順勢植入橫切邏輯。

字節碼生成技術實現AOP,其實就是繼承被代理對象,然後Override需要被代理的方法,在覆蓋該方法時,自然是可以插入我們自己的代碼的。因為需要Override被代理對象的方法,所以自然用CGLIB技術實現AOP時,就必須要求需要被代理的方法不能是final方法,因為final方法不能被子類覆蓋。

a.使用CGLIB動態代理不要求必須有接口,生成的代理對象是目標對象的子類對象,所以需要代理的方法不能是private或者final或者static的。
b.使用CGLIB動態代理需要有對cglib的jar包依賴(導入asm.jar和cglib-nodep-2.1_3.jar)

CGLibProxy與JDKProxy的代理機制基本類似,只是其動態代理的代理對象並非某個接口的實現,而是針對目標類擴展的子類。換句話說JDKProxy返回動態代理類,是目標類所實現接口的另一個實現版本,它實現了對目標類的代理(如同UserDAOProxy與UserDAOImp的關系),而CGLibProxy返回的動態代理類,則是目標代理類的一個子類(代理類擴展了UserDaoImpl類)

cglib 代理特點:
CGLIB 是針對類來實現代理,它的原理是對指定的目標類生成一個子類,並覆蓋其中方法。因為采用的是繼承,所以不能對 finall 類進行繼承

我們使用CGLIB實現上面的例子:

代理的最終操作類:

技術分享
package com.spring.aop;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CglibProxy implements MethodInterceptor{
    
    //增強器,動態代碼生成器
    Enhancer enhancer = new Enhancer();
    
    /**
     * 創建代理對象
     * @param clazz
     * @return 返回代理對象
     */
    public Object getProxy(Class clazz){
        //設置父類,也就是被代理的類(目標類)
        enhancer.setSuperclass(clazz);
        //設置回調(在調用父類方法時,回調this.intercept())
        enhancer.setCallback(this);
        //通過字節碼技術動態創建子類實例(動態擴展了UserServiceImpl類)
        return enhancer.create();
    }
    
    /**
     * 攔截方法:在代理實例上攔截並處理目標方法的調用,返回結果
     * obj:目標對象代理的實例;
     * method:目標對象調用父類方法的method實例;
     * args:調用父類方法傳遞參數;
     * proxy:代理的方法去調用目標方法
     */
    public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) 
        throws Throwable{
        
        System.out.println("--------測試intercept方法的四個參數的含義-----------");
        System.out.println("obj:"+obj.getClass());
        System.out.println("method:"+method.getName());
        System.out.println("proxy:"+proxy.getSuperName());
        if(args!=null&&args.length>0){
            for(Object value : args){
                System.out.println("args:"+value);
            }
        }

        //目標方法之前執行
        System.out.println("do sth Before...");
        //目標方法調用
        //通過代理類實例調用父類的方法,即是目標業務類方法的調用
        Object result = proxy.invokeSuper(obj, args);
        //目標方法之後執行
        System.out.println("do sth After...\n");
        return result;
    }
}
技術分享

測試類:

技術分享
package com.spring.aop;

public class CglibProxyTest {
    
    public static void main(String[] args){
        CglibProxy proxy=new CglibProxy();
        //通過java.lang.reflect.Proxy的getProxy()動態生成目標業務類的子類,即是代理類,再由此得到代理實例
        //通過動態生成子類的方式創建代理類
        IUserService target=(IUserService)proxy.getProxy(UserServiceImpl.class);
        target.addUser();
        target.deleteUser();
    }
}
技術分享

基本流程:需要自己寫代理類,它實現MethodInterceptor接口,有一個intercept()回調方法用於攔截對目標方法的調用,裏面使用methodProxy來調用目標方法。創建代理對象要用Enhance類,用它設置好代理的目標類、由intercept()回調的代理類實例、最後用create()創建並返回代理實例。

輸出:

技術分享

我們看到達到了同樣的效果。它的原理是生成一個父類enhancer.setSuperclass(clazz)的子類enhancer.create(),然後對父類的方法進行攔截enhancer.setCallback(this). 對父類的方法進行覆蓋,所以父類方法不能是final的。

總結:
  (1).通過輸出可以看出,最終調用的是com.spring.aop.UserServiceImpl的子類(也是代理類)com.spring.aop.UserServiceImpl$$EnhancerByCGLIB$$43831205的方法。
  (2). private,final和static修飾的方法不能被代理。

註意:
  (1).CGLIB是通過實現目標類的子類來實現代理,不需要定義接口。
  (2).生成代理對象使用最多的是通過Enhancer和繼承了Callback接口的MethodInterceptor接口來生成代理對象,設置callback對象的作用是當調用代理對象方法的時候會交給callback對象的來處理。
  (3).創建子類對象是通過使用Enhancer類的對象,通過設置enhancer.setSuperClass(Class class)和enhancer.setCallback(Callback callback)來創建代理對象。

解釋MethodInterceptor接口的intercept方法:

Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;

參數說明:Object var1代表的是子類代理對象,Method var2代表的是要調用的方法反射對象,第三個參數是傳遞給調用方法的參數,前三個參數和JDK的InvocationHandler接口的invoke方法中參數含義是一樣的,第四個參數MethodProxy對象是cglib生成的用來代替method對象的,使用此對象會比jdk的method對象的效率要高。

如果使用method對象來調用目標對象的方法: method.invoke(var1, var3),則會陷入無限遞歸循環中, 因為此時的目標對象是目標類的子代理類對象。

MethodProxy類提供了兩個invoke方法:

public Object invokeSuper(Object obj, Object[] args) throws Throwable;
public Object invoke(Object obj, Object[] args) throws Throwable;

註意此時應該使用invokeSuper()方法,顧名思義調用的是父類的方法,若使用invoke方法,則需要提供一個目標類對象,但我們只有目標類子類代理對象,所以會陷入無限遞歸循環中。

CGLIB所創建的動態代理對象的性能比JDK所創建的動態代理對象的性能高很多,但創建動態代理對象時比JDK創建動態代理對象要花費更長的時間。

JDK代理和CGLIB代理的總結(生成代理對象的前提是有AOP切入)
(1)、如果目標對象實現了接口,默認情況下會采用JDK的動態代理實現AOP。 如果就是單純的用IOC生成一個對象,也沒有AOP的切入不會生成代理的,只會NEW一個實例,給Spring的Bean工廠。
(2)、如果目標對象實現了接口,可以強制使用CGLIB實現AOP
如何強制使用CGLIB實現AOP
* 添加CGLIB庫
* 在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>就能強制使用
(3)、如果目標對象沒有實現了接口,必須采用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換(沒有實現接口的就用CGLIB代理,使用了接口的類就用JDK動態代理)

JDK動態代理和CGLIB字節碼生成的區別:
(1)、JDK動態代理只能對實現了接口的類生成代理,而不能針對類。CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法。因為是繼承,所以該類或方法最好不要聲明成final。
(2)、JDK代理是不需要依賴第三方的庫,只要JDK環境就可以進行代理,它有幾個要求
* 實現InvocationHandler;
* 使用Proxy.newProxyInstance產生代理對象;
* 被代理的對象必須要實現接口;
CGLib 必須依賴於CGLib的類庫,但是它需要類來實現任何接口代理的是指定的類生成一個子類,覆蓋其中的方法,是一種繼承。
(3)、jdk的核心是實現InvocationHandler接口,使用invoke()方法進行面向切面的處理,調用相應的通知。cglib的核心是實現MethodInterceptor接口,使用intercept()方法進行面向切面的處理,調用相應的通知。

五、小結

AOP 廣泛應用於處理一些具有橫切性質的系統級服務,AOP 的出現是對 OOP 的良好補充,它使得開發者能用更優雅的方式處理具有橫切性質的服務。不管是哪種 AOP 實現,不論是 AspectJ、還是 Spring AOP,它們都需要動態地生成一個 AOP 代理類,區別只是生成 AOP 代理類的時機不同:AspectJ 采用編譯時生成 AOP 代理類,因此具有更好的性能,但需要使用特定的編譯器進行處理;而 Spring AOP 則采用運行時生成 AOP 代理類,因此無需使用特定編譯器進行處理。由於 Spring AOP 需要在每次運行時生成 AOP 代理,因此性能略差一些。

Spring系列之AOP