SpringAOP的使用

懵懂小虎發表於2022-01-12

1 什麼是AOP

AOP(Aspect Orient Programming),直譯過來就是面向切面程式設計。AOP是一種程式設計思想,是物件導向(OOP)的一種補充和完善。對於物件導向來說是將程式抽象成各個層次的物件,而面向切面則是將程式抽象成各個切面。通俗一點來講就是面向切面就是將物件導向的一些通用性的功能或非核心功能單獨抽取出來。

2 為什麼要使用AOP

在聊這個問題之前,我們先看下假如在專案中有如下需求:

  • 記錄每個方法的入參及出參
  • 記錄每個方法的耗時
  • 檢查某些方法的入參是否合法

真實場景可能遠不止這些,使用物件導向來處理的話,我們是否要在每個方法中都要加相同的邏輯才能達到這樣的效果,隨著專案業務增大,後期維護起來可以用災難來形容。那麼如果使用AOP來實現上面的需求,我們只需要在想監控的方法定義切面類即可,這樣重複性的工作切不是核心業務就可以與我們的核心業務分離開來,後期維護起來只維護切面類即可。

3 AOP的前置知識

瞭解過AOP的實現,都知道底層使用的是動態代理,在Spring中,使用了兩種動態代理方式:

  • JDK動態代理
  • CGLIB動態代理

3.1 JDK動態代理

動態代理是相對於靜態代理而提出的設計模式,對於靜態代理,一個代理類只能代理一個物件,如果有多個物件需要代理,那麼就需要有多個代理類,造成程式碼的冗餘,程式碼維護性也差,JDK動態代理,從字面意思可以看出,JDK動態代理的物件是動態生成的。那麼要實現JDK動態代理必須有個前提條件,就是被代理的物件必須實現介面。既然說到靜態代理,那麼下面通過一個簡單的例子看下靜態代理的缺點。

實現靜態代理步驟:

  1. 建立UserService介面
public interface UserService {
    String getUserName(String id);
}
  1. 建立UserServiceImpl類並實現UserService介面
public class UserServiceImpl implements UserService {
    @Override
    public String getUserName(String id) {
        System.out.println("入參使用者ID:"+id);
        return "小鵬";
    }
}
  1. 建立UserServiceProxy代理類並實現UserService介面並將UserService的介面傳入到代理類
public class UserServiceProxy implements UserService {
    private UserService userService;

    public UserServiceProxy(UserService userService){
        this.userService = userService;
    }

    @Override
    public String getUserName(String id) {
        System.out.println("靜態代理開始------------");
        String result = this.userService.getUserName(id);
        System.out.println("靜態代理結束------------獲取被代理的結果:"+result);
        return result+"【代理】";
    }
}
  1. 測試案例
UserServiceProxy userServiceProxy = new UserServiceProxy(new UserServiceImpl());
System.out.println("被代理類處理後的結果:"+userServiceProxy.getUserName("1"));
---------------
測試結果:
靜態代理開始------------
入參使用者ID:1
靜態代理結束------------獲取被代理的結果:小鵬
被代理類處理後的結果:小鵬【代理】

從上面程式碼可以看出,如果有多個介面實現,想要代理,那麼就需要寫相對應的代理類,後期介面方法增加或修改,實現類修改那是無可厚非的,但是靜態代理對應的代理類也要一起修改。使用JDK動態代理來改造上面的案例,實現步驟:

  1. 建立UserService介面
  2. 建立UserServiceImpl類並實現UserService介面
  3. 建立JdkAutoProxy
    第一、二步參考上面案例。使用JDK動態代理,必須實現InvocationHandler介面。
public class JdkAutoProxy implements InvocationHandler {
    private Object target;

    public JdkAutoProxy(Object target) {
        this.target = target;
    }

    public Object getNewInstance() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK動態代理開始-------------");
        Object result = method.invoke(target, args);
        System.out.println("JDK動態代理結束-------------,入參:" + JSON.toJSONString(args));
        return result + "-JDK動態代理";
    }
}
或者這樣寫
public class JdkAutoProxy {
    private Object target;

