Spring AOP 超詳細分析

Spring核心機制之 AOP

1. Spring AOP簡介

衆所周知,Spring的兩大核心機制爲:java

  • IoC
  • AOP (Aspect Oriented Programming,面向切面編程)

本篇博客就來細細品一下AOP的實現以及它的原理;web

AOP意味面向切面編程,咱們以前一直使用的是OOP(面向對象編程):spring

  • OOP:將程序中全部參與模塊都抽象化爲對象,而後經過對象之間的相互調用來完成業務需求;
  • AOP:是對OOP的一個補充,是在另一個維度上抽象出對象,具體是指程序運行時動態的將非業務代碼切入到業務代碼中,從而實現代碼的解耦和;
    在這裏插入圖片描述

AOP的優勢

  • 下降模塊耦合度
  • 使系統容易擴展
  • 延遲設計決定:使用AOP,設計師能夠推遲爲未來的需求做決定,由於需求做爲獨立的方面很容易實現
  • 更好的代碼複用性

2. AOP例子實踐

1. 案例起源

建立一個計算機接口Com,定義如下四個方法:編程

public interface Com {
    int add(int x, int y);
    int sub(int x, int y);
    int mul(int x, int y);
    int div(int x, int y);
}

定義其實現類:ComImpl:app

public class ComImpl implements Com {
    public int add(int x, int y) {
        int result = x+y;
        return result;
    }

    public int sub(int x, int y) {
        int result = x-y;
        return result;
    }

    public int mul(int x, int y) {
        int result = x*y;
        return result;
    }

    public int div(int x, int y) {
        int result = x/y;
        return result;
    }
}

測試類:框架

public class Test {
    public static void main(String[] args) {
        Com com = new ComImpl();
        System.out.println(com.add(1,4));
    }
}

輸出:
5
15svg

這個例子很簡單,這裏不作描述了,接下來看新的需求測試

  • 要求在每一個方法執行的同時,完成打印日誌信息;

這個需求也很簡單,咱們能夠經過在ComImpl類中的四個方法中每一個方法都添加以下:this

public int add(int x, int y) {
        System.out.println("add方法的參數是:"+x+","+y);
        int result = x+y;
        System.out.println("add方法的結果爲:"+result);
        return result;
    }

其餘三個方法也相似,我就再也不寫其代碼,這種方法能完成業務需求,可是其弊端顯著:spa

  • 重複代碼過多,代碼冗餘;
  • 業務代碼和打印日誌代碼耦合性很是高,不利於後期的維護;
    • 例如: 假如我要對打印日誌的格式稍做修改,我就得去改變四個方法中得打印日誌部分,假若有100個方法呢?每次都要手動去修改100個方法中的日誌打印這塊?

那麼如何解決這個問題呢?
咱們能夠發現日誌打印的代碼基本都是一個格式的,咱們能不能將這些相同部分的代碼提取出來造成一個橫切面呢?而且將這個橫切面抽象成一個對象,將全部的打印日誌代碼寫到這個對象中,以實現業務和代碼的分離;
上面就是AOP的思想 ☝☝☝☝

2. 靜態代理實現AOP

靜態代理的要求:

  • 代理對象和被代理對象實現同一個接口,接口中包含着真實業務;
  • 代理對象注入被代理對象,同時能夠添加輔助業務;

在這裏插入圖片描述

缺點:實質上仍是比較繁雜,由於你仍是須要在代理類的每一個真實業務中添加本身的輔助業務,這樣仍是有許多重複的代碼,不便於擴展;

3. 動態代理實現AOP

上面的思想咱們能夠用動態代理來實現;

對於ComImpl,咱們只保留其業務代碼(即最初的版本,不在其中添加日誌代碼);

1. 動態代理類的實現

建立MyInvocationHandler類,這個類實現InvocationHandler接口,成爲一個動態代理類:

public class MyInvocationHandler implements InvocationHandler {
    Object targetObj;
    //返回代理對象
    public Object bind(Object targetObj) {
        this.targetObj = targetObj;
        return Proxy.newProxyInstance(this.targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(),
                this);
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        //日誌業務
        System.out.println(method.getName()+"的參數是:"+Arrays.toString(args));
        //主業務
        result = method.invoke(this.targetObj, args);
        //日誌業務
        System.out.println(method.getName()+"的結果是:"+result);
        return result;
    }
}

