Spring原始碼系列(三)--spring-aop的基礎元件、架構和使用
阿新 • • 發佈:2020-09-15
# 簡介
前面已經講完 spring-bean( 詳見[Spring](https://www.cnblogs.com/ZhangZiSheng001/category/1776792.html) ),這篇部落格開始攻克 Spring 的另一個重要模組--spring-aop。
spring-aop 可以實現動態代理(底層是使用 JDK 動態代理或 cglib 來生成代理類),在專案中,一般被用來實現日誌、許可權、事務等的統一管理。
我將通過兩篇部落格來詳細介紹 spring-aop 的使用、原始碼等。這是第一篇部落格,主要介紹 spring-aop 的元件、架構、使用等。
# 專案環境
maven:3.6.3
作業系統:win10
JDK:8u231
Spring:5.2.6.RELEASE
# 幾個重要的元件
說到 spring-aop,我們經常會使用到**`Pointcut`、`Joinpoint`、`Advice`、`Aspect`**等等基礎元件,它們都是抽象出來的“標準”,有的來自 aopalliance,有的來自 AspectJ,也有的是 spring-aop 原創。
**想要學好 spring-aop,必須理解好這幾個基礎元件**。但是,理解它們非常難,一個原因是網上能講清楚的不多,第二個原因是這些元件本身抽象得不夠直觀(spring 官網承認了這一點)。
## AOP聯盟的元件--Joinpoint、Advice
在 spring-aop 的包中內嵌了 aopalliance 的包(aopalliance 就是一個制定 AOP 標準的聯盟、組織),這個包是 AOP 聯盟提供的一套“標準”,提供了 AOP 一些通用的元件,包的結構大致如下。
```powershell
└─org
└─aopalliance
├─aop
│ Advice.class
│ AspectException.class
│
└─intercept
ConstructorInterceptor.class
ConstructorInvocation.class
Interceptor.class
Invocation.class
Joinpoint.class
MethodInterceptor.class
MethodInvocation.class
```
完整的 aopalliance 包,除了 aop 和 intercept,還包括了 instrument 和 reflect,後面這兩個部分 spring-aop 沒有引入,這裡就不說了。
使用 UML 表示以上類的關係,如下。可以看到,這主要包含兩個部分:`Joinpoint`和`Advice`。
1. **Joinpoint**:**一個事件,包括呼叫某個方法(構造方法或成員方法)、操作某個成員屬性等**。
例如,我呼叫了`user.study()` 方法,這個事件本身就屬於一個`Joinpoint`。`Joinpoint`是一個“動態”的概念,通過它可以獲取這個事件的靜態資訊,例如當前事件對應的`AccessibleObject`(`AccessibleObject`是`Field`、`Method`、`Constructor`等的超類)。spring-aop 主要使用到`Joinpoint`的子介面`MethodInvocation`。
2. **Advice**:**對`Joinpoint`執行的某些操作**。
例如,JDK 動態代理使用的`InvocationHandler`、cglib 使用的`MethodInterceptor`,在抽象概念上可以算是`Advice`(即使它們沒有繼承`Advice`)。spring-aop 主要使用到`Advice`的子介面`MethodInterceptor`。
3. **Joinpoint 和 Advice 的關係**:
`Joinpoint`是`Advice`操作的物件,一個`Advice`可以操作多個`Joinpoint`,一個`Joinpoint`也可以被多個`Advice`操作。**在 spring-aop 裡,`Joinpoint`物件會持有一條`Advice`鏈,呼叫`Joinpoint.proceed()`將逐一執行其中的`Advice`(需要判斷是否執行),執行完`Advice`鏈`Advice`鏈,將最終執行被代理物件的方法**。
## AspectJ 的元件--Pointcut、Aspect
AspectJ 是一個非常非常強大的 AOP 工具,可以實現編譯期織入、編譯後織入和類載入時織入,並且提供了一套 AspectJ 語法(spring-aop 支援這套語法,但要額外引入 aspectjweaver 包)。spring-aop 使用到了 AspectJ 的兩個元件,`Pointcut`和`Aspect`。
其中,**`Pointcut`可以看成一個過濾器,它可以用來判斷當前`Advice`是否攔截指定`Joinpoint`**,如下圖所示。注意,不同的`Advice`也可以共用一個`Pointcut`。
`Aspect`這個沒什麼特別,就是一組`Pointcut`+`Advice`的集合。下面這段程式碼中,有兩個`Advice`,分別為`printRequest`和`printResponse`,它們共享同一個`Pointcut`,而這個類裡的`Pointcut`+`Advice`可以算是一個`Aspect`。
```java
@Aspect
public class UserServiceAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceAspect.class);
@Pointcut("execution(* cn.zzs.spring.UserService+.*(..)))")
public void genericPointCut() {
}
@Before(value = "genericPointCut()")
public void printRequest(JoinPoint joinPoint) throws InterruptedException {
//······
}
@After(value = "genericPointCut()")
public void printResponse(JoinPoint joinPoint) throws InterruptedException {
//······;
}
}
```
## spring-aop 的元件--Advisor
`Advisor`是 spring-aop 原創的一個元件,**一個`Advice`加上它對應的過濾器,就組成了一個`Advisor`**。在上面例子中,`printRequest`的`Advice`加上它的`Pointcut`,就是一個`Advisor`。而`Aspect`由多個`Advisor`組成。
注意,這裡的過濾器,可以是`Pointcut`,也可以是 spring-aop 自定義的`ClassFilter`。
# spring-aop 和 AspectJ 的關係
從 AOP 的功能完善程度來講,AspectJ 支援編譯期織入、編譯後織入和類載入時織入,並且提供了一套 AspectJ 語法,非常強大。在 AspectJ 面前,spring-aop 就是個“小弟弟”。
spring-aop 之所以和 AspectJ 產生關聯,主要是因為借鑑了 AspectJ 語法(這套語法一般使用註解實現,用於定義`Aspect`、`Pointcut`、`Advice`等),包括使用到 AspectJ 的註解以及解析語法的類。如果我們希望在 spring-aop 中使用 AspectJ 註解語法,需要額外引入 aspectjweaver 包。
# 如何使用 spring-aop
接下來展示的程式碼可能有的人看了會覺得奇怪,“怎麼和我平時用 spring-aop 不一樣呢?”。這裡先說明一點,**因為本文講的是 spring-aop,所以,我用的都是 spring-aop 的 API**,而實際專案中,由於 spring 封裝了一層又一層,導致我們感知不到 spring-aop 的存在。
通常情況下,Spring 是通過向`BeanFactory`註冊`BeanPostProcessor`(例如,`AbstractAdvisingBeanPostProcessor`)的方式對 bean 進行動態代理,原理並不複雜,相關內容可以通過 spring-bean 瞭解( [Spring原始碼系列(二)--bean元件的原始碼分析](https://www.cnblogs.com/ZhangZiSheng001/p/13196228.html) )。
接下來讓我們拋開這些“高階封裝”,看看 spring-aop 的真面目。
## spring-aop 的代理工廠
下面通過一個 UML 圖來了解下 spring-aop 的結構,如下。
![ProxyFactoryUML](https://img2020.cnblogs.com/blog/1731892/202009/1731892-20200915090711597-1505551851.png)
spring-aop 為我們提供了三種代理工廠,其中`ProxyFactory`比較普通,`AspectJProxyFactory`支援 AspectJ 語法的代理工廠,`ProxyFactoryBean`可以給 Spring IoC 管理的 bean 進行代理。
下面介紹如何使用這些代理工廠來獲得代理類。
## 使用ProxyFactory生成代理類
`ProxyFactory`的測試程式碼如下,如果指定了介面,一般會使用 JDK 動態代理,否則使用 cglib。
```java
@Test
public void test01() {
ProxyFactory proxyFactory = new ProxyFactory();
// 設定被代理的物件
proxyFactory.setTarget(new UserService());
// 設定代理介面--如果設定了介面,一般會使用JDK動態代理,否則用cglib
// proxyFactory.setInterfaces(IUserService.class);
// 新增第一個Advice
proxyFactory.addAdvice(new MethodInterceptor() {
public Object invoke(MethodInvocation invocation) throws Throwable {
TimeUnit.SECONDS.sleep(1);
LOGGER.info("列印{}方法的日誌", invocation.getMethod().getName());
// 執行下一個Advice
return invocation.proceed();
}
});
// 新增第二個Advice······
IUserService userController = (IUserService)proxyFactory.getProxy();
userController.save();
userController.delete();
userController.update();
userController.find();
}
```
執行以上方法,可以看到控制檯輸出:
```powershell
2020-09-12 16:32:02.704 [main] INFO cn.zzs.spring.ProxyFactoryTest - 列印save方法的日誌
增加使用者
2020-09-12 16:32:03.725 [main] INFO cn.zzs.spring.ProxyFactoryTest - 列印delete方法的日誌
刪除使用者
2020-09-12 16:32:04.726 [main] INFO cn.zzs.spring.ProxyFactoryTest - 列印update方法的日誌
修改使用者
2020-09-12 16:32:05.726 [main] INFO cn.zzs.spring.ProxyFactoryTest - 列印find方法的日誌
查詢使用者
```
## 使用ProxyFactoryBean生成代理類
`ProxyFactoryBean`和`ProxyFactory`差不多,區別在於`ProxyFactoryBean`的 target 是一個 bean。因為需要和 bean 打交道,所以這裡需要建立 beanFactory 以及註冊 bean。另外,我們可以設定每次生成的代理類都不同。
```java
@Test
public void test01() {
// 註冊bean
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("userService",
BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition());
ProxyFactoryBean proxyFactory = new ProxyFactoryBean();
// 設定beanFactory
proxyFactory.setBeanFactory(beanFactory);
// 設定被代理的bean
proxyFactory.setTargetName("userService");
// 新增Advice
proxyFactory.addAdvice(new MethodInterceptor() {
public Object invoke(MethodInvocation invocation) throws Throwable {
TimeUnit.SECONDS.sleep(1);
LOGGER.info("列印{}方法的日誌", invocation.getMethod().getName());
return invocation.proceed();
}
});
// 設定scope
//proxyFactory.setSingleton(true);
proxyFactory.setSingleton(false);
IUserService userController = (IUserService)proxyFactory.getObject();
userController.save();
userController.delete();
userController.update();
userController.find();
IUserService userController2 = (IUserService)proxyFactory.getObject();
System.err.println(userController == userController2);
}
```
## 使用AspectJProxyFactory生成代理類
使用`AspectJProxyFactory`要額外引入 aspectjweaver 包,如下:
```xml
org.aspectj
aspectjweaver
1.9.6
```
接下來配置一個`Aspect`,如下。這裡定義了一個`Advice`,即 printRequest 方法;定義了一個`Pointcut`,即攔截`UserService`及其子類。
```java
@Aspect
public class UserServiceAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceAspect.class);
@Pointcut("execution(* cn.zzs.spring.UserService+.*(..)))")
public void genericPointCut() {
}
@Before(value = "genericPointCut()")
public void printRequest(JoinPoint joinPoint) throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
LOGGER.info("call {}_{} with args:{}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
joinPoint.getArgs());
}
}
```
編寫生成代理類的方法,如下。`AspectJProxyFactory`會利用 AspectJ 的類來解析 Aspect,並轉換為 spring-aop 需要的`Advisor`。
```java
@Test
public void test01() {
AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
// 設定被代理物件
proxyFactory.setTarget(new UserService());
// 新增Aspect
proxyFactory.addAspect(UserServiceAspect.class);
// 提前過濾不符合Poincut的類,這樣在執行 Advice chain 的時候就不要再過濾
proxyFactory.setPreFiltered(true);
UserService userController = (UserService)proxyFactory.getProxy();
userController.save();
userController.delete();
userController.update();
userController.find();
}
```
關於 spring-aop 的元件、架構、使用等內容,就介紹到這裡,第二篇部落格再分析具體原始碼。
感謝閱讀。以上內容如有錯誤,歡迎指正。
> 相關原始碼請移步:[ spring-aop]( https://github.com/ZhangZiSheng001/spring-projects/tree/master/spring-aop )
> 本文為原創文章,轉載請附上原文出處連結:[https://www.cnblogs.com/ZhangZiSheng001/p/13671149.html](https://www.cnblogs.com/ZhangZiSheng001/p/136711