    public JdkAutoProxy(Object target) {
        this.target = target;
    }

    public Object getProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> {
            System.out.println("JDK動態代理開始-------------");
            Object result = method.invoke(target, args);
            System.out.println("JDK動態代理結束-------------,入參:" + JSON.toJSONString(args));
            return result + "-JDK動態代理";
        });
    }
}

JDK動態代理的newProxyInstance有三個引數:

  • loader:用哪個類載入器去載入代理物件
  • interfaces:動態代理類需要實現的介面,從這裡就看出JDK動態代理的類必須有實現介面
  • h:動態代理方法在執行是,會呼叫h裡面的invoke方法去執行
  1. 測試案例
JdkAutoProxy jdkAutoProxy = new JdkAutoProxy(new UserServiceImpl());
UserService userService = (UserService)jdkAutoProxy.getProxy();
System.out.println(userService.getUserName("1"));
-----------
測試結果:
JDK動態代理開始-------------
入參使用者ID:1
JDK動態代理結束-------------,入參:["1"]
小鵬-JDK動態代理

通過上面動態代理的案例可以得出結論,代理類只建立一個就行,其他需要被代理的類,傳入代理類就行。

3.2 CGLIB動態代理

從上面的JDK動態代理的實現可以發現,JDK動態代理有一個缺點,就是被代理的類必須實現介面。在實際開發過程這顯然是不滿足需要,沒有實現介面的類想被代理怎麼辦呢?接下來就是CGLIB發揮作用了。

由於CGLIB不是JAVA自帶功能,需要引入第三方jar

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.11</version>
</dependency>

實現步驟如下:

  1. 建立UserBaseService
public class UserBaseService {
    public String getUserName(String id){
        return "小鵬";
    }
}
  1. 建立CGLIBProxy代理類,並實現MethodInterceptor介面
public class CGLIBProxy implements MethodInterceptor {
    private Object target;

    public CGLIBProxy(Object target) {
        this.target = target;
    }

    public Object getProxy() {
        Enhancer enhancer = new Enhancer();
        //設定父類
        enhancer.setSuperclass(target.getClass());
        //設定回撥
        enhancer.setCallback(this);
        //建立物件
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLIB動態代理開始--------------");
        System.out.println(String.format("呼叫類:%s,呼叫方法:%s,入參資訊:%s", o.getClass().getName(), method.getName(), JSON.toJSONString(objects)));
        Object result = methodProxy.invoke(this.target, objects);
        System.out.println(String.format("呼叫結果:%s", result));
        return result + "【CGLIB】";
    }
}
  1. 測試
CGLIBProxy cglibProxy = new CGLIBProxy(new UserBaseService());
UserBaseService userBaseService = (UserBaseService) cglibProxy.getProxy();
System.out.println(String.format("被代理類處理後的結果:%s" ,userBaseService.getUserName("1")));
-------------
測試結果:
CGLIB動態代理開始--------------
呼叫類:com.tenghu.sa.service.UserBaseService$$EnhancerByCGLIB$$a0441b92,呼叫方法:getUserName,入參資訊:["1"]
呼叫結果:小鵬
被代理類處理後的結果:小鵬【CGLIB】

JDK動態代理與CGLIB動態代理,通過案例可以看出區別了,可以試一下使用CGLIB動態代理將實現了介面的類傳入看下會不會執行成功。接下來我們來看下SpringAOP怎麼使用。

4 Spring AOP

