Spring Aop 詳解一

夢想家haima發表於2020-10-18

Aop 是一個程式設計思想,最初是一個理論,最後落地成了很多的技術實現。

我們寫一個系統,都希望儘量少寫點兒重複的東西。而很多時候呢,又不得不寫一些重複的東西。比如訪問某些方法的許可權執行某些方法效能的日誌資料庫操作的方法進行事務控制。以上提到的,許可權的控制,事務控制,效能監控的日誌 可以叫一個切面。像一個橫切面穿過這一些列需要控制的方法。通過aop程式設計,實現了對切面業務的統一處理。

以上是我對aop的一個總體概括


aop的原始實現

通過動態代理和反射實現,又稱之為JDK動態代理

  • MyInterceptor.java
package demo.aop.jdkproxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 攔截器
 *    1、目標類匯入進來
 *    2、事務匯入進來
 *    3、invoke完成
 *        1、開啟事務
 *        2、呼叫目標物件的方法
 *        3、事務的提交
 * @author zd
 *
 */
public class MyInterceptor implements InvocationHandler{
    private Object target;//目標類
    private Transaction transaction;


    public MyInterceptor(Object target, Transaction transaction) {
        super();
        this.target = target;
        this.transaction = transaction;
    }


    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        String methodName = method.getName();
        if("savePerson".equals(methodName)||"updatePerson".equals(methodName)
                ||"deletePerson".equals(methodName)){
            this.transaction.beginTransaction();//開啟事務
            method.invoke(target);//呼叫目標方法
            this.transaction.commit();//事務的提交
        }else{
            method.invoke(target);
        }
        return null;
    }
}

  • PersonDao.java
package demo.aop.jdkproxy;

public interface PersonDao {
    public void savePerson();
    public void updatePerson();
}

  • PersonDaoImpl.java
package demo.aop.jdkproxy;

public class PersonDaoImpl implements PersonDao{
    public void savePerson() {
        System.out.println("save person");
    }

    public void updatePerson() {
        // TODO Auto-generated method stub
        System.out.println("update person");
    }
}

  • Transaction.java
package demo.aop.jdkproxy;

public class Transaction {
    public void beginTransaction(){
        System.out.println("begin transaction");
    }
    public void commit(){
        System.out.println("commit");
    }
}

  • JDKProxyTest
package demo.aop.jdkproxy;

import org.junit.Test;

import java.lang.reflect.Proxy;

/**
 * 1、攔截器的invoke方法是在時候執行的?
 *     當在客戶端,代理物件呼叫方法的時候,進入到了攔截器的invoke方法
 * 2、代理物件的方法體的內容是什麼?
 *     攔截器的invoke方法的內容就是代理物件的方法的內容
 * 3、攔截器中的invoke方法中的引數method是誰在什麼時候傳遞過來的?
 *     代理物件呼叫方法的時候,進入了攔截器中的invoke方法,所以invoke方法中的引數method就是
 *       代理物件呼叫的方法
 * @author zd
 *
 */
public class JDKProxyTest {
    @Test
    public void testJDKProxy(){
        /**
         * 1、建立一個目標物件
         * 2、建立一個事務
         * 3、建立一個攔截器
         * 4、動態產生一個代理物件
         */
        Object target = new PersonDaoImpl();
        Transaction transaction = new Transaction();
        MyInterceptor interceptor = new MyInterceptor(target, transaction);
        /**
         * 1、目標類的類載入器
         * 2、目標類實現的所有的介面
         * 3、攔截器
         */
        PersonDao personDao = (PersonDao) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), interceptor);
        //personDao.savePerson();
        personDao.updatePerson();
    }
}

執行結果

begin transaction
update person
commit

原始實現部分,想必現在很少會有人再這麼寫了。但這個對於我們理解Aop的思想很有幫助。

  • 我們可以看到 代理物件 personDao呼叫的方法updatePerson中沒有模擬事務的程式碼,但最終代理物件卻輸出了begin transactioncommit

