Spring Aop 詳解一
阿新 • • 發佈:2020-10-18
>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