4.1 相關概念

  • 橫切關注點:一些具有橫切多個不同軟體模組的行為,通過傳統的軟體開發方法不能夠有效地實現模組化一類特殊關注點。橫切關注點可以對某些方法進行攔截,攔截後對原方法進行增強處理。
  • 切面(Aspect):切面就是對橫切關注點的抽象,這個關注點可以橫切多個物件,在程式碼中用一個類來表示。
  • 連線點(JoinPoint):連線點是在程式執行過程中某個特定的點,比如某個方法呼叫的時候或處理異常的時候,由於Spring只支援方法型別的連線點,所以在Spring AOP中的一個連線點表示一個方法的執行。
  • 切入點(Pointcut):切入點是匹配連線點的攔截規則,在滿足這個切入點的連線點上執行通知。切入點的表示式如何與連線點相匹配是AOP的核心,Spring預設使用AspectJ切入點語法。
  • 通知(Advice):在切面上攔截到某個特定的連線點之後執行的操作
  • 目標物件(Target Object):目標物件,被一個或多個切面所通知的物件,及業務中需要進行增強的業務物件。
  • 織入(Weaving):織入是把切面作用到目標物件,然後產生一個代理物件的過程。
  • 引入(Introduction):引入是用來在執行時給一個類宣告額外的方法或屬性,即不需為實現一個介面,就能使用介面中的方法。

SpringAOP有以下通知型別:

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

Spring中使用AOP可以通過XML配置的方式,也可以通過註解的方式,下面通過簡單的案例分別使用。下面的案例是Spring整合了AspectJ,因此需要引入AspectJjar

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.3.9</version>
</dependency>

Spring的這個包就包含了AspectJ,因此直接引入即可,不需要額外的引入,另外還需要引入Spring的相關包。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.3.9</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.3.9</version>
</dependency>

4.2 基於XML配置方式

準備介面及實現類參考上面案例即可,建立一個LogProxy代理類

public class LogProxy {
    public void before(JoinPoint joinPoint){
        System.out.println("1、在連線點前面執行,前置通知不會影響連線點的執行,除非此處丟擲異常。入參資訊: "+ JSON.toJSONString(joinPoint.getArgs()));
    }

    public void after(JoinPoint joinPoint){
        System.out.println("4、在連線點執行完成後執行,不管是正常執行完成,還是丟擲異常,都會執行返回通知中的內容。 。入參資訊: "+ JSON.toJSONString(joinPoint.getArgs()));
    }

    public void afterReturn(JoinPoint joinPoint,String result){
        System.out.println("3、在連線點正常執行完成後執行,如果連線點丟擲異常,則不會執行,入參資訊:"+JSON.toJSONString(joinPoint.getArgs())+" 返回結果:"+result);
    }

    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("2、環繞增強開始-----------入參:"+JSON.toJSONString(proceedingJoinPoint.getArgs()));
        Object result = proceedingJoinPoint.proceed();
        System.out.println("環繞增強結束-----------");
        return result+"環繞增強後";
    }

    public void afterThrow(JoinPoint joinPoint,Throwable throwable){
        System.out.println("在連線點丟擲異常後執行,入參資訊:"+JSON.toJSONString(joinPoint.getArgs())+":異常資訊:"+throwable.getLocalizedMessage());
    }
}

準備Spring的配置檔案

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.tenghu.sa"/>
    <bean id="userService" class="com.tenghu.sa.service.impl.UserServiceImpl"/>
    <bean id="userBaseService" class="com.tenghu.sa.service.UserBaseService"/>
    <bean id="logProxy" class="com.tenghu.sa.proxy.LogProxy"/>
    <aop:config proxy-target-class="true">
        <aop:aspect id="logAspect" ref="logProxy">
            <aop:pointcut id="log" expression="execution(* com.tenghu.sa.service.*.*(..))"/>
            <aop:before method="before" pointcut-ref="log"/>
            <aop:after method="after" pointcut-ref="log"/>
            <aop:after-returning method="afterReturn" pointcut-ref="log" returning="result"/>
            <aop:after-throwing method="afterThrow" pointcut-ref="log" throwing="throwable"/>
            <aop:around method="around" pointcut-ref="log"/>
        </aop:aspect>
    </aop:config>
</beans>

