1. 程式人生 > >Spring Aop 詳解一

Spring Aop 詳解一

>Aop 是一個程式設計思想,最初是一個理論,最後落地成了很多的技術實現。 我們寫一個系統,都希望儘量少寫點兒重複的東西。而很多時候呢,又不得不寫一些重複的東西。比如`訪問某些方法的許可權`,`執行某些方法效能的日誌`,`資料庫操作的方法進行事務控制`。以上提到的,許可權的控制,事務控制,效能監控的日誌 可以叫一個切面。像一個`橫切面穿過這一些列需要控制的方法`。通過aop程式設計,實現了對切面業務的統一處理。 以上是我對aop的一個總體概括 --------------------------------------- ## aop的原始實現 通過動態代理和反射實現,又稱之為JDK動態代理 - MyInterceptor.java ```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 ```java package demo.aop.jdkproxy; public interface PersonDao { public void savePerson(); public void updatePerson(); } ``` - PersonDaoImpl.java ```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 transaction`和`commit` ## 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](https://gitee.com/haimama/java-study/tree/master/spring-aop-demo) > spring aop翻譯文件[http://shouce.jb51.net/spring/aop.html](http://shouce.jb51.net/spring/ao