簡介
前面已經講完 spring-bean( 詳見Spring ),這篇部落格開始攻克 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 一些通用的元件,包的結構大致如下。
└─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
。
- Joinpoint:一個事件,包括呼叫某個方法(構造方法或成員方法)、操作某個成員屬性等。
例如,我呼叫了user.study()
方法,這個事件本身就屬於一個Joinpoint
。Joinpoint
是一個“動態”的概念,通過它可以獲取這個事件的靜態資訊,例如當前事件對應的AccessibleObject
(AccessibleObject
是Field
、Method
、Constructor
等的超類)。spring-aop 主要使用到Joinpoint
的子介面MethodInvocation
。
- Advice:對
Joinpoint
執行的某些操作。
例如,JDK 動態代理使用的InvocationHandler
、cglib 使用的MethodInterceptor
,在抽象概念上可以算是Advice
(即使它們沒有繼承Advice
)。spring-aop 主要使用到Advice
的子介面MethodInterceptor
。
- 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
。
@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元件的原始碼分析 )。
接下來讓我們拋開這些“高階封裝”,看看 spring-aop 的真面目。
spring-aop 的代理工廠
下面通過一個 UML 圖來了解下 spring-aop 的結構,如下。
spring-aop 為我們提供了三種代理工廠,其中ProxyFactory
比較普通,AspectJProxyFactory
支援 AspectJ 語法的代理工廠,ProxyFactoryBean
可以給 Spring IoC 管理的 bean 進行代理。
下面介紹如何使用這些代理工廠來獲得代理類。
使用ProxyFactory生成代理類
ProxyFactory
的測試程式碼如下,如果指定了介面,一般會使用 JDK 動態代理,否則使用 cglib。
@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();
}
執行以上方法,可以看到控制檯輸出:
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。另外,我們可以設定每次生成的代理類都不同。
@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 包,如下:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
<!-- <scope>runtime</scope> -->
</dependency>
接下來配置一個Aspect
,如下。這裡定義了一個Advice
,即 printRequest 方法;定義了一個Pointcut
,即攔截UserService
及其子類。
@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
。
@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://www.cnblogs.com/ZhangZiSheng001/p/13671149.html