輕鬆理解 Spring AOP
- 2021 年 4 月 14 日
- 筆記
- JAVA, spring, springboot
閱讀本文之前建議先了解動態代理,可以參看我的另一篇部落格 輕鬆理解 Java 靜態代理/動態代理
Spring AOP 簡介
Spring AOP 的基本概念
AOP (Aspect-Oriented Programming),即 面向切面編程, 它與 OOP (Object-Oriented Programming, 面向對象編程) 相輔相成, 提供了與 OOP 不同的抽象軟體結構的視角.
在 OOP 中, 我們以類(class)作為我們的基本單元, 而 AOP 中的基本單元是 Aspect(切面)
AOP是 Spring 是最難理解的概念之一,同時也是非常重要的知識點,因為它真的很常用。
面向切面編程
在面向切面編程的思想裡面,把功能分為兩種
- 核心業務:登陸、註冊、增、刪、改、查、都叫核心業務
- 周邊功能:日誌、事務管理這些次要的為周邊業務
在面向切面編程中,核心業務功能和周邊功能是分別獨立進行開發,兩者不是耦合的;
然後把切面功能和核心業務功能 “編織” 在一起,這就叫AOP
AOP 的目的
AOP能夠將那些與業務無關,卻為業務模組所共同調用的邏輯或責任(例如事務處理、日誌管理、許可權控制等)封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可拓展性和可維護性。
AOP 術語和流程
進一步了解AOP之前,我們先來看看AOP中使用到的一些術語,以及AOP運行的流程。
術語
- 連接點(join point):對應的是具體被攔截的對象,因為Spring只能支援方法,所以被攔截的對象往往就是指特定的方法。具體是指一個方法
- 切點(point cut):有時候,我們的切面不單單應用於單個方法,也可能是多個類的不同方法,這時,可以通過正則式和指示器的規則去定義,從而適配連接點。切點就是提供這樣一個功能的概念。具體是指具體共同特徵的多個方法。
- 通知(advice):它會根據約定織入流程中,需要弄明白它們在流程中的順序和運行的條件,有這幾種:
- 前置通知(before advice)
- 環繞通知(around advice)
- 後置通知(after advice)
- 異常通知(afterThrowing advice)
- 事後返回通知(afterReturning advice)
- 目標對象(target):即被代理的對象,通俗理解各個切點的所在的類就是目標對象。
- 引入(introduction):是指引入新的類和其方法,增強現有Bean的功能。
- 織入(weaving):它是一個通過動態代理技術,為原有服務對象生成代理對象,然後將與切點定義匹配的連接點攔截,並按約定將各類通知織入約定流程的過程。
- 切面(aspect):定義切點、各類通知和引入的內容,AOP將通過它的資訊來增強Bean的功能或將對應的方法織入流程。
上述的描述還是比較抽象的,配合下面的流程講解以及例子,應該充分掌握這些概念了。
流程
畫了一張圖,通過張圖可以清晰的了解AOP的整個流程,以及上面各個術語的意義和關係。
圖片的流程順序基於Spring 5
五大通知執行順序
不同版本的Spring是有一定差異的,使用時候要注意
-
Spring 4
- 正常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> 環繞返回 ==> 環繞最終 ==> @After ==> @AfterReturning
- 異常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> 環繞異常 ==> 環繞最終 ==> @After ==> @AfterThrowing
-
Spring 5
- 正常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> @AfterReturning ==> @After ==> 環繞返回 ==> 環繞最終
- 異常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> @AfterThrowing ==> @After ==> 環繞異常 ==> 環繞最終
例子
圖例
舉一個實際中的例子來說明一下方便理解:
房東的核心訴求其實就是簽合約,收錢,淺綠部分都是次要的,交給中介就好。
不過有的人可能就有疑問了,讓房東帶著不是更好嗎,租客溝通起來不是更輕鬆嗎?為啥非要分成兩部分呢?
那麼請看下面這種情況
當我們有很多個房東的時候,中介的優勢就體現出來了。代入到我們實際的業務中,AOP能夠極大的減輕我們的開發工作,讓關注點程式碼與業務程式碼分離!實現解藕!
實際的程式碼
用一個實際程式碼案例來感受一下
- 創建一個房東
@Component("landlord")
public class Landlord {
public void service() {
System.out.println("簽合約");
System.out.println("收錢");
}
}
- 創建中介
@Component
@Aspect
class Broker {
@Before("execution(* pojo.Landlord.service())")
public void before(){
System.out.println("帶租客看房");
System.out.println("談錢");
}
@After("execution(* pojo.Landlord.service())")
public void after(){
System.out.println("給鑰匙");
}
}
3.在 applicationContext.xml 中配置自動注入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xmlns:context="//www.springframework.org/schema/context"
xmlns:aop="//www.springframework.org/schema/aop"
xsi:schemaLocation="//www.springframework.org/schema/beans
//www.springframework.org/schema/beans/spring-beans.xsd //www.springframework.org/schema/context //www.springframework.org/schema/context/spring-context.xsd //www.springframework.org/schema/aop //www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="aspect" />
<context:component-scan base-package="pojo" />
<aop:aspectj-autoproxy/>
</beans>
- 測試
public class Test {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
Landlord landlord = (Landlord) context.getBean("landlord", Landlord.class);
landlord.service();
}
}
5.執行看到效果:
帶租客看房
談錢
簽合約
收錢
給鑰匙
這個例子中我們用到了@Before
和@After
兩個註解,其實就是設置的前置通知和後置通知。
最後的結果似乎與我們之前圖例中的順序不同,給鑰匙在收錢之後了,這個問題留到後面再解決,目前只需要簡單感受一下aop的使用即可。
預告:這種情況下應該使用環繞通知來完成這個需求
使用 Spring AOP
使用註解開發AOP
目前使用註解的方式進行Spring開發才是主流,包括SpringBoot中,已經是全註解開發,所以我們採用@AspectJ的註解方式,重新實現一下上面的用例,來學習AOP的使用。
註解 | 說明 |
---|---|
@Before |
前置通知,在連接點方法前調用 |
@Around |
環繞通知,它將覆蓋原有方法,可以想像成前置+原方法+後置 |
@After |
後置通知,在連接點方法後調用 |
@AfterReturning |
返回通知,在連接點方法執行並正常返回後調用,要求連接點方法在執行過程中沒有發生異常 |
@AfterThrowing |
異常通知,當連接點方法異常時調用 |
第一步:選擇連接點
Spring 是方法級別的 AOP 框架,我們主要也是以某個類額某個方法作為連接點,另一種說法就是:選擇哪一個類的哪一方法用以增強功能。
@Component
public class Landlord {
public void service() {
System.out.println("簽合約");
System.out.println("收錢");
}
}
我們在這裡就選擇上述 Landlord 類中的 service() 方法作為連接點。
第二步:創建切面
選擇好了連接點就可以創建切面了,我們可以把切面理解為一個攔截器,當程式運行到連接點的時候,被攔截下來,在開頭加入了初始化的方法,在結尾也加入了銷毀的方法而已,在 Spring 中只要使用 @Aspect
註解一個類,那麼 Spring IoC 容器就會認為這是一個切面了:
@Component
@Aspect
class Broker {
@Before("execution(* com.aduner.demo03.pojo.Landlord.service())")
public void before(){
System.out.println("帶租客看房");
System.out.println("談錢");
}
@After("execution(* com.aduner.demo03.pojo.Landlord.service())")
public void after(){
System.out.println("給鑰匙");
}
}
切面的類仍然是一個 Bean ,需要
@Component
註解標註
在上面的註解中定義了 execution 的正則表達式,Spring會通過這個正則式去匹配、去確定對應的方法(連接點)是否啟用切面編程
execution(* com.aduner.demo03.pojo.Landlord.service())
依次對這個表達式作出分析:
- execution:執行方法的時候會觸發
*
:任意返回類型的方法- com.aduner.demo03.pojo.Landlord:類的全限定名
- service():攔截的方法的名稱
- 如果是service(*) 就是表示任意參數的service方法
第三步:定義切點
每一個註解都重複寫了同一個正則式,這顯然比較冗餘。為了克服這個問題,Spring定義了切點(Pointcut)的概念,切點的作用就是向Spring描述哪些類的哪些方法需要啟用AOP編程,這樣可以有效的降低程式碼的複雜度,而且有利於維護的方便。
@Component
@Aspect
class Broker {
@Pointcut("execution(* com.aduner.demo03.pojo.Landlord.service())")
public void pointcut() {
}
@Before("pointcut()")
public void before() {
System.out.println("帶租客看房");
System.out.println("談錢");
}
@After("pointcut()")
public void after() {
System.out.println("給鑰匙");
}
}
第四步:配置好config
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.aduner.demo03.*",
excludeFilters = {@ComponentScan.Filter(classes = {Service.class})})
public class AppConfig {
}
第五步:測試 AOP
@Test
void testAspect(){
ApplicationContext ctx = new AnnotationConfigApplicationContext( AppConfig.class ) ;
Landlord landlord=ctx.getBean(Landlord.class);
landlord.service();
((ConfigurableApplicationContext)ctx).close();
}
結果
……
帶租客看房
談錢
簽合約
收錢
給鑰匙
……
環繞通知
現在我們來解決一下前面遺留的那個問題,收錢和給鑰匙的問題。
我們需要的應該是給了鑰匙之後再收錢,但是現在是反過來的。
要實現這個需求,用到環繞通知,這是 Spring AOP 中最強大的通知,集成了前置通知和後置通知。
環繞通知(Around)是所有通知中最為強大的通知,強大也意味著難以控制。一般而言,使用它的場景是在你需要大幅度修改原有目標對象的服務邏輯時,否則都盡量使用其他的通知。
環繞通知是一個取代原有目標對象方法的通知,當然它也提供了回調原有目標對象方法的能力。
- 我們先來修改一下Landlord
@Component
public class Landlord {
public void service(int steps) {
if (steps == 1) {
System.out.println("簽合約");
} else if(steps==2){
System.out.println("收錢");
}
else {
System.out.println("簽合約");
System.out.println("收錢");
}
}
}
我們將service添加一個參數,第一步簽合約,第二部收錢,如果沒有制定第一步或者第二步,就一起執行。
- 然後重新編寫一下我們的切面
@Component
@Aspect
class Broker {
@Pointcut("execution(* com.aduner.demo03.pojo.Landlord.service(*))")
public void pointcut() {
}
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) {
System.out.println("帶租客看房");
System.out.println("談價格");
try {
// joinPoint.proceed(); 這樣就是執行原方法
joinPoint.proceed(new Object[]{1}); //重新指定方法的參數
System.out.println("交鑰匙");
joinPoint.proceed(new Object[]{2});
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
- 修改一下剛剛的測試類,給到一個初始參數
@Test
void testAspect(){
ApplicationContext ctx = new AnnotationConfigApplicationContext( AppConfig.class ) ;
Landlord landlord=ctx.getBean(Landlord.class);
landlord.service(0);
((ConfigurableApplicationContext)ctx).close();
}
運行!成功!
……
帶租客看房
談價格
簽合約
交鑰匙
收錢
……
ProceedingJoinPoint對象
注意到切面編寫中Around裡面try
中的joinPoint.proceed()
方法
ProceedingJoinPoint對象是JoinPoint的子介面,該對象只用在@Around的切面方法中,添加了以下兩個方法。
Object proceed() throws Throwable //執行目標方法
Object proceed(Object[] var1) throws Throwable //傳入的新的參數去執行目標方法
前面的例子中我們顯示把房東的工作分為了兩步,然後再環繞通知中重新賦予參數並調用了兩次,在兩次中間插入了中介的工作。
實際開發中,上面這樣的寫法其實又會造成新的耦合,而且還會造成其他通知的混亂(調用了兩次方法,其實會讓有些通知返回兩次)。
當然這只是一個例子,為了幫助更好的理解環繞通知。
多個切面
Spring可以支援多個切面同時運行,如果剛好多個切面的切點相同,切面的運行順序就是一個關鍵了。
默認情況下,切面的運行順序是混亂的,如果需要指定切面的運行順序,我們需要用到@Order
註解
@Component
@Aspect
@Order(1)
public class FirstAspect {
……
}
--------------------
@Component
@Aspect
@Order(2)
public class SecondAspect {
……
}
@Order
註解中的值就是切片的順序,但是他們不是順序執行的而是包含關係。
總結
- AOP的出現是為了對程式解耦,減少系統的重複程式碼,提高可拓展性和可維護性。
- 常見的應用場景有許可權管理、快取、記錄跟蹤、優化、校準、日誌、事務等等等等……總之AOP的使用是非常常見的。
- 需要注意不同Spring版本之間的AOP通知順序是有差別的。
- 環繞通知很靈活、強大,但是也就意味著很難控制,如非必要,優先使用其他通知來完成。
- 多切面作用同一個切點時候注意切片順序。