Spring原理與原始碼分析系列(六)- Spring AOP入門與概述

是Guava不是瓜娃發表於2018-03-20

一、AOP

1、什麼是AOP

AOP :Aspect-Oriented Programming,面向切面程式設計的簡稱。

在我們的專案程式碼中,有大量與日誌、事務、許可權(AOP稱之為橫切關注點)相關的程式碼鑲嵌在業務程式碼當中,造成大量程式碼的重複與程式碼的冗餘。
雖然可以將這些重複的程式碼封裝起來再進行呼叫,但是這樣的呼叫方式比較單一,不夠靈活,無法更好地以模組化的方式,對這些橫切關注點進行組織和實現。

這裡寫圖片描述

AOP提出切面(Aspect)的概念,以模組化的方式對橫切關注點進行封裝,再通過織入的方式將切面織入到業務邏輯程式碼當中。這樣橫切關注點與業務邏輯程式碼分離,業務邏輯程式碼中就不再含有日誌、事務、許可權等程式碼的呼叫,可以很好的進行管理。

這裡寫圖片描述

2、AOP基本概念

AOP中的相關術語有:Joinpoint,Pointcut,Advice,Aspect,Introduction,Weaving。
這些概念將在Spring AOP中詳細描述,此處略去。

(1)點(Joinpoint)
連線點是在應用執行過程中能夠插入切面的一個點。這個點可以是呼叫方法時、丟擲異常時、甚至修改一個欄位時。切面程式碼可以利用這些點插入到應用的正常流程之中,並新增行為。

(2)切點(Pointcut)
如果說通知定義了切面“是什麼”和“何時”的話,那麼切點就定義了“何處”。比如我想把日誌引入到某個具體的方法中,這個方法就是所謂的切點。

(3)通知(Advice)
在AOP中,切面的工作被稱為通知。通知定義了切面“是什麼”以及“何時”使用。除了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。

Spring切面可以應用5種型別的通知:

  • 前置通知(Before):在目標方法被呼叫之前呼叫通知功能;
  • 後置通知(After):在目標方法完成之後呼叫通知,此時不會關心方法的輸出是什麼;
  • 返回通知(After-returning):在目標方法成功執行之後呼叫通知;
  • 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知;
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的行為;

(4)切面(Aspect)
切面是通知和切點的結合。通知和切點共同定義了切面的全部內容———他是什麼,在何時和何處完成其功能。

(5)引入(Introduction)
引入允許我們向現有的類新增新的方法和屬性(Spring提供了一個方法注入的功能)。

(6)織入(Weaving)
把切面應用到目標物件來建立新的代理物件的過程,織入一般發生在如下幾個時機:

  • 編譯時:當一個類檔案被編譯時進行織入,這需要特殊的編譯器才可以做的到,例如AspectJ的織入編譯器
  • 類載入時:使用特殊的ClassLoader在目標類被載入到程式之前增強類的位元組程式碼
  • 執行時:切面在執行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理應該是使用了JDK的動態代理技術。

(此處術語解釋來源:
https://www.jianshu.com/p/5155aabaec3f

3、AOP的分類

AOP主要分為靜態AOP和動態AOP。
(1)靜態AOP

靜態AOP,也稱為第一代AOP,是指將相應橫切關注點以Aspect形式實現之後,在編譯階段,通過特定編譯器將Aspect織入到目標類當中。

優點:Aspect直接以Java位元組碼的形式編譯到Java Class類中,Java虛擬機器可以正常載入類,沒有效能損失。
缺點:不夠靈活,如果要修改織入的位置時,就需要修改Aspect和重新編譯。

靜態AOP需要有3個重要因素:

1)共同的介面:定義方法不提供實現,供外部呼叫;
2)實現類:上述介面具體實現;
3)代理類:注入的是實現類,呼叫介面的方法實際是呼叫實現類的方法的實現。

下面通過程式碼來看看靜態AOP是如何實現的。

//共同介面
public interface CommonService {
    public void sayHello();
}
//介面的具體實現
public class CommonServiceImpl implements CommonService {
    @Override
    public void sayHello() {
        System.out.println("hello, 靜態代理!");
    }
}
//代理類
public class StaticProxy implements CommonService {
    private CommonService realService;

    public StaticProxy(CommonService realService){
        this.realService = realService;
    }

    @Override
    public void sayHello() {
        //織入橫切邏輯
        System.out.println("日誌輸出1.。。");
        realService.sayHello();
        //織入橫切邏輯
        System.out.println("日誌輸出2.。。");
    }
}
//測試
public static void main(String[] args) {
     CommonService realService = new CommonServiceImpl();
     StaticProxy proxy = new StaticProxy(realService);
     proxy.sayHello();
}

輸出結果:

日誌輸出1.。。
hello, 靜態代理!
日誌輸出2.。。

可以看到,在執行前,即將橫切邏輯織入到所需要織入的位置。
這裡寫圖片描述

我們為RealService類產生了一個代理物件StaticProxy,假如還有RealService2,RealService3….,就要繼續生成代理物件StaticProxy2,StaticProxy3,,,,,這樣比較麻煩,因此就需要動態AOP來實現。

(2)動態AOP

動態AOP,也稱為第二代AOP,是指在類載入或者系統執行階段,將Aspect程式碼動態織入到目標類當中。
與靜態AOP最大的不同之處在於織入過程發生在系統執行後,而不是預編譯階段。

