1. 程式人生 > >Spring原始碼系列(三)--spring-aop的基礎元件、架構和使用

Spring原始碼系列(三)--spring-aop的基礎元件、架構和使用

# 簡介 前面已經講完 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