bind 方法是 MyInvocationHandler 類提供給外部調用的方法,傳入委託對象,bind 方法會返回一個代理對象,bind 方法完成了兩項工做:

  • (1)將外部傳進來的委託對象保存到成員變量中,由於業務方法調用時須要用到委託對象。
  • (2)經過 Proxy.newProxyInstance 方法建立一個代理對象,解釋一下 Proxy.newProxyInstance 方法的參數:
    • 咱們知道對象是 JVM 根據運行時類來建立的,此時須要動態建立一個代理對象的運行時類,同時須要將這個動態建立的運行時類加載到 JVM 中,這一步須要獲取到類加載器才能實現,咱們能夠經過委託對象的運行時類來反向獲取類加載器,obj.getClass().getClassLoader() 就是經過委託對象的運行時類來獲取類加載器的具體實現;
    • 同時代理對象須要具有委託對象的全部功能,即須要擁有委託對象的全部接口,所以傳入obj.getClass().getInterfaces();
    • this 指當前 MyInvocationHandler 對象。

以上所有是反射的知識點,invoke 方法:method 是描述委託對象全部方法的對象,agrs 是描述委託對象方法參數列表的對象。 method.invoke(this.obj,args) 是經過反射機制來調用委託對象的方法,即業務方法。 所以在 method.invoke(this.obj, args) 先後添加打印日誌信息,就等同於在委託對象的業務方法先後添加打印日誌信息,而且已經作到了分類,業務方法在委託對象中,打印日誌信息在代理對象中;

2. 測試

給出測試類:

public class Test {
    public static void main(String[] args) {
        //真實業務類
        Com com = new ComImpl();
        //代理類的對象(這個不叫代理對象)
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler();
        //根據代理類的對象獲取代理對象
        Com com1 = (Com) myInvocationHandler.bind(com);
        com1.add(1,5);
        com1.sub(6, 3);
    }
}

輸出:
add的參數是:[1, 5]
add的結果是:6
sub的參數是:[6, 3]
sub的結果是:3

3. 結果分析

從測試中咱們能夠看到已經達到了咱們的要求,業務和日誌都能正確實現,並且:

  • 業務和日誌代碼分離,ComImpl類中只有業務代碼,而日誌代碼在MyInvocationHandler類中;

3. Spring中的AOP

在上面的案例中,咱們用動態代理實現了AOP,可是在Spring中,咱們不須要建立MyInvocationHandler類,Spring已經對其完成了封裝,咱們只須要建立一個切面類,Spring底層會自動根據切面類以及目標類生成一個代理對象;

1. 第一步:建立一個切面類:LoggerAspect

@Aspect
@Component
public class LoggerAspect {
    //int表明返回值類型,必須寫,後面是具體類名,*表明這個類的全部方法,..表明全部參數
    @Before("execution(public int www.springAOP.ComImpl.*(..))")
    public void before(JoinPoint joinPoint) {
        String name = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("方法名爲:"+name+" 參數爲:"+args);
    }

    @After("execution(public int www.springAOP.ComImpl.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("方法結束");
    }

    @AfterReturning(value = "execution(public int www.springAOP.ComImpl.*(..))", returning = "result")
    //注意上面的result必須與下面的形參名如出一轍,且必須有這個形參
    public void afterReturn(JoinPoint joinPoint, Object result) {
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法結果爲:"+result);
    }

    @AfterThrowing(value = "execution(public int www.springAOP.ComImpl.*(..))", throwing = "ex")
    public void afterThrow(JoinPoint joinPoint, Exception ex) {
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法拋出異常:"+ex);
    }
}

下面來分別解釋如下各個註解:

  • @Aspect : 聲明該類爲切面類
    @Component :將該類注入到IoC容器
    (注意:被切入的那個類,即ComImpl必須加上@Component註解⭐)
  • @Before :表示before方法執行的時機
    • execution表達式: public能夠省略,int表明返回值,不能夠省略,int能夠用*來代替,即全部返回值;
      www.springAOP.ComImpl表明須要代理的真實類的全路徑,緊跟後面的.表明這個類的全部方法,若是你要指定方法,能夠將改爲你所指定的方法名;後面的()裏面表明參數,…表明全部參數,一樣的,你要指定參數,能夠將…換成指定的參數類型;
    • execution後面的表達式就是一個範圍,表明在這個範圍內進行切入;上面的就表明ComImpl全部方法在執行前都會執行LoggerAspect中的before方法;
  • @after :同理,表示 ComImpl 全部方法執行以後會執行 LoggerAspect 類中的 after 方法;
  • @afterReturn : 表示 ComImpl 全部方法在 return 以後會執行 LoggerAspect 類中的 afterReturn 方法;
  • @afterThrowing :表示 ComImpl 全部方法在拋出異常時會執行 LoggerAspect 類中的 afterThrowing 方法;

2. XML中的配置

在applicationContext_AOP.xml中進行以下配置:

<context:component-scan base-package="www.springAOP"/>

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

第一行就不用說了,自動掃描包,使用註解的時候都必須用到這一行;

第二行則是:

  • 使Aspect註解生效,爲目標類自動生成代理對象;

    • Spring容器會結合切面類和目標類自動生成代理對象,Spring框架的底層就是經過動態代理的方式完成AOP;

    • 目標類其實就是在那些execution表達式裏;

3. 測試

public class AOPTest {
    ApplicationContext applicationContext;
    @Before
    public void testInitial() {
        applicationContext = new ClassPathXmlApplicationContext
                ("applicationContext_AOP.xml");
    }
    @Test
    public void testMethod() {
        //注意這裏返回的是代理類,這個代理類繼承了Com這個接口的
        //這裏返回的不是ComImpl,因此前面的類型只能是Com
        Com com = (Com) applicationContext.getBean("comImpl");
        com.div(2,1);
        System.out.println("---------------------------------");
        com.add(2,5);
        System.out.println("---------------------------------");
        com.div(2,0);
    }
}
方法名爲:div 參數爲:[2, 1]
方法結束
div方法結果爲:2
---------------------------------
方法名爲:add 參數爲:[2, 5]
方法結束
add方法結果爲:7
---------------------------------
方法名爲:div 參數爲:[2, 0]
方法結束
div方法拋出異常:java.lang.ArithmeticException: / by zero

java.lang.ArithmeticException: / by zero
。。。。。。(報錯信息一大堆)

注意:

  • 可能會疑問comImpl咱們都沒有在xml中配置它,怎麼能getBean獲取?
    由於ComImpl使用了@Component註解,自動注入了IoC容器,並且前面咱們知道,它的默認id就是類名的第一個字母小寫後的名字;
  • 由於切面類中須要切入的是ComImpl類,因此這裏getBean獲取這個類的實例實際上是獲取它的代理類的實例; (⭐⭐)

4. AOP術語解釋

在Spring AOP中,有以下幾個術語:

1. Aspect(切面類)

  • 切面是一個模板,它定義了全部須要完成的工做,好比切入的範圍和時間,都是在切面中來完成;
  • 在Spring中,經過實現@Aspect註解來構造一個切面;
  • 上面的例子中,LoggerAspect就是一個切面類;

2. Advice(通知)

  • 定義了切面是什麼,什麼時候使用,描述了切面要完成的工做,還解決什麼時候執行這個工做的問題。
    • 其實通知簡單地說就是切面類的代碼,即非業務代碼,上面例子中就是LoggerAspect中的代碼;
  • 在切面的某個特定的鏈接點上執行的動做。其中包括了「around」、「before」和「after」等不一樣類型的通知。許多AOP框架(包括Spring)都是以攔截器作通知模型,並維護一個以鏈接點爲中心的攔截器鏈。

3. Target(目標)

  • 被橫切的對象,對應上面例子中的ComImpl類的實例化對象,將通知放入其中;

4. Proxy(代理)

  • 切面對象、通知、目標混合以後的內容,即咱們用JDK動態代理機制建立的對象;

5. Join point(鏈接點)

  • 在Java程序執行中,咱們能夠把每一個方法當作一個點,全部方法的執行就是這些點串聯的結果;而鏈接點就是目標類須要被切入的的位置,即通知要插入業務代碼的具體位置;