優點:比較靈活,可以動態更改織入邏輯。
缺點:由於發生階段是在類載入或者系統執行階段,因此會造成執行時效能損失。

動態AOP可以通過JDK動態代理或者Cglib來實現,下面通過JDK動態代理的方式來實現動態AOP。

//同樣需要公共介面
public interface CommonService {
    public void sayHello();
}
//具體實現類
public class CommonServiceImpl implements CommonService {
    @Override
    public void sayHello() {
        System.out.println("hello, 動態代理!");
    }
}
//動態代理類
public class DynamicProxy implements InvocationHandler {
    //要代理的物件
    private Object obj;

    //實際注入的物件
    public DynamicProxy(Object realObj){
        this.obj = realObj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("日誌輸出1.....");
        method.invoke(obj, args);
        System.out.println("日誌輸出1.....");

        return null;
    }
}
//測試:通過公共介面的ClassLoader和代理類生成一個代理類物件
public static void main(String[] args) {
    //產生一個代理類
    InvocationHandler handler = new DynamicProxy(new CommonServiceImpl());
     //產生代理類物件
     CommonService proxyService
         = (CommonService) Proxy.newProxyInstance(CommonService.class.getClassLoader(),
          new Class<?>[]{CommonService.class},
          handler);
     proxyService.sayHello();
}

輸出:

日誌輸出1…..
hello, 動態代理!
日誌輸出2…..

這裡有幾個需要注意的地方:

  • InvocationHandler :InvocationHandler 用來生成動態代理類,它的構造方法注入了具體的實現類,表示被代理的物件;
  • invoke()方法的幾個引數

    proxy:表示代理類物件本身,作用不大
    method:正在被呼叫的方法;
    args:方法的引數;

  • Proxy.newProxyInstance
    該方法是java.lang.reflect包下Proxy類的靜態方法,方法定義如下:
    public static Object newProxyInstance(
    ClassLoader loader,
    Class<?>[] interfaces,
    InvocationHandler h)

    其中:

    loader:表示被代理物件的類載入器;
    interfaces:代理類要實現的介面列表,只能是介面型別;
    h:h的型別為InvocationHandler,它是一個介面,也定義在java.lang.reflect包中,它只定義了一個方法invoke,對代理介面所有方法的呼叫都會轉給該方法

4、AOP實現機制

Java平臺主要提供瞭如下兩種方式實現AOP:

  • 動態代理(Dynamic Proxy)—需要有介面
  • 動態位元組碼增強(Cglib)—不需要介面

(1)動態代理(Dynamic Proxy)
JDK1.3之後,引入了動態代理(Dynamic Proxy)機制。

在執行期間為相應介面動態生成對應的代理物件,再將橫切關注點邏輯封裝到動態代理的InvocationHandler中。在系統執行期間,將橫切邏輯織入到代理類指定位置中。

缺點:動態代理只針對介面有效。

在上節動態AOP中我們已經看到了如何使用JDK動態代理的方式去實現AOP,本節不再贅述。

(2)動態位元組碼增強(Cglib)
有的時候我們無法生成介面,自然就無法使用動態代理的方式,這個時候我們就可以使用ASM(位元組碼增強框架)或者Cglib技術來動態生成位元組碼的Class檔案,而只要符合JVM規範的.class檔案都可以被JVM所載入。

Cglib就是在系統執行期間,通過動態位元組碼增強技術,為需要織入橫切邏輯的目標類生成對應子類,代理類重寫了父類所有public非final的方法(即橫切邏輯加到方法當中)。
當程式呼叫目標類時,通過攔截方法實際最後執行的是具有橫切邏輯的子類。

缺點:如果目標類或目標類中的方法是final的話,就無法進行織入(final類不能被繼承,final方法不能被重寫)。

下面通過程式碼來看看動態位元組碼增強(Cglib)是如何實現AOP的。

//需要織入橫切邏輯的目標類
public class Login {
    public void login(String username){
        if ("admin".equals(username)){
            System.out.println("登入成功!");
        }else {
            System.out.println("登入失敗!");
        }
    }
}

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //織入橫切邏輯
        System.out.println("許可權檢查1,,,");
         //呼叫目標類方法
        Object result = methodProxy.invokeSuper(o, objects);
        //織入橫切邏輯
        System.out.println("許可權檢查2,,,");

        return result;
    }
}

public class CglibTest {

    // 生成代理物件
    private static<T> T getProxy(Class<T> clazz){
        Enhancer enhancer = new Enhancer();
        //將目標類設定為父類
        enhancer.setSuperclass(clazz);
        //設定回撥
        enhancer.setCallback(new MyMethodInterceptor());
        return (T) enhancer.create();
    }
    public static void main(String[] args) {
        Login loginProxy = getProxy(Login.class);
        loginProxy.login("admin");
    }
}

輸出結果:

許可權檢查1,,,
登入成功!
許可權檢查2,,,

可以看到,首先重寫MethodInterceptor介面中的intercept方法,在該方法中完成橫切邏輯的織入以及目標類的方法的呼叫,
然後通過Enhancer類生成代理物件,將目標類設定為代理類,並完成回撥

AOP的內容就介紹到此,下面將介紹Spring AOP的相關理論和實戰。


2018/03/20 in NJ.

相關文章