coding++:Spring 中的 AOP 原理

coding++發表於2020-04-16

為什麼使用 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的 。

 

相關文章