SpringAOP的使用

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="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xmlns:aop="//www.springframework.org/schema/aop"
       xmlns:context="//www.springframework.org/schema/context"
       xsi:schemaLocation="//www.springframework.org/schema/beans
       //www.springframework.org/schema/beans/spring-beans.xsd
       //www.springframework.org/schema/aop
       //www.springframework.org/schema/aop/spring-aop.xsd
       //www.springframework.org/schema/context
       //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="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xmlns:aop="//www.springframework.org/schema/aop"
       xmlns:context="//www.springframework.org/schema/context"
       xsi:schemaLocation="//www.springframework.org/schema/beans
       //www.springframework.org/schema/beans/spring-beans.xsd
       //www.springframework.org/schema/aop
       //www.springframework.org/schema/aop/spring-aop.xsd
       //www.springframework.org/schema/context
       //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將被匹配
Tags: