Spring中的AOP和AspectJ框架

Spring的面向切面編程(AOP)

1,Java動態代理Demo

  • 有這樣一個業務場景,設計一個計算器類,裏面有加減乘除四則運算,在裏面添加日誌功能,日誌記錄輸入的參數以及輸出的結果。如果不使用動態代理,我們會這樣寫這個功能。

    寫一個計算器的接口,裏面有四則運算

 public interface ArithmeticCalculator {
        //加
        int add (int i , int j);
    
        //減
        int sub (int i , int j);
    
        //除
        int div (int i , int j);
    
        //乘
        int mul (int i , int j);
    
    }
    
    實現這個接口,實現具體的計算步驟和日誌記錄
    /**
     * 計算器接口的實現
     *
     *  實現計算器的日誌功能,  日誌記錄計算的數字, 日誌記錄計算後的結果
     * @author
     * @version 1.00
     * @time 2020/3/5 0005  下午 4:43
     */
    public class ArithmeticCalculatorLoggingImpl implements ArithmeticCalculator {
        @Override
        public int add(int i, int j) {
            //打印日誌
            System.out.println("執行方法 add 計算的數字爲" + Arrays.asList(i,j));
            //計算
            int result = i + j;
            //打印日誌
            System.out.println("方法 add 計算的結果爲" + result);
            return result ;
        }
    
        @Override
        public int sub(int i, int j) {
            //打印日誌
            System.out.println("執行方法 sub 計算的數字爲" + Arrays.asList(i,j));
            //計算
            int result = i - j;
            //打印日誌
            System.out.println("方法 sub 計算的結果爲" + result);
            return result ;
        }
    
        @Override
        public int div(int i, int j) {
            //打印日誌
            System.out.println("執行方法 div 計算的數字爲" + Arrays.asList(i,j));
            //計算
            int result = i / j;
            //打印日誌
            System.out.println("方法 div 計算的結果爲" + result);
            return result ;
        }
    
        @Override
        public int mul(int i, int j) {
            //打印日誌
            System.out.println("執行方法 mul 計算的數字爲" + Arrays.asList(i, j));
            //計算
            int result = i * j;
            //打印日誌
            System.out.println("方法 mul 計算的結果爲" + result);
            return result;
        }
    
    }

我們完成了一開始制定的目標,實現了計算和日誌功能,但是這個計算器存在着很多致命的缺陷:
①代碼混亂:越來越多的非業務需求(日誌和驗證等)加入後,原有的業務方法急劇膨脹。每個方法在處理核心邏輯的同時還必須兼顧其他多個關注點。
②代碼分散: 以日誌需求爲例,只是爲了滿足這個單一需求,就不得不在多個模塊(方法)裏多次重複相同的日誌代碼。如果日誌需求發生變化,必須修改所有模塊。


- 我們可以使用動態代理實現上述功能,代理模式原理:使用一個代理將原本對象包裝起來,然後用該代理對象”取代”原始對象。任何對原始對象的調用都要通過代理。代理對象決定是否以及何時將方法調用轉到原始對象上。
  - 
  - 下面的demo爲使用動態 代理實現的日誌功能
        /**
         * jdk動態代理demo,
         * @author
         * @version 1.00
         * @time 2020/3/7 0007  下午 1:54
         */
        public class ArithmeticCalculatorProxy2 implements InvocationHandler {
        
            //被代理對象,,
            private Object target;
        
            //有參構造器  對被代理對象進行實例化
            public ArithmeticCalculatorProxy2(Object target) {
                this.target = target;
            }
        
            //invoke方法,,調用此方法,實際上是調用的被代理對象的代理方法
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //日誌
                System.out.println("計算器開始運行["+ method.getName()+"]方法,參數爲"+ Arrays.asList(args) +"." );
        
                //實際調用,獲取結果
                Object result = method.invoke(target, args);
        
                //日誌
                System.out.println("計算器計算結束,計算方法爲["+ method.getName()+"],結果爲["+ result +"]." );
                //返回結果
                return result;
            }
        
            //產生代理對象
            public static Object getProxyInstance(Object target){
                ClassLoader classLoader = target.getClass().getClassLoader();
                Class<?>[] interfaces = target.getClass().getInterfaces();
        
                //jkd自帶的生成動態代理的接口
                //三個參數  被代理對象的類加載器,被代理對象的接口,InvocationHandler接口的實現類
                Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, new ArithmeticCalculatorProxy2(target));
                //返回代理對象
                return proxyInstance;
            }
        
        }
        
        測試
        public class ArithmeticCalculatorTest {
        
        
            public static void main(String[] args) {
        
                ArithmeticCalculatorImpl arithmeticCalculator = new ArithmeticCalculatorImpl();
                ArithmeticCalculator proxyInstance =(ArithmeticCalculator)ArithmeticCalculatorProxy2.getProxyInstance(arithmeticCalculator);
                proxyInstance.add(1,2);
            }
        }
        
        
        打印臺輸出:
        計算器開始運行[add]方法,參數爲[1, 2].
        計算器計算結束,計算方法爲[add],結果爲[3].

2,AOP概念理解

  • AOP概述:
    • AOP(Aspect-Oriented Programming,面向切面編程):是一種新的方法論,是對傳
      統 OOP(Object-Oriented Programming,面向對象編程)的補充。
      面向對象 縱向繼承機制
      面向切面 橫向抽取機制
    • AOP編程操作的主要對象是切面(aspect),而切面模塊化橫切關注點(公共功能)。
    • 在應用AOP編程時,仍然需要定義公共功能,但可以明確的定義這個功能應用在哪裏,以什麼方式應用,並且不必修改受影響的類。這樣一來橫切關注點就被模塊化到特殊的類裏——這樣的類我們通常稱之爲“切面”。
    • AOP的好處:
      ① 每個事物邏輯位於一個位置,代碼不分散,便於維護和升級
      ② 業務模塊更簡潔,只包含核心業務代碼
    • AOP的底層原理是動態代理,由Spring框架生成代理對象,對原有的方法進行增強。
  • AOP中的概念:
    • 橫切關注點:從每個方法中抽取出來的同一類非核心業務。
    • 切面(Aspect):封裝橫切關注點信息的類,每個關注點體現爲一個通知方法。
    • 通知(Advice):切面必須要完成的各個具體工作。
    • 目標(Target):被通知的對象,,即被代理對象。
    • 代理(Proxy):向目標對象應用通知之後創建的代理對象。
    • 連接點(Joinpoint):橫切關注點在程序代碼中的具體體現,對應程序執行的某個特定位置。例如:類某個方法調用前、調用後、方法捕獲到異常後等。在應用程序中可以使用橫縱兩個座標來定位一個具體的連接點:縱座標:通知位於被增強方法的什麼位置,方法執行前,方法執行後,方法執行出現異常後,方法正常返回後。 橫座標:目標中的某個方法。
    • 切入點(pointCut):定位連接點的方式。每個類的方法中都包含多個連接點,所以連接點是類中客觀存在的事物。如果把連接點看作數據庫中的記錄,那麼切入點就是查詢條件——AOP可以通過切入點定位到特定的連接點。切點通過org.springframework.aop.Pointcut 接口進行描述,它使用類和方法作爲連接點的查詢條件。

3,AspectJ框架簡介

  1. AspectJ:Java社區裏最完整最流行的AOP框架。在Spring2.0以上版本中,可以使用基於AspectJ註解或基於XML配置的AOP。
  2. 如果不是maven項目,需要放入AspectJ的框架,如果是maven項目,放入依賴即可。
    1. l com.springsource.net.sf.cglib-2.2.0.jar
      l com.springsource.org.aopalliance-1.0.0.jar
      l com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
      l spring-aop-4.0.0.RELEASE.jar
      l spring-aspects-4.0.0.RELEASE.jar
      需要放入的jar包
  3. 在spring的配置文件中引入aop名稱空間。
  4. 開啓自動代理的配置,如果是springBoot項目則無需配置。
 <aop:aspectj-autoproxy>

當Spring IOC容器偵測到bean配置文件中的aop:aspectj-autoproxy元素時,會自動爲 與AspectJ切面匹配的bean創建代理。

4,用AspectJ註解聲明切面

  1. 要在Spring中聲明AspectJ切面,只需要在IOC容器中將切面聲明爲bean實例。
  2. 當在Spring IOC容器中初始化AspectJ切面之後,Spring IOC容器就會爲那些與 AspectJ切面相匹配的bean創建代理。
  3. 在AspectJ註解中,切面只是一個帶有@Aspect註解的Java類,它往往要包含很多通知。
  4. 通知是標註有某種註解的簡單的Java方法。
  5. AspectJ支持5種類型的通知註解:
    1. @Before:前置通知,在方法執行之前執行
  6. @After:後置通知,在方法執行之後執行
  7. @AfterRunning:返回通知,在方法返回結果之後執行
  8. @AfterThrowing:異常通知,在方法拋出異常之後執行
  9. @Around:環繞通知,圍繞着方法執行

5,AOP細節