Spring AOP概念核心詞

  • 切面(Aspect):一個關注點的模組化,這個關注點可能會橫切多個物件。事務管理是J2EE應用中一個關於橫切關注點的很好的例子。在Spring AOP中,切面可以使用基於模式)或者基於@Aspect註解的方式來實現。

  • 連線點(Joinpoint):在程式執行過程中某個特定的點,比如某方法呼叫的時候或者處理異常的時候。在Spring AOP中,一個連線點總是表示一個方法的執行。

  • 通知(Advice):在切面的某個特定的連線點上執行的動作。其中包括了“around”、“before”和“after”等不同型別的通知(通知的型別將在後面部分進行討論)。許多AOP框架(包括Spring)都是以攔截器做通知模型,並維護一個以連線點為中心的攔截器鏈。

  • 切入點(Pointcut):匹配連線點的斷言。通知和一個切入點表示式關聯,並在滿足這個切入點的連線點上執行(例如,當執行某個特定名稱的方法時)。切入點表示式如何和連線點匹配是AOP的核心:Spring預設使用AspectJ切入點語法。

  • 引入(Introduction):用來給一個型別宣告額外的方法或屬性(也被稱為連線型別宣告(inter-type declaration))。Spring允許引入新的介面(以及一個對應的實現)到任何被代理的物件。例如,你可以使用引入來使一個bean實現IsModified介面,以便簡化快取機制。

  • 目標物件(Target Object): 被一個或者多個切面所通知的物件。也被稱做被通知(advised)物件。 既然Spring AOP是通過執行時代理實現的,這個物件永遠是一個被代理(proxied)物件。

  • AOP代理(AOP Proxy):AOP框架建立的物件,用來實現切面契約(例如通知方法執行等等)。在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。

  • 織入(Weaving):把切面連線到其它的應用程式型別或者物件上,並建立一個被通知的物件。這些可以在編譯時(例如使用AspectJ編譯器),類載入時和執行時完成。Spring和其他純Java AOP框架一樣,在執行時完成織入。


上面為官方文件,有的地方還是很難讀懂,畢竟是純概念。下面我用自己的話來翻譯一下,如果有不對的地方,請指正

  • 切面 統一處理的業務,比如上文提到的 許可權控制,事務處理
  • 連線點 原本被執行的方法,一個執行的方法可能被多個切面橫切
  • 通知 切面方法的執行,比如許可權控制的具體執行過程(許可權控制可以用前置通知@Before)
  • 切入點 切入點的概念通常和連線點概念容易分不清,切入點其實是一個規則,也就是說什麼樣的情況下(滿足什麼規則),
    就會去執行連結點的那些方法,這個規則就是切入點,這種規則用切入點表示式去制定
  • 引入(Introduction) 被代理的物件可以引入新介面,通過預設的實現類,讓這個被代理的類增強
  • 目標物件 就是被切面執行了的物件
  • AOP代理 代理包括jdk代理和cglib代理,是aop底層實現過程
  • 織入 就是切面中的方法完成載入執行的過程

這裡有8個概念,但真正要完成aop的理解,還不得不再引入兩個概念。

  • 被代理物件 我們可以看到,上面說到目標物件永遠是一個被代理的物件,也是被通知的物件。
  • 代理物件 代理物件呢, 就是最後通知後,生成的物件。

切入點表示式

  • execution
    用於匹配指定型別內的方法執行,匹配的是方法,可以確切到方法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern (param-pattern)
          throws-pattern?)
          
modifiers-pattern 修飾符表示式   :public protect private ,可預設,表示不限制
ret-type-pattern    返回值表示式   如 String代表返回值為String ,*代表任意返回值都可以   必填欄位
declaring-type-pattern  型別,可以由完整包名加類名組成   可以只寫包名加.*限定包下的所有類   可預設,表示不限制
name-pattern    方法名錶達式,可以由*統配所有字元   必填欄位
param-pattern   引數列表,可以用..來表示所有的方法  必填欄位
execution(public * * (..))   //所有public的方法
execution(* set*(..))  //所有set開頭的方法
execution( * com.xyz.service.AccountService.* (..) ) //AccountService的所有方法,如果AccountService是介面,指實現了這個介面的所有方法
execution(* com.xyz.service.*.*(..)) //com.xyz.service包下的所有類的所有方法
execution(* com.xyz.service..*.*(..)) //com.xyz.service包及**子包**下的所有類的所有方法
  • within
    用於匹配指定型別內的方法執行,匹配的是型別內的方法,型別下的所有方法
