4、Spring+AOP介紹與使用

Hello-zhou發表於2020-12-17

04Spring AOP介紹與使用

AOP:Aspect Oriented Programming 面向切面程式設計

OOP:Object Oriented Programming 物件導向程式設計

​ 面向切面程式設計:基於OOP基礎之上新的程式設計思想,OOP面向的主要物件是類,而AOP面向的主要物件是切面,在處理日誌、安全管理、事務管理等方面有非常重要的作用。AOP是Spring中重要的核心點,雖然IOC容器沒有依賴AOP,但是AOP提供了非常強大的功能,用來對IOC做補充。通俗點說的話就是在程式執行期間,將某段程式碼動態切入指定方法指定位置進行執行的這種程式設計方式。

1、AOP的概念

為什麼要引入AOP?

Calculator.java

package com.mashibing.inter;

public interface Calculator {

    public int add(int i,int j);

    public int sub(int i,int j);

    public int mult(int i,int j);

    public int div(int i,int j);
}

MyCalculator.java

package com.mashibing.inter;

public class MyCalculator implements Calculator {
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    public int mult(int i, int j) {
        int result = i * j;
        return result;
    }

    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}

MyTest.java

public class MyTest {
    public static void main(String[] args) throws SQLException {
        MyCalculator myCalculator = new MyCalculator();
        System.out.println(myCalculator.add(1, 2));
    }
}

​ 此程式碼非常簡單,就是基礎的javase的程式碼實現,此時如果需要新增日誌功能應該怎麼做呢,只能在每個方法中新增日誌輸出,同時如果需要修改的話會變得非常麻煩。

MyCalculator.java

package com.mashibing.inter;

public class MyCalculator implements Calculator {
    public int add(int i, int j) {
        System.out.println("add 方法開始執行,引數為:"+i+","+j);
        int result = i + j;
        System.out.println("add 方法開始完成結果為:"+result);
        return result;
    }

    public int sub(int i, int j) {
        System.out.println("sub 方法開始執行,引數為:"+i+","+j);
        int result = i - j;
        System.out.println("add 方法開始完成結果為:"+result);
        return result;
    }

    public int mult(int i, int j) {
        System.out.println("mult 方法開始執行,引數為:"+i+","+j);
        int result = i * j;
        System.out.println("add 方法開始完成結果為:"+result);
        return result;
    }

    public int div(int i, int j) {
        System.out.println("div 方法開始執行,引數為:"+i+","+j);
        int result = i / j;
        System.out.println("add 方法開始完成結果為:"+result);
        return result;
    }
}

​ 可以考慮將日誌的處理抽象出來,變成工具類來進行實現:

LogUtil.java

package com.mashibing.util;

import java.util.Arrays;

public class LogUtil {

    public static void start(Object ... objects){
        System.out.println("XXX方法開始執行,使用的引數是:"+ Arrays.asList(objects));
    }

    public static void stop(Object ... objects){
        System.out.println("XXX方法執行結束,結果是:"+ Arrays.asList(objects));
    }
}

MyCalculator.java

package com.mashibing.inter;

import com.mashibing.util.LogUtil;

public class MyCalculator implements Calculator {
    public int add(int i, int j) {
        LogUtil.start(i,j);
        int result = i + j;
        LogUtil.stop(result);
        return result;
    }

    public int sub(int i, int j) {
        LogUtil.start(i,j);
        int result = i - j;
        LogUtil.stop(result);
        return result;
    }

    public int mult(int i, int j) {
        LogUtil.start(i,j);
        int result = i * j;
        LogUtil.stop(result);
        return result;
    }

    public int div(int i, int j) {
        LogUtil.start(i,j);
        int result = i / j;
        LogUtil.stop(result);
        return result;
    }
}

​ 按照上述方式抽象之後,程式碼確實簡單很多,但是大家應該已經發現在輸出的資訊中並不包含具體的方法名稱,我們更多的是想要在程式執行過程中動態的獲取方法的名稱及引數、結果等相關資訊,此時可以通過使用動態代理的方式來進行實現。

CalculatorProxy.java

package com.mashibing.proxy;

import com.mashibing.inter.Calculator;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * 幫助Calculator生成代理物件的類
 */
public class CalculatorProxy {

    /**
     *
     *  為傳入的引數物件建立一個動態代理物件
     * @param calculator 被代理物件
     * @return
     */
    public static Calculator getProxy(final Calculator calculator){


        //被代理物件的類載入器
        ClassLoader loader = calculator.getClass().getClassLoader();
        //被代理物件的介面
        Class<?>[] interfaces = calculator.getClass().getInterfaces();
        //方法執行器,執行被代理物件的目標方法
        InvocationHandler h = new InvocationHandler() {
            /**
             *  執行目標方法
             * @param proxy 代理物件,給jdk使用,任何時候都不要操作此物件
             * @param method 當前將要執行的目標物件的方法
             * @param args 這個方法呼叫時外界傳入的引數值
             * @return
             * @throws Throwable
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //利用反射執行目標方法,目標方法執行後的返回值
//                System.out.println("這是動態代理執行的方法");
                Object result = null;
                try {
                    System.out.println(method.getName()+"方法開始執行,引數是:"+ Arrays.asList(args));
                    result = method.invoke(calculator, args);
                    System.out.println(method.getName()+"方法執行完成,結果是:"+ result);
                } catch (Exception e) {
                    System.out.println(method.getName()+"方法出現異常:"+ e.getMessage());
                } finally {
                    System.out.println(method.getName()+"方法執行結束了......");
                }
                //將結果返回回去
                return result;
            }
        };
        Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
        return (Calculator) proxy;
    }
}

​ 我們可以看到這種方式更加靈活,而且不需要在業務方法中新增額外的程式碼,這才是常用的方式。如果想追求完美的同學,還可以使用上述的日誌工具類來完善。

LogUtil.java

package com.mashibing.util;

import java.lang.reflect.Method;
import java.util.Arrays;

public class LogUtil {

    public static void start(Method method, Object ... objects){
//        System.out.println("XXX方法開始執行,使用的引數是:"+ Arrays.asList(objects));
        System.out.println(method.getName()+"方法開始執行,引數是:"+ Arrays.asList(objects));
    }

    public static void stop(Method method,Object ... objects){
//        System.out.println("XXX方法執行結束,結果是:"+ Arrays.asList(objects));
        System.out.println(method.getName()+"方法開始執行,引數是:"+ Arrays.asList(objects));

    }

    public static void logException(Method method,Exception e){
        System.out.println(method.getName()+"方法出現異常:"+ e.getMessage());
    }
    
    public static void end(Method method){
        System.out.println(method.getName()+"方法執行結束了......");
    }
}

CalculatorProxy.java

package com.mashibing.proxy;

import com.mashibing.inter.Calculator;
import com.mashibing.util.LogUtil;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * 幫助Calculator生成代理物件的類
 */
public class CalculatorProxy {

    /**
     *
     *  為傳入的引數物件建立一個動態代理物件
     * @param calculator 被代理物件
     * @return
     */
    public static Calculator getProxy(final Calculator calculator){


        //被代理物件的類載入器
        ClassLoader loader = calculator.getClass().getClassLoader();
        //被代理物件的介面
        Class<?>[] interfaces = calculator.getClass().getInterfaces();
        //方法執行器,執行被代理物件的目標方法
        InvocationHandler h = new InvocationHandler() {
            /**
             *  執行目標方法
             * @param proxy 代理物件,給jdk使用,任何時候都不要操作此物件
             * @param method 當前將要執行的目標物件的方法
             * @param args 這個方法呼叫時外界傳入的引數值
             * @return
             * @throws Throwable
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //利用反射執行目標方法,目標方法執行後的返回值
//                System.out.println("這是動態代理執行的方法");
                Object result = null;
                try {
                    LogUtil.start(method,args);
                    result = method.invoke(calculator, args);
                    LogUtil.stop(method,args);
                } catch (Exception e) {
                    LogUtil.logException(method,e);
                } finally {
                    LogUtil.end(method);
                }
                //將結果返回回去
                return result;
            }
        };
        Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
        return (Calculator) proxy;
    }
}

​ 很多同學看到上述程式碼之後可能感覺已經非常完美了,但是要說明的是,這種動態代理的實現方式呼叫的是jdk的基本實現,如果需要代理的目標物件沒有實現任何介面,那麼是無法為他建立代理物件的,這也是致命的缺陷。而在Spring中我們可以不編寫上述如此複雜的程式碼,只需要利用AOP,就能夠輕輕鬆鬆實現上述功能,當然,Spring AOP的底層實現也依賴的是動態代理。

AOP的核心概念及術語

  • 切面(Aspect): 指關注點模組化,這個關注點可能會橫切多個物件。事務管理是企業級Java應用中有關橫切關注點的例子。 在Spring AOP中,切面可以使用通用類基於模式的方式(schema-based approach)或者在普通類中以@Aspect註解(@AspectJ 註解方式)來實現。
  • 連線點(Join point): 在程式執行過程中某個特定的點,例如某個方法呼叫的時間點或者處理異常的時間點。在Spring AOP中,一個連線點總是代表一個方法的執行。
  • 通知(Advice): 在切面的某個特定的連線點上執行的動作。通知有多種型別,包括“around”, “before” and “after”等等。通知的型別將在後面的章節進行討論。 許多AOP框架,包括Spring在內,都是以攔截器做通知模型的,並維護著一個以連線點為中心的攔截器鏈。
  • 切點(Pointcut): 匹配連線點的斷言。通知和切點表示式相關聯,並在滿足這個切點的連線點上執行(例如,當執行某個特定名稱的方法時)。切點表示式如何和連線點匹配是AOP的核心:Spring預設使用AspectJ切點語義。
  • 引入(Introduction): 宣告額外的方法或者某個型別的欄位。Spring允許引入新的介面(以及一個對應的實現)到任何被通知的物件上。例如,可以使用引入來使bean實現 IsModified介面, 以便簡化快取機制(在AspectJ社群,引入也被稱為內部型別宣告(inter))。
  • 目標物件(Target object): 被一個或者多個切面所通知的物件。也被稱作被通知(advised)物件。既然Spring AOP是通過執行時代理實現的,那麼這個物件永遠是一個被代理(proxied)的物件。
  • AOP代理(AOP proxy):AOP框架建立的物件,用來實現切面契約(aspect contract)(包括通知方法執行等功能)。在Spring中,AOP代理可以是JDK動態代理或CGLIB代理。
  • 織入(Weaving): 把切面連線到其它的應用程式型別或者物件上,並建立一個被被通知的物件的過程。這個過程可以在編譯時(例如使用AspectJ編譯器)、類載入時或執行時中完成。 Spring和其他純Java AOP框架一樣,是在執行時完成織入的。

AOP的通知型別

  • 前置通知(Before advice): 在連線點之前執行但無法阻止執行流程進入連線點的通知(除非它引發異常)。
  • 後置返回通知(After returning advice):在連線點正常完成後執行的通知(例如,當方法沒有丟擲任何異常並正常返回時)。
  • 後置異常通知(After throwing advice): 在方法丟擲異常退出時執行的通知。
  • 後置通知(總會執行)(After (finally) advice): 當連線點退出的時候執行的通知(無論是正常返回還是異常退出)。
  • 環繞通知(Around Advice):環繞連線點的通知,例如方法呼叫。這是最強大的一種通知型別,。環繞通知可以在方法呼叫前後完成自定義的行為。它可以選擇是否繼續執行連線點或直接返回自定義的返回值又或丟擲異常將執行結束。

AOP的應用場景

  • 日誌管理
  • 許可權認證
  • 安全檢查
  • 事務控制

2、Spring AOP的簡單配置

​ 在上述程式碼中我們是通過動態代理的方式實現日誌功能的,但是比較麻煩,現在我們將要使用spring aop的功能實現此需求,其實通俗點說的話,就是把LogUtil的工具類換成另外一種實現方式。

1、新增pom依賴

        <!-- https://mvnrepository.com/artifact/cglib/cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>

2、編寫配置

  • 將目標類和切面類加入到IOC容器中,在對應的類上新增元件註解

    • 給LogUtil新增@Component註解

    • 給MyCalculator新增@Service註解

    • 新增自動掃描的配置

      <!--別忘了新增context名稱空間-->
      <context:component-scan base-package="com.mashibing"></context:component-scan>
      
  • 設定程式中的切面類

    • 在LogUtil.java中新增@Aspect註解
  • 設定切面類中的方法是什麼時候在哪裡執行

    package com.mashibing.util;
    
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    
    @Component
    @Aspect
    public class LogUtil {
    
        /*
        設定下面方法在什麼時候執行
            @Before:在目標方法之前執行:前置通知
            @After:在目標方法之後執行:後置通知
            @AfterReturning:在目標方法正常返回之後:返回通知
            @AfterThrowing:在目標方法丟擲異常後開始執行:異常通知
            @Around:環繞:環繞通知
    
            當編寫完註解之後還需要設定在哪些方法上執行,使用表示式
            execution(訪問修飾符  返回值型別 方法全稱)
         */
        @Before("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
        public static void start(){
    //        System.out.println("XXX方法開始執行,使用的引數是:"+ Arrays.asList(objects));
    //        System.out.println(method.getName()+"方法開始執行,引數是:"+ Arrays.asList(objects));
            System.out.println("方法開始執行,引數是:");
        }
    
        @AfterReturning("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
        public static void stop(){
    //        System.out.println("XXX方法執行結束,結果是:"+ Arrays.asList(objects));
    //        System.out.println(method.getName()+"方法執行結束,結果是:"+ Arrays.asList(objects));
            System.out.println("方法執行完成,結果是:");
    
        }
    
        @AfterThrowing("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
        public static void logException(){
    //        System.out.println(method.getName()+"方法出現異常:"+ e.getMessage());
            System.out.println("方法出現異常:");
        }
    
        @After("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
        public static void end(){
    //        System.out.println(method.getName()+"方法執行結束了......");
            System.out.println("方法執行結束了......");
        }
    }
    
  • 開啟基於註解的aop的功能

    <?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
           https://www.springframework.org/schema/aop/spring-aop.xsd
    ">
        <context:component-scan base-package="com.mashibing"></context:component-scan>
        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    </beans>
    

3、測試

MyTest.java

import com.mashibing.inter.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
        Calculator bean = context.getBean(Calculator.class);
        bean.add(1,1);
    }
}

​ spring AOP的動態代理方式是jdk自帶的方式,容器中儲存的元件是代理物件com.sun.proxy.$Proxy物件

4、通過cglib來建立代理物件

MyCalculator.java

package com.mashibing.inter;

import org.springframework.stereotype.Service;

@Service
public class MyCalculator {
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    public int mult(int i, int j) {
        int result = i * j;
        return result;
    }

    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}

MyTest.java

public class MyTest {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
        MyCalculator bean = context.getBean(MyCalculator.class);
        bean.add(1,1);
        System.out.println(bean);
        System.out.println(bean.getClass());
    }
}

​ 可以通過cglib的方式來建立代理物件,此時不需要實現任何介面,代理物件是

class com.mashibing.inter.MyCalculator$$EnhancerBySpringCGLIB$$1f93b605型別

綜上所述:在spring容器中,如果有介面,那麼會使用jdk自帶的動態代理,如果沒有介面,那麼會使用cglib的動態代理。動態代理的實現原理,後續會詳細講。

注意:

1、切入點表示式

​ 在使用表示式的時候,除了之前的寫法之外,還可以使用萬用字元的方式:

​ *:

​ 1、匹配一個或者多個字元

​ execution( public int com.mashibing.inter.My*alculator.*(int,int))

​ 2、匹配任意一個引數,

​ execution( public int com.mashibing.inter.MyCalculator.*(int,*))

​ 3、只能匹配一層路徑,如果專案路徑下有多層目錄,那麼*只能匹配一層路徑

​ 4、許可權位置不能使用*,如果想表示全部許可權,那麼不寫即可

​ execution( * com.mashibing.inter.MyCalculator.*(int,*))

​ ..:

​ 1、匹配多個引數,任意型別引數

​ execution( * com.mashibing.inter.MyCalculator.*(..))

​ 2、匹配任意多層路徑

​ execution( * com.mashibing..MyCalculator.*(..))

​ 在寫表示式的時候,可以有N多種寫法,但是有一種最偷懶和最精確的方式:

​ 最偷懶的方式:execution(* *(..)) 或者 execution(* *.*(..))

​ 最精確的方式:execution( public int com.mashibing.inter.MyCalculator.add(int,int))

​ 除此之外,在表示式中還支援 &&、||、!的方式

​ &&:兩個表示式同時

​ execution( public int com.mashibing.inter.MyCalculator.*(..)) && execution(* *.*(int,int) )

​ ||:任意滿足一個表示式即可

​ execution( public int com.mashibing.inter.MyCalculator.*(..)) && execution(* *.*(int,int) )

​ !:只要不是這個位置都可以進行切入

​ &&:兩個表示式同時

​ execution( public int com.mashibing.inter.MyCalculator.*(..))

2、通知方法的執行順序

​ 在之前的程式碼中大家一直對通知的執行順序有疑問,其實執行的結果並沒有錯,大家需要注意:

​ 1、正常執行:@Before--->@After--->@AfterReturning

​ 2、異常執行:@Before--->@After--->@AfterThrowing

3、獲取方法的詳細資訊

​ 在上面的案例中,我們並沒有獲取Method的詳細資訊,例如方法名、引數列表等資訊,想要獲取的話其實非常簡單,只需要新增JoinPoint引數即可。

LogUtil.java

package com.mashibing.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class LogUtil {

    @Before("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public static void start(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法開始執行,引數是:"+ Arrays.asList(args));
    }

    @AfterReturning("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public static void stop(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法執行完成,結果是:");

    }

    @AfterThrowing("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public static void logException(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法出現異常:");
    }

    @After("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public static void end(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法執行結束了......");
    }
}

​ 剛剛只是獲取了方法的資訊,但是如果需要獲取結果,還需要新增另外一個方法引數,並且告訴spring使用哪個引數來進行結果接收

LogUtil.java

    @AfterReturning(value = "execution( public int com.mashibing.inter.MyCalculator.*(int,int))",returning = "result")
    public static void stop(JoinPoint joinPoint,Object result){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法執行完成,結果是:"+result);

    }

​ 也可以通過相同的方式來獲取異常的資訊

LogUtil.java

    @AfterThrowing(value = "execution( public int com.mashibing.inter.MyCalculator.*(int,int))",throwing = "exception")
    public static void logException(JoinPoint joinPoint,Exception exception){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法出現異常:"+exception);
    }

4、spring對通過方法的要求

​ spring對於通知方法的要求並不是很高,你可以任意改變方法的返回值和方法的訪問修飾符,但是唯一不能修改的就是方法的引數,會出現引數繫結的錯誤,原因在於通知方法是spring利用反射呼叫的,每次方法呼叫得確定這個方法的引數的值。

LogUtil.java

    @After("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    private int end(JoinPoint joinPoint,String aa){
//        System.out.println(method.getName()+"方法執行結束了......");
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法執行結束了......");
        return 0;
    }

5、表示式的抽取

如果在實際使用過程中,多個方法的表示式是一致的話,那麼可以考慮將切入點表示式抽取出來:

​ a、隨便生命一個沒有實現的返回void的空方法

​ b、給方法上標註@Potintcut註解

package com.mashibing.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class LogUtil {
    
    @Pointcut("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public void myPoint(){}
    
    @Before("myPoint()")
    public static void start(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法開始執行,引數是:"+ Arrays.asList(args));
    }

    @AfterReturning(value = "myPoint()",returning = "result")
    public static void stop(JoinPoint joinPoint,Object result){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法執行完成,結果是:"+result);

    }

    @AfterThrowing(value = "myPoint()",throwing = "exception")
    public static void logException(JoinPoint joinPoint,Exception exception){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法出現異常:"+exception.getMessage());
    }

    @After("myPoint()")
    private int end(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法執行結束了......");
        return 0;
    }
}

6、環繞通知的使用

LogUtil.java

package com.mashibing.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class LogUtil {
    @Pointcut("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public void myPoint(){}
    
    /**
     * 環繞通知是spring中功能最強大的通知
     * @param proceedingJoinPoint
     * @return
     */
    @Around("myPoint()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        String name = proceedingJoinPoint.getSignature().getName();
        Object proceed = null;
        try {
            System.out.println("環繞前置通知:"+name+"方法開始,引數是"+Arrays.asList(args));
            //利用反射呼叫目標方法,就是method.invoke()
            proceed = proceedingJoinPoint.proceed(args);
            System.out.println("環繞返回通知:"+name+"方法返回,返回值是"+proceed);
        } catch (Throwable e) {
            System.out.println("環繞異常通知"+name+"方法出現異常,異常資訊是:"+e);
        }finally {
            System.out.println("環繞後置通知"+name+"方法結束");
        }
        return proceed;
    }
}

​ 總結:環繞通知的執行順序是優於普通通知的,具體的執行順序如下:

環繞前置-->普通前置-->目標方法執行-->環繞正常結束/出現異常-->環繞後置-->普通後置-->普通返回或者異常。

但是需要注意的是,如果出現了異常,那麼環繞通知會處理或者捕獲異常,普通異常通知是接收不到的,因此最好的方式是在環繞異常通知中向外丟擲異常。

7、多切面執行的順序

​ 如果有多個切面要進行執行,那麼順序是什麼樣的呢?

LogUtil.java

package com.mashibing.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class LogUtil {
    @Pointcut("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public void myPoint(){}
    @Before("myPoint()")
    public static void start(JoinPoint joinPoint){
//        System.out.println("XXX方法開始執行,使用的引數是:"+ Arrays.asList(objects));
//        System.out.println(method.getName()+"方法開始執行,引數是:"+ Arrays.asList(objects));
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法開始執行,引數是:"+ Arrays.asList(args));
    }

    @AfterReturning(value = "myPoint()",returning = "result")
    public static void stop(JoinPoint joinPoint,Object result){
//        System.out.println("XXX方法執行結束,結果是:"+ Arrays.asList(objects));
//        System.out.println(method.getName()+"方法執行結束,結果是:"+ Arrays.asList(objects));
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法執行完成,結果是:"+result);

    }

    @AfterThrowing(value = "myPoint()",throwing = "exception")
    public static void logException(JoinPoint joinPoint,Exception exception){
//        System.out.println(method.getName()+"方法出現異常:"+ e.getMessage());
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法出現異常:"+exception.getMessage());
    }

    @After("myPoint()")
    private int end(JoinPoint joinPoint){
//        System.out.println(method.getName()+"方法執行結束了......");
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法執行結束了......");
        return 0;
    }

    /**
     * 環繞通知是spring中功能最強大的通知
     * @param proceedingJoinPoint
     * @return
     */
    //@Around("myPoint()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        String name = proceedingJoinPoint.getSignature().getName();
        Object proceed = null;
        try {
            System.out.println("環繞前置通知:"+name+"方法開始,引數是"+Arrays.asList(args));
            //利用反射呼叫目標方法,就是method.invoke()
            proceed = proceedingJoinPoint.proceed(args);
            System.out.println("環繞返回通知:"+name+"方法返回,返回值是"+proceed);
        } catch (Throwable e) {
            System.out.println("環繞異常通知"+name+"方法出現異常,異常資訊是:"+e);
        }finally {
            System.out.println("環繞後置通知"+name+"方法結束");
        }
        return proceed;
    }
}

SecurityAspect.java

package com.mashibing.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class SecurityAspect {

    @Before("com.mashibing.util.LogUtil.myPoint()")
    public static void start(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法開始執行,引數是:"+ Arrays.asList(args));
    }

    @AfterReturning(value = "com.mashibing.util.LogUtil.myPoint()",returning = "result")
    public static void stop(JoinPoint joinPoint,Object result){
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法執行完成,結果是:"+result);

    }

    @AfterThrowing(value = "com.mashibing.util.LogUtil.myPoint()",throwing = "exception")
    public static void logException(JoinPoint joinPoint,Exception exception){
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法出現異常:"+exception.getMessage());
    }

    @After("com.mashibing.util.LogUtil.myPoint()")
    private int end(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法執行結束了......");
        return 0;
    }

    /**
     * 環繞通知是spring中功能最強大的通知
     * @param proceedingJoinPoint
     * @return
     */
    //@Around("myPoint()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        String name = proceedingJoinPoint.getSignature().getName();
        Object proceed = null;
        try {
            System.out.println("環繞前置通知:"+name+"方法開始,引數是"+Arrays.asList(args));
            //利用反射呼叫目標方法,就是method.invoke()
            proceed = proceedingJoinPoint.proceed(args);
            System.out.println("環繞返回通知:"+name+"方法返回,返回值是"+proceed);
        } catch (Throwable e) {
            System.out.println("環繞異常通知"+name+"方法出現異常,異常資訊是:"+e);
        }finally {
            System.out.println("環繞後置通知"+name+"方法結束");
        }
        return proceed;
    }
}

​ 在spring中,預設是按照切面名稱的字典順序進行執行的,但是如果想自己改變具體的執行順序的話,可以使用@Order註解來解決,數值越小,優先順序越高。

LogUtil.java

package com.mashibing.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
@Order(2)
public class LogUtil {
    @Pointcut("execution( public int com.mashibing.inter.MyCalculator.*(int,int))")
    public void myPoint(){}
    @Before("myPoint()")
    public static void start(JoinPoint joinPoint){
//        System.out.println("XXX方法開始執行,使用的引數是:"+ Arrays.asList(objects));
//        System.out.println(method.getName()+"方法開始執行,引數是:"+ Arrays.asList(objects));
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法開始執行,引數是:"+ Arrays.asList(args));
    }

    @AfterReturning(value = "myPoint()",returning = "result")
    public static void stop(JoinPoint joinPoint,Object result){
//        System.out.println("XXX方法執行結束,結果是:"+ Arrays.asList(objects));
//        System.out.println(method.getName()+"方法執行結束,結果是:"+ Arrays.asList(objects));
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法執行完成,結果是:"+result);

    }

    @AfterThrowing(value = "myPoint()",throwing = "exception")
    public static void logException(JoinPoint joinPoint,Exception exception){
//        System.out.println(method.getName()+"方法出現異常:"+ e.getMessage());
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法出現異常:"+exception.getMessage());
    }

    @After("myPoint()")
    private int end(JoinPoint joinPoint){
//        System.out.println(method.getName()+"方法執行結束了......");
        String name = joinPoint.getSignature().getName();
        System.out.println("Log:"+name+"方法執行結束了......");
        return 0;
    }

    /**
     * 環繞通知是spring中功能最強大的通知
     * @param proceedingJoinPoint
     * @return
     */
    //@Around("myPoint()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        String name = proceedingJoinPoint.getSignature().getName();
        Object proceed = null;
        try {
            System.out.println("環繞前置通知:"+name+"方法開始,引數是"+Arrays.asList(args));
            //利用反射呼叫目標方法,就是method.invoke()
            proceed = proceedingJoinPoint.proceed(args);
            System.out.println("環繞返回通知:"+name+"方法返回,返回值是"+proceed);
        } catch (Throwable e) {
            System.out.println("環繞異常通知"+name+"方法出現異常,異常資訊是:"+e);
        }finally {
            System.out.println("環繞後置通知"+name+"方法結束");
        }
        return proceed;
    }
}

SecurityAspect.java

package com.mashibing.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
@Order(1)
public class SecurityAspect {

    @Before("com.mashibing.util.LogUtil.myPoint()")
    public static void start(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法開始執行,引數是:"+ Arrays.asList(args));
    }

    @AfterReturning(value = "com.mashibing.util.LogUtil.myPoint()",returning = "result")
    public static void stop(JoinPoint joinPoint,Object result){
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法執行完成,結果是:"+result);

    }

    @AfterThrowing(value = "com.mashibing.util.LogUtil.myPoint()",throwing = "exception")
    public static void logException(JoinPoint joinPoint,Exception exception){
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法出現異常:"+exception.getMessage());
    }

    @After("com.mashibing.util.LogUtil.myPoint()")
    private int end(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println("Security:"+name+"方法執行結束了......");
        return 0;
    }

    /**
     * 環繞通知是spring中功能最強大的通知
     * @param proceedingJoinPoint
     * @return
     */
    //@Around("myPoint()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        String name = proceedingJoinPoint.getSignature().getName();
        Object proceed = null;
        try {
            System.out.println("環繞前置通知:"+name+"方法開始,引數是"+Arrays.asList(args));
            //利用反射呼叫目標方法,就是method.invoke()
            proceed = proceedingJoinPoint.proceed(args);
            System.out.println("環繞返回通知:"+name+"方法返回,返回值是"+proceed);
        } catch (Throwable e) {
            System.out.println("環繞異常通知"+name+"方法出現異常,異常資訊是:"+e);
        }finally {
            System.out.println("環繞後置通知"+name+"方法結束");
        }
        return proceed;
    }
}

​ 如果需要新增環繞通知呢,具體的執行順序又會是什麼順序呢?

​ 因為環繞通知在進行新增的時候,是在切面層引入的,所以在哪個切面新增環繞通知,那麼就會在哪個切面執行。

3、基於配置的AOP配置

​ 之前我們講解了基於註解的AOP配置方式,下面我們開始講一下基於xml的配置方式,雖然在現在的企業級開發中使用註解的方式比較多,但是你不能不會,因此需要簡單的進行配置,註解配置快速簡單,配置的方式共呢個完善。

1、將所有的註解都進行刪除

2、新增配置檔案

aop.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
       https://www.springframework.org/schema/aop/spring-aop.xsd
">

    <context:component-scan base-package="com.mashibing"></context:component-scan>
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <bean id="logUtil" class="com.mashibing.util.LogUtil2"></bean>
    <bean id="securityAspect" class="com.mashibing.util.SecurityAspect"></bean>
    <bean id="myCalculator" class="com.mashibing.inter.MyCalculator"></bean>
    <aop:config>
        <aop:pointcut id="globalPoint" expression="execution(public int com.mashibing.inter.MyCalculator.*(int,int))"/>
        <aop:aspect ref="logUtil">
            <aop:pointcut id="mypoint" expression="execution(public int com.mashibing.inter.MyCalculator.*(int,int))"/>
            <aop:before method="start" pointcut-ref="mypoint"></aop:before>
            <aop:after method="end" pointcut-ref="mypoint"></aop:after>
            <aop:after-returning method="stop" pointcut-ref="mypoint" returning="result"></aop:after-returning>
            <aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"></aop:after-throwing>
            <aop:around method="myAround" pointcut-ref="mypoint"></aop:around>
        </aop:aspect>
        <aop:aspect ref="securityAspect">
            <aop:before method="start" pointcut-ref="globalPoint"></aop:before>
            <aop:after method="end" pointcut-ref="globalPoint"></aop:after>
            <aop:after-returning method="stop" pointcut-ref="globalPoint" returning="result"></aop:after-returning>
            <aop:after-throwing method="logException" pointcut-ref="globalPoint" throwing="exception"></aop:after-throwing>
            <aop:around method="myAround" pointcut-ref="mypoint"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

相關文章