5.1切入點表達式

  1. 作用:通過表達式的方式定位一個或多個具體的連接點。
  2. 語法細節:
    1. 切入點表達式的語法格式
  1`. [execution([權限修飾符] [返回值類型] [簡單類名/全類名] 方法名)([參數列表]))]`
  1. 舉例說明
表達式 	execution(* com.atguigu.spring.ArithmeticCalculator.*(..))

含義 : ArithmeticCalculator接口中聲明的所有方法。 第一個“”代表任意修飾符及任意返回值。 第二個“”代表任意方法。 “…”匹配任意數量、任意類型的參數。 若目標類、接口與該切面類在同一個包中可以省略包名。

表達式 	execution(public * ArithmeticCalculator.*(..))

含義 ArithmeticCalculator接口的所有公有方法

表達式 	execution(public double   ArithmeticCalculator.*(..))

含義 ArithmeticCalculator接口中返回double類型數值的方法

表達式 	execution(public double ArithmeticCalculator.*(double,   .	·.))

含義 第一個參數爲double類型的方法。 “…” 匹配任意數量、任意類型的參數

表達式 	execution(public double ArithmeticCalculator.*(double,   double))

含義 參數類型爲double,double類型的方法

在AspectJ中,切入點表達式可以通過 “&&”、“||”、“!”等操作符結合起來。

 表達式 	execution (* .add(int,..)) \|\|   execution( *.sub(int,..))

含義 任意類中第一個參數爲int類型的add方法或sub方法

表達式 	!execution (* *.add(int,..))      

含義 匹配不是任意類中第一個參數爲int類型的add方法

  1. 切入點表達式應用到實際的切面類中
  此例子爲一個前置通知
  
  @Aspect //標註爲一個切面類
  @Component //添加到ioc容器中
  public class CalculatorAspect {
      //前置通知,通過切入點表達式確定切入點 
      @Before("execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))")
      public void logBefore(JoinPoint joinPoint){
          //獲取輸入的參數
          Object[] args = joinPoint.getArgs();
          //獲取方法名
          String methodName = joinPoint.getSignature().getName();
          System.out.println("計算器開始執行,方法爲["+methodName+"],輸入參數:"+ Arrays.asList(args));
      }
  }

5.2 當前連接點細節

  1. 概述:切入點表達式通常都會是從宏觀上定位一組方法,和具體某個通知的註解結合起來就能夠確定對應的連接點。那麼就一個具體的連接點而言,我們可能會關心這個連接點的一些具體信息,例如:當前連接點所在方法的方法名、當前傳入的參數值等等。這些信息都封裝在JoinPoint接口的實例對象中。
  2. JointPoint接口:

