Spring 詳解(二)——- AOP關鍵概念以及兩種實現方式

  • 2019 年 10 月 6 日
  • 筆記

目錄


1. AOP 關鍵詞

  • target:目標類,需要被代理的類。例如:ArithmeticCalculator
  • Joinpoint(連接點):所謂連接點是指那些可能被攔截到的方法。例如:所有的方法
  • PointCut 切入點:已經被增強的連接點。例如:add()
  • advice:通知/增強,增強程式碼。例如:showRaram、showResult
  • Weaving(織入):是指把增強 advice 應用到目標對象 target 來創建新的代理對象proxy的過程.
  • proxy 代理類:通知+切入點
  • Aspect(切面)::是切入點 pointcut 和通知 advice 的結合

2. AOP 的作用

當我們為系統做參數驗證,登錄許可權驗證或者日誌操作等,為了實現程式碼復用,我們可能把日誌處理抽離成一個新的方法。但是這樣我們仍然必須手動插入這些方法,這樣的話模組之間高耦合,不利於後期的維護和功能的擴展,有了 AOP 我們可以將功能抽成一個切面,程式碼復用好,低耦合。

3. AOP 的通知類型

Spring 按照通知 Advice 在目標類方法的連接點位置,可以分為5類

  • 前置通知[Before advice]:在連接點前面執行,前置通知不會影響連接點的執行,除非此處拋出異常。
  • 正常返回通知[After returning advice]:在連接點正常執行完成後執行,如果連接點拋出異常,則不會執行。
  • 異常返回通知[After throwing advice]:在連接點拋出異常後執行。
  • 返回通知[After (finally) advice]:在連接點執行完成後執行,不管是正常執行完成,還是拋出異常,都會執行返回通知中的內容。
  • 環繞通知[Around advice]:環繞通知圍繞在連接點前後,比如一個方法調用的前後。這是最強大的通知類型,能在方法調用前後自定義一些操作。環繞通知還需要負責決定是繼續處理join point(調用ProceedingJoinPoint的proceed方法)還是中斷執行。 Spring 中使用五種通知

