Spring Aop 詳解一
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 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)
的代理物件使用,可以注入代理物件 -
target
用於匹配當前目標物件型別的執行方法,可以注入目標物件,被代理的物件 -
args
用於匹配當前執行的方法傳入的引數為指定型別的執行方法,可以注入連線點(方法)的引數列表 -
@target
-
@args
-
@within
-
@annotation
-
bean
程式碼實戰
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()方法丟擲了異常
執行順序如下,我們可以看到因為接口出現了異常,所以後置通知並沒執行,環繞通知的後面部分也沒執行,但最終通知
和異常通知
被執行
- 環繞通知的前面部分
- 前置通知
- 最終通知
- 異常通知
下文預告
- 切入點表示式詳解
- 通知優先順序
- 通知中引用切點的引數,目標物件,代理物件、切點物件及其方法呼叫
- @DeclareParents 實現引入
- @ControllerAdvice 實現統一錯誤處理
本文完整程式碼參考 https://gitee.com/haimama/java-study/tree/master/spring-aop
spring aop翻譯文件http://shouce.jb51.net/spring/aop.html