5.3 通知

  1. 概述:
    1. 在具體的連接點上要執行的操作。
  2. 一個切面可以包括一個或者多個通知。
  3. 通知所使用的註解的值往往是切入點表達式。
  4. 前置通知
    1. 前置通知:在方法執行之前執行的通知
      使用@Before註解
  5. 後置通知
    1. 後置通知:後置通知是在連接點完成之後執行的,即連接點返回結果或者拋出異常的時候
      使用@After註解
  6. 返回通知
    1. 返回通知:無論連接點是正常返回還是拋出異常,後置通知都會執行。如果只想在連接點返回的時候記錄日誌,應使用返回通知代替後置通知。
    2. 使用@AfterReturning註解,在返回通知中訪問連接點的返回值
      ①在返回通知中,只要將returning屬性添加到@AfterReturning註解中,就可以訪問連接點的返回值。該屬性的值即爲用來傳入返回值的參數名稱
      ②必須在通知方法的簽名中添加一個同名參數。在運行時Spring AOP會通過這個參數傳遞返回值
      ③原始的切點表達式需要出現在pointcut屬性中
       @Aspect //標註爲一個切面類
       @Component //添加到ioc容器中
       public class CalculatorAspect {
       
           /**
            * 返回通知,,在@AfterReturning註解中,使用returning屬性指定返回值的名稱,接可以在方法中使用返回值
            * @param joinPoint
            * @param result
            */
           @AfterReturning(value = "execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))",
                   returning = "result")
           public void logReturning(JoinPoint joinPoint, Object result){
               //獲取方法名
               String methodName = joinPoint.getSignature().getName();
               System.out.println("計算器執行結束,方法爲["+methodName+"],返回結果:"+ result);
           }
       
       }
  1. 異常通知
    1. 異常通知:只在連接點拋出異常時才執行異常通知
    2. 將throwing屬性添加到@AfterThrowing註解中,也可以訪問連接點拋出的異常。Throwable是所有錯誤和異常類的頂級父類,所以在異常通知方法可以捕獲到任何錯誤和異常。
    3. 如果只對某種特殊的異常類型感興趣,可以將參數聲明爲其他異常的參數類型。然後通知就只在拋出這個類型及其子類的異常時才被執行。
      注意的是,在指定異常時,可以指定某個特定的異常類型,比如指定空指針異常,那麼發送數學異常就不會執行通知,所以,我們一般指定一個較大的異常類型(如:Exception 類型)進行異常捕獲。
  2. 環繞通知
    1. 環繞通知是所有通知類型中功能最爲強大的,能夠全面地控制連接點,甚至可以控制是否執行連接點。
    2. 對於環繞通知來說,連接點的參數類型必須是ProceedingJoinPoint。它是 JoinPoint的子接口,允許控制何時執行,是否執行連接點。
    3. 在環繞通知中需要明確調用ProceedingJoinPoint的proceed()方法來執行被代理的方法。如果忘記這樣做就會導致通知被執行了,但目標方法沒有被執行。
      ProceedingJoinPoint.proceed() 方法,相當於動態代理中的InvocationHandler接口中的invoke方法中的 method.invoke(target, args)方法。
    4. 注意:環繞通知的方法需要返回目標方法執行之後的結果,即調用 joinPoint.proceed();的返回值,否則會出現空指針異常。
      	  @Aspect //標註爲一個切面類
          @Component //添加到ioc容器中
          public class CalculatorAspect {
          
              /**
               * 環繞通知
               * @param joinPoint 的類型必須爲ProceedingJoinPoint,
               */
              @Around(value = "execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))")
              public void logAround(ProceedingJoinPoint joinPoint){
                  //獲取輸入的參數
                  Object[] args = joinPoint.getArgs();
                  //獲取方法名
                  String methodName = joinPoint.getSignature().getName();
          
                  try {
                      //前置通知
                      System.out.println("計算器開始執行,方法爲["+methodName+"],輸入參數:"+ Arrays.asList(args));
          
                      //如果目標方法有返回值,必須要返回方法執行的結果,否則會出現空指針異常 此處只是demo  沒有返回執行結果
                      Object proceed = joinPoint.proceed();
          
                      //返回通知  正確返回結果
                      System.out.println("計算器開始完成,方法爲["+methodName+"]");
          
                  } catch (Throwable throwable) {
          
                      //異常通知
                      System.out.println("計算器開始完成,方法爲["+methodName+"]");
          
                      //異常進行拋出,如果不拋出的情況下,目標方法額外的異常通知會失效,額外的異常(@AfterThrowing)
                      throw new RuntimeException("環繞通知代理異常");
                  }
          
                  //後置通知
                  System.out.println("計算器執行結束,方法爲["+methodName+"],返回結果:"+ methodName);
              }
          
          }

5.4重用切入點表達式

  • 在編寫AspectJ切面時,可以直接在通知註解中書寫切入點表達式。但同一個切點表達式可能會在多個通知中重複出現。
  • 在AspectJ切面中,可以通過@Pointcut註解將一個切入點聲明成簡單的方法。切入點的方法體通常是空的,因爲將切入點定義與應用程序邏輯混在一起是不合理的。
  • 切入點方法的訪問控制符同時也控制着這個切入點的可見性。如果切入點要在多個切面中共用,最好將它們集中在一個公共的類中。在這種情況下,它們必須被聲明爲public。在引入這個切入點時,必須將類名也包括在內。如果類沒有與這個切面放在同一個包中,還必須包含包名。
  • 其他通知可以通過方法名稱引入該切入點。

5.5 指定切面的優先級

  • 在同一個連接點上應用不止一個切面時,除非明確指定,否則它們的優先級是不確定的。
  • 切面的優先級可以通過實現Ordered接口或利用@Order註解指定。
  • 實現Ordered接口,getOrder()方法的返回值越小,優先級越高。
  • 若使用@Order註解,序號出現在註解中。
        //前置通知,通過切入點表達式確定切入點
        @Before("execution(public void com.demo.aop.ArithmeticCalculatorImpl(int, int))")
        @Order(1)	//數組越小,優先級越高
        public void logBefore(JoinPoint joinPoint){
            //獲取輸入的參數
            Object[] args = joinPoint.getArgs();
            //獲取方法名
            String methodName = joinPoint.getSignature().getName();
            System.out.println("計算器開始執行,方法爲["+methodName+"],輸入參數:"+ Arrays.asList(args));
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章