1. 前置通知 <aop:before method="" pointcut="" pointcut-ref=""/> method : 通知,及方法名 pointcut :切入點表達式,此表達式只能當前通知使用。 pointcut-ref : 切入點引用,可以與其他通知共享切入點。 通知方法格式:public void myBefore(JoinPoint joinPoint){ 參數1:org.aspectj.lang.JoinPoint 用於描述連接點(目標方法),獲得目標方法名等 2. 後置通知 目標方法後執行,獲得返回值 <aop:after-returning method="" pointcut-ref="" returning=""/> returning 通知方法第二個參數的名稱 通知方法格式:public void myAfterReturning(JoinPoint joinPoint,Object result){ 參數1:連接點描述 參數2:類型Object,參數名 returning="result" 配置的 3. 異常通知 目標方法發生異常後 <aop:after-throwing method="testException" throwing="e" pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/> throwing 發生的異常 通知方法格式:public Object testRound(ProceedingJoinPoint pjp){ 參數1:ProceedingJoinPoint 返回值為 reslut

4. 基於 xml 的配置方式

xml 配置文件

<context:component-scan base-package="com.anqi">      <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>  </context:component-scan>  <!--1、 創建目標類 -->  <bean id="arithmeticCalculator" class="com.anqi.testAop.ArithmeticCalculatorImpl"></bean>  <!--2、創建切面類(通知)  -->  <bean id="logAspect" class="com.anqi.testAop.MyLogger"></bean>  <aop:config>      <aop:aspect ref="logAspect">          <!-- 切入點表達式 也可以在通知內部分別設置切入點表達式 -->          <aop:pointcut expression="execution(* com.anqi.testAop.*.*(..))" id="myPointCut"/>          <!-- 配置前置通知,注意 method 的值要和 對應切面的類方法名稱相同 -->          <aop:before method="before" pointcut-ref="myPointCut" />          <aop:after method="after" pointcut-ref="myPointCut" />          <aop:after-returning method="testAfterReturn" returning="result" pointcut-ref="myPointCut"/>          <aop:after-throwing method="testException" throwing="e" pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/>          <!--<aop:around method="testRound"  pointcut-ref="myPointCut"  /> 最強大,但是一般不使用-->      </aop:aspect>  </aop:config>

目標類

public interface ArithmeticCalculator {      int add(int i, int j);      int sub(int i, int j);        int mul(int i, int j);      int div(int i, int j);  }    public class ArithmeticCalculatorImpl implements ArithmeticCalculator {      @Override      public int add(int i, int j) {          int result = i + j;          return result;      }        @Override      public int sub(int i, int j) {          int result = i - j;          return result;      }        @Override      public int mul(int i, int j) {          int result = i * j;          return result;      }        @Override      public int div(int i, int j) {          int result = i / j;          return result;      }  }

切面類

import org.aspectj.lang.JoinPoint;  import org.aspectj.lang.ProceedingJoinPoint;  import java.util.Arrays;    /**   * 創建日誌類   */  public class MyLogger {        public void before(JoinPoint joinPoint) {          System.out.println("前置通知 參數為["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");      }      public void after(JoinPoint joinPoint) {          System.out.println("後置通知 "+ joinPoint.getSignature().getName());      }        public void testException(JoinPoint joinPoint, Throwable e) {          System.out.println("拋出異常: "+ e.getMessage());      }        public void testAfterReturn(JoinPoint joinPoint, Object result) {          System.out.println("返回通知,返回值為 " + result);      }        public Object testRound(ProceedingJoinPoint pjp) {          Object result = null;          String methodName = pjp.getSignature().getName();          Object[] args = pjp.getArgs();          try {              //前置通知              System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));              //執行目標方法              result = pjp.proceed();              //返回通知              System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);            }catch(Throwable e) {              //異常通知              System.out.println("!!!異常通知 --> The Method"+methodName+" ends with"+ result);          }          //後置通知          System.out.println("!!!後置通知 --> The Method"+methodName+" ends"+ args);          return result;      }  }

測試

import org.springframework.context.ApplicationContext;  import org.springframework.context.support.ClassPathXmlApplicationContext;  public class Main {      public static void main(String[] args) {          ApplicationContext application = new ClassPathXmlApplicationContext("spring-context.xml");          ArithmeticCalculator a = application.getBean(ArithmeticCalculator.class);          int result = a.add(1,2);          System.out.println(result);          System.out.println(a.div(5,0));      }  }  /*      前置通知 參數為[1,2]      後置通知 add      返回通知,返回值為 3      3      前置通知 參數為[5,0]      後置通知 div      拋出異常: / by zero  */

5. 基於註解的配置方式

xml

<?xml version="1.0" encoding="UTF-8"?>  <beans xmlns="http://www.springframework.org/schema/beans"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xmlns:context="http://www.springframework.org/schema/context"         xmlns:aop="http://www.springframework.org/schema/aop"         xsi:schemaLocation="http://www.springframework.org/schema/beans         http://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/context         http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">        <context:component-scan base-package="com.anqi">          <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>      </context:component-scan>      <!-- 使 AspectJ 註解起作用: 自動為匹配的類生成代理對象 -->      <aop:aspectj-autoproxy/>  </beans>

目標類

public interface ArithmeticCalculator {      int add(int i, int j);      int sub(int i, int j);        int mul(int i, int j);      int div(int i, int j);  }  import org.springframework.stereotype.Service;    @Service  public class ArithmeticCalculatorImpl implements ArithmeticCalculator {      @Override      public int add(int i, int j) {          int result = i + j;          return result;      }        @Override      public int sub(int i, int j) {          int result = i - j;          return result;      }        @Override      public int mul(int i, int j) {          int result = i * j;          return result;      }        @Override      public int div(int i, int j) {          int result = i / j;          return result;      }  }

切面

import org.aspectj.lang.JoinPoint;  import org.aspectj.lang.ProceedingJoinPoint;  import org.aspectj.lang.annotation.*;  import org.springframework.stereotype.Component;  import java.util.Arrays;    /**   * 創建日誌類   */  @Aspect  @Component  public class MyLogger {        @Before("execution(* com.anqi.testAop.*.*(..))")      public void before(JoinPoint joinPoint) {          System.out.println("前置通知 參數為["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");      }      @After("execution(* com.anqi.testAop.*.*(..))")      public void after(JoinPoint joinPoint) {          System.out.println("後置通知 "+ joinPoint.getSignature().getName());      }        @AfterThrowing(value="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))", throwing = "e")      public void testException(JoinPoint joinPoint, Throwable e) {          System.out.println("拋出異常: "+ e.getMessage());      }        @AfterReturning(value="execution(* com.anqi.testAop.*.*(..))", returning = "result")      public void testAfterReturn(JoinPoint joinPoint, Object result) {          System.out.println("返回通知,返回值為 " + result);      }        @Around("execution(* com.anqi.testAop.*.*(..))")      public Object testRound(ProceedingJoinPoint pjp) {          Object result = null;          String methodName = pjp.getSignature().getName();          Object[] args = pjp.getArgs();          try {              //前置通知              System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));              //執行目標方法              result = pjp.proceed();              //返回通知              System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);            }catch(Throwable e) {              //異常通知              System.out.println("!!!異常通知 --> The Method"+methodName+" ends with"+ result);          }          //後置通知          System.out.println("!!!後置通知 --> The Method"+methodName+" ends"+ args);          return result;      }  }

輸出結果與第一種方式一致,這裡就不再贅述了。

6. 切面的優先順序

可以使用@Order來指定切面的優先順序

//參數驗證切面  @Order(1)  @Aspect  @Component  public class ValidateAspect {    @Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")  public void validateArgs(JoinPoint join) {      String methodName = join.getSignature().getName();      Object[] args = join.getArgs();      System.out.println("validate"+methodName+Arrays.asList(args));      }  }    //把這個類聲明為一個切面:需要把該類放入到 IOC 容器中, 再聲明為一個切面  @Order(2)  @Aspect  @Component  public class LoggingAspect2 {    /**   * 聲明該方法是一個前置通知: 在目標方法開始之前執行   * @param join   */  @Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")  public void beforeMehod(JoinPoint join) {      String methodName = join.getSignature().getName();      List<Object> args = Arrays.asList(join.getArgs());      System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);      }  }

7. 重用切點表達式

//把這個類聲明為一個切面:需要把該類放入到 IOC 容器中, 再聲明為一個切面  @Order(2)  @Aspect  @Component  public class LoggingAspect {        /**       * 定義一個方法, 用於聲明切入點表達式, 一般地, 該方法中再不需要填入其他程式碼       */      @Pointcut("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")      public void declareJointPointExpression() {}          /**       * 聲明該方法是一個前置通知: 在目標方法開始之前執行       * @param join       */      @Before("declareJointPointExpression()")      public void beforeMehod(JoinPoint join) {          String methodName = join.getSignature().getName();          List<Object> args = Arrays.asList(join.getArgs());          System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);      }  }

8. 兩種方式的比較(摘自 spring 官方文檔)

如果您選擇使用Spring AOP,則可以選擇@AspectJ或XML樣式。需要考慮各種權衡。

XML樣式可能是現有Spring用戶最熟悉的,並且由真正的POJO支援。當使用AOP作為配置企業服務的工具時,XML可能是一個不錯的選擇(一個好的測試是你是否認為切入點表達式是你可能想要獨立改變的配置的一部分)。使用XML樣式,從您的配置可以更清楚地了解系統中存在哪些方面。

XML風格有兩個缺點。首先,它沒有完全封裝它在一個地方解決的要求的實現。DRY原則規定,系統中的任何知識都應該有單一,明確,權威的表示。使用XML樣式時,有關如何實現需求的知識將分支到支援bean類的聲明和配置文件中的XML。使用@AspectJ樣式時,此資訊封裝在單個模組中:方面。其次,XML樣式在它所表達的內容方面比@AspectJ樣式稍微受限:僅支援「單例」方面實例化模型,並且不可能組合在XML中聲明的命名切入點。例如,

@Pointcut("execution(* get*())")  public void propertyAccess() {}    @Pointcut("execution(org.xyz.Account+ *(..))")  public void operationReturningAnAccount() {}    @Pointcut("propertyAccess() && operationReturningAnAccount()")  public void accountPropertyAccess() {}

在XML樣式中,您可以聲明前兩個切入點:

<aop:pointcut id="propertyAccess"          expression="execution(* get*())"/>    <aop:pointcut id="operationReturningAnAccount"          expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺點是您無法 accountPropertyAccess通過組合這些定義來定義切入點。

@AspectJ 樣式支援額外的實例化模型和更豐富的切入點組合。它具有將方面保持為模組化單元的優點。它還具有以下優點:Spring AOP 和 AspectJ 都可以理解(並因此消耗)@AspectJ 方面。因此,如果您以後決定需要 AspectJ 的功能來實現其他要求,則可以輕鬆遷移到基於 AspectJ 的方法。總而言之,只要您的方面不僅僅是簡單的企業服務配置,Spring 團隊更喜歡 @AspectJ 風格。