within (com.xyz.service.*) // com.xyz.service包下面的所有類的所有方法
within (com.xyz.service..*) // com.xyz.service包及**子包**下面的所有類的所有方法
within (com.xyz.service.impl.UserServiceImpl) // UserServiceImpl類下面所有方法
  • this
    用於匹配當前AOP代理物件型別的執行方法,在前文中引入(Introduction)的代理物件使用,可以注入代理物件

  • bean
    指定spring容器中特定名稱的bean的所有方法為連線點

  • target
    用於匹配當前目標物件型別的執行方法,可以注入目標物件,被代理的物件

  • args
    用於匹配當前執行的方法傳入的引數為指定型別的執行方法,可以注入連線點(方法)的引數列表

  • @target 暫未解讀

  • @args 暫未解讀

  • @within 暫未解讀

  • @annotation 暫未解讀

程式碼實戰

5種通知的案例

  • DemoAspect.java
    定義切面,及通知,這裡為了測試更多的案例,表示式切到AdviceKindTestController.java

package demo.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Slf4j
@Component //必須是個bean
public class DemoAspect {

    //前置通知
    @Before("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public void auth() {
        log.info("前置通知,假裝校驗了個許可權");
    }

    //後置通知
    @AfterReturning("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public  Object  afterSomething(){
        log.info("後置通知,不太清楚運用場景");
        return "ok";
    }

    //環繞通知
    //如果環繞通知 不返回執行結果  方法不會返回任何結果,導致介面拿不到任何資料
    //所以一定把proceed 返回
    //ProceedingJoinPoint 是 JoinPoint的子類,僅當環繞通知的時候,可以注入ProceedingJoinPoint的連線點
    @Around("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public  Object  getMethodTime(ProceedingJoinPoint point) throws Throwable {
        log.info("環繞通知,統計方法耗時,方法執行前");
        Long beforeMillis = System.currentTimeMillis();
        Object proceed = point.proceed();
        Long taketimes= System.currentTimeMillis()-beforeMillis;
        log.info(String.format("該方法用時%s毫秒",taketimes));
        return proceed;
    }

    //異常通知
    @AfterThrowing("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public void throwSomething() {
        log.info("異常通知,只有異常了才會通知。具體場景,不是特別瞭解");
    }

    //最終通知
    @After("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
    public void closeSomething() {
        log.info("最終通知,官網說,可以用來回收某些資源。無論發不發生異常,都會被執行");
    }

}


  • AdviceKindTestController.java

測試用的介面類

package demo.aop.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AdviceKindTestController {

    @GetMapping("/advice") //http://localhost:8080/advice
    public String test() throws InterruptedException {
        Thread.sleep(4);
        return "ok";
    }


    @GetMapping("/advice/throwing") //http://localhost:8080/advice/throwing
    public String test2(){
        int i=1/0;
        return "ok";
    }
}

訪問 http://localhost:8080/advice 後臺輸出為

2020-10-18 11:24:01.201                 : 環繞通知,統計方法耗時,方法執行前
2020-10-18 11:24:01.201                 : 前置通知,假裝校驗了個許可權
2020-10-18 11:24:01.201                 : 該方法用時6毫秒
2020-10-18 11:24:01.202                 : 最終通知,官網說,可以用來回收某些資源。無論發不發生異常,都會被執行
2020-10-18 11:24:01.202                 : 後置通知,不太清楚運用場景

執行順序

- 環繞通知的前面部分
- 前置通知
- 環繞通知的後面部分
- 最終通知
- 後置通知

訪問 http://localhost:8080/advice/throwing 後臺輸出為

2020-10-18 11:30:34.935                 : 環繞通知,統計方法耗時,方法執行前
2020-10-18 11:30:34.935                 : 前置通知,假裝校驗了個許可權
2020-10-18 11:30:34.936                 : 最終通知,官網說,可以用來回收某些資源。無論發不發生異常,都會被執行
2020-10-18 11:30:34.936                 : 異常通知,只有異常了才會通知。具體場景,不是特別瞭解
java.lang.ArithmeticException: / by zero    //test2()方法丟擲了異常

執行順序如下,我們可以看到因為介面出現了異常,所以後置通知並沒執行,環繞通知的後面部分也沒執行,但最終通知異常通知被執行

- 環繞通知的前面部分
- 前置通知
- 最終通知
- 異常通知

下文預告

  • 切入點表示式詳解
  • 通知優先順序
  • 通知中引用連線點的引數,目標物件,代理物件、連線點(org.aspectj.lang.JoinPoint)物件及其方法呼叫
  • @DeclareParents 實現引入
  • @ControllerAdvice 實現統一錯誤處理

本文完整程式碼參考 https://gitee.com/haimama/java-study/tree/master/spring-aop-demo
spring aop翻譯文件http://shouce.jb51.net/spring/aop.html

相關文章