配置檔案中的bean配置,可以使用註解的方式,在UserServiceImplUserBaseService類使用@Service註解,在代理類LogProxy上使用@Component註解。
測試案例

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-aop.xml");
UserService userService = applicationContext.getBean("userService",UserService.class);
System.out.println(userService.getUserName("12"));
-------------
測試結果:
1、在連線點前面執行,前置通知不會影響連線點的執行,除非此處丟擲異常。入參資訊: ["12"]
2、環繞增強開始-----------入參:["12"]
入參使用者ID:12
環繞增強結束-----------
3、在連線點正常執行完成後執行,如果連線點丟擲異常,則不會執行,入參資訊:["12"] 返回結果:小鵬環繞增強後
4、在連線點執行完成後執行,不管是正常執行完成,還是丟擲異常,都會執行返回通知中的內容。 。入參資訊: ["12"]
小鵬環繞增強後

從結果上面可以看出,SpringAOP已經正常輸出日誌,日誌資訊包含了入參及出參資訊,如果我們被代理的方法報異常,則會走到afterThrow方法。

4.3 基於註解的方式

對上面的代理類進行改造

@Aspect
@Component
public class LogProxy {
    @Pointcut(value = "execution(* com.tenghu.sa.service.*.*(..))")
    public void log() {

    }

    @Before(value = "log()")
    public void before(JoinPoint joinPoint) {
        System.out.println("1、在連線點前面執行,前置通知不會影響連線點的執行,除非此處丟擲異常。入參資訊: " + JSON.toJSONString(joinPoint.getArgs()));
    }

    @After(value = "log()")
    public void after(JoinPoint joinPoint) {
        System.out.println("4、在連線點執行完成後執行,不管是正常執行完成,還是丟擲異常,都會執行返回通知中的內容。 。入參資訊: " + JSON.toJSONString(joinPoint.getArgs()));
    }

    @AfterReturning(value = "log()", returning = "result")
    public void afterReturn(JoinPoint joinPoint, String result) {
        System.out.println("3、在連線點正常執行完成後執行,如果連線點丟擲異常,則不會執行,入參資訊:" + JSON.toJSONString(joinPoint.getArgs()) + " 返回結果:" + result);
    }

    @Around(value = "log()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("2、環繞增強開始-----------入參:" + JSON.toJSONString(proceedingJoinPoint.getArgs()));
        Object result = proceedingJoinPoint.proceed();
        System.out.println("環繞增強結束-----------");
        return result + "環繞增強後";
    }

    @AfterThrowing(value = "log()", throwing = "throwable")
    public void afterThrow(JoinPoint joinPoint, Throwable throwable) {
        System.out.println("在連線點丟擲異常後執行,入參資訊:" + JSON.toJSONString(joinPoint.getArgs()) + ":異常資訊:" + throwable.getLocalizedMessage());
    }
}

XML的配置就簡單了,配置將AOP配置修改為<aop:aspectj-autoproxy/>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.tenghu.sa"/>
    <aop:aspectj-autoproxy/>
</beans>

不出異常的情況下,執行結果與XML的方式一致,這裡就不單獨貼出來了。從上面案例中使用到了切入點的註解@PointcutXML中配置的aop:pointcut,裡面使用到了execution表示式,該表示式的作用就是Spring啟動時會根據表示式配置的規則掃描那些類下面的方法符合規則,將其對應的類存入到Spring的代理工廠中。execution表示式各個部分含義說明:

execution(<修飾符模式>?<返回型別模式><方法名模式>(<引數模式>)<異常模式>?)

其中,除了返回型別模式、方法名模式和引數模式外,其他項都是可選的。以上面案例中的表示式為例execution(* com.tenghu.sa.service.*.*(..)),其中含義是匹配com.tenghu.sa.service這個package下的任意類的任意方法名、任意方法入參和任意方法返回值的這部分方法。除了execution表示式,還支援其他的表示式。

4.4 切入點表示式

Spring AOP 支援以下 AspectJ 切入點指示符 (PCD),用於切入點表示式:

  • execution:用於匹配方法執行連線點。這是使用 Spring AOP 時要使用的主要切入點指示符。
  • within:限制匹配以連線某些型別中的點(使用Spring AOP時在匹配型別中宣告的方法的執行)。
  • this:限制匹配到連線點(使用Spring AOP時方法的執行),其中Bean引用(Spring AOP代理)是給定型別的例項。
  • target:限制匹配到連線點(使用Spring AOP時方法的執行),其中目標物件(正在代理的應用程式物件)是給定型別的例項。
  • args:限制匹配到連線點(使用Spring AOP時方法的執行),其中引數是給定型別的例項。
  • @target:限制匹配到連線點(使用Spring AOP時方法的執行),其中執行物件的類具有給定型別的註釋。
  • @args:限制匹配到連線點(使用Spring AOP時方法的執行),其中傳遞的實際引數的執行時型別具有給定型別的註釋。
  • @within:限制匹配以連線具有給定註釋的型別中的點(使用Spring AOP時在具有給定註釋的型別中宣告的方法的執行)。
  • @annotation:限制匹配到連線點的主題(在Spring AOP中執行的方法)具有給定註釋的連線點。

下面通過簡單的例子來說明表示式的用法:

  • execution:使用execution(方法表示式)匹配方法執行。
表示式 描述
execution(public * *(..)) 匹配任意類的公用方法
execution(* set*(..)) 匹配以set開頭的任意方法
execution(* com.tenghu.sa.service.UserService.*(..)) 匹配UserService下的任意方法
  • within:使用within(型別表示式)用於匹配指定的類的任何方法。

注意:within只能指定類,然後該類內的所有方法都將被匹配

表示式 描述
within(com.tenghu..*) cn.javass包及子包下的任何類的任何方法執行
within(com.tenghu..UserService+) cn.javass包或所有子包下IPointcutService型別及子型別的任何方法
within(@com.tenghu..Secure *) 持有cn.javass..Secure註解的任何型別的任何方法,必須是在目標物件上宣告這個註解,在介面上宣告的對它不起作用
  • this:使用this(type)
表示式 描述
this(com.tenghu.sa.service.UserService) 當前AOP物件實現了 IPointcutService介面的任何方法,也可能是引入介面
  • target:使用target(type)

type指的是一個類或者介面的完整包路徑
功能:匹配type型別的目標物件的所有方法。即目標物件可以向上轉型為type型別就算是匹配成功

表示式 描述
target(com.tenghu.sa.service.UserService) 當前目標物件(非AOP代理物件)實現了 IPointcutService介面的任何方法
  • args:使用args(引數型別列表)匹配當前執行的方法傳入時的引數型別為指定型別的執行方法。

注意:是匹配傳入的引數型別,不是匹配方法簽名的引數型別;引數型別列表中的引數必須是型別全限定名,萬用字元不支援;args屬於動態切入點,這種切入點開銷非常大,非特殊情況最好不要使用;

表示式 描述
args(java.lang.String,..) 任何一個以接受傳入引數型別為java.lang.String 開頭,且其後可跟任意個任意型別的引數的方法執行,args指定的引數型別是在執行時動態匹配的
  • @target:使用@target(註解型別)匹配持有指定註解的型別的目標物件。

注意:註解型別也必須是全限定型別名

表示式 描述
@target(com.tenghu.sa.annotation.Secure) 任何目標物件持有Secure註解的類方法;必須是在目標物件上宣告這個註解,在介面上宣告的對它不起作用
  • @args:使用@args(註解列表)匹配執行時傳入的引數的型別持有指定註解的方法,並且args括號內可以指定多個arg;

注意:註解型別也必須是全限定型別名;

表示式 描述
@args(com.tenghu.sa.annotation.Secure) 任何一個只接受一個引數的方法,且方法執行時傳入的引數型別持有com.tenghu.sa.annotation.Secure註解
  • @within:使用@within(註解型別)匹配所以持有指定註解型別內的方法。

注意:註解型別也必須是全限定型別名;

表示式 描述
@within(com.tenghu.sa.annotation.Secure) 任何目標物件對應的型別持有Secure註解的類方法;必須是在目標物件上宣告這個註解,在介面上宣告的對它不起作用
  • @annotation:使用@annotation(註解型別)匹配持有指定註解的方法;

注意:註解型別也必須是全限定型別名;

表示式 描述
@annotation(com.tenghu.sa.annotation.Secure) 方法上持有註解 com.tenghu.sa.annotation.Secure將被匹配

相關文章