目錄
概要
和面向對象編程的區別
優點
AOP的底層原理
JDK動態代理技術
AOP七大術語
切點表達式
AOP實現方式
Spring對AOP的實現包括以下3種方式:
在本篇文章中,我們主要講解前兩種方式。
基于AspectJ的AOP注解式開發
定義目標類以及目標方法
定義切面類
目標類和切面類都納入Spring bean管理
在切面類中添加通知
再通知上添加切點表達式
在Spring配置文件中啟用自動代理
通知類型
接下來,編寫程序來測試這幾個通知的執行順序
切面的先后順序
優先使用切點表達式
基于XML配置方式的AOP
編寫目標類
編寫切面類,并編寫通知
編寫Spring配置文件
概要
在軟件開發的旅程中,我們常常會遇到一些橫切關注點(cross - cutting concerns),這些關注點如同貫穿整個應用程序的絲線,涉及多個不同的模塊和功能。例如,日志記錄、事務管理、權限驗證等功能,它們并非屬于某個特定的業務模塊,卻又在許多業務方法中都需要被執行。?
在傳統的編程模式下,為了實現這些橫切關注點,開發者往往不得不將相關的代碼片段分散地嵌入到各個業務方法中。以日志記錄為例,可能在每個需要記錄日志的業務方法開頭和結尾都要添加打印日志的代碼,這不僅導致代碼的大量重復,還使得業務邏輯與這些輔助功能的代碼緊密糾纏在一起,嚴重影響了代碼的可讀性、可維護性和可擴展性。?
Spring AOP 應運而生,它提供了一種優雅而強大的解決方案,能夠將這些橫切關注點從業務邏輯中分離出來,以一種模塊化的方式進行集中管理和維護。通過定義切面(Aspect),Spring AOP 可以在不修改業務邏輯代碼的前提下,在特定的連接點(Join Point,如方法調用、異常拋出等)插入相應的通知(Advice,如前置通知、后置通知、環繞通知等),從而實現對橫切關注點的統一處理。
和面向對象編程的區別
維度 | 面向對象編程 | 面向切面編程 |
---|---|---|
核心思想 | 以 “對象” 為中心,將現實世界抽象為類、對象及對象間的交互。 | 以 “切面” 為中心,將橫切關注點從業務邏輯中分離,實現模塊化管理。 |
基本單元 | 類(Class)和對象(Object),通過封裝、繼承、多態構建程序結構。 | 切面(Aspect),通過定義切點(Pointcut)和通知(Advice)織入橫切邏輯。 |
關注點類型 | 聚焦于業務邏輯的縱向模塊化(如用戶管理、訂單處理等獨立業務模塊)。 | 聚焦于橫切關注點的橫向抽取(如日志、權限、事務等貫穿多個模塊的功能)。 |
優點
- 代碼復用性強
- 代碼易維護
- 使開發者更專注于業務邏輯
AOP的底層原理
JDK動態代理技術
為接口創建代理類的字節碼文件,使用ClassLoader將字節碼文件加載到JVM,創建代理類實例對象,執行對象的目標方法。
AOP七大術語
- 連接點Joinpoint:指那些被攔截到的點(位置)。在spring中,這些點指的是方法,因為spring只支持方法類型的連接點
- 切點Pointcut:指我們要對哪些Joinpoint進行攔截的定義,在程序執行流程中,真正織入切面的方法(一個切點對應多個連接點)
- 切面Aspect:切點+通知就是一個切面,需要自己編寫和配置
- 通知Advice:通知又叫增強,就是具體你要織入的代碼
- 織入Weaving:把通知應用到目標對象上的過程(指把增強應用到目標對象來創建新的代理對象的過程)
- 代理對象 Proxy:一個目標對象被織入通知后產生的新對象
- 目標對象 Target:被織入通知的對象
其中的通知(Aspect)包括:
- 前置通知:befer
- 后置通知:after-returning
- 最終通知:after
- 異常通知:throwing
- 環繞通知:around
public class Cat {?// Cat類的run方法?public void run() {?System.out.println("Cat is running");?}?
}?
?
class Test {?public static void main(String[] args) {?Cat cat = new Cat();?cat.run();?}?
}
切點表達式
切點表達式用來定義通知(Advice)往哪些方法上切入,語法格式如下:?
execution([訪問控制權限修飾符] 返回值類型 [全限定類名]方法名(形式參數列表) [異常])
- 訪問權限控制符:(可選項)沒寫就是4種權限都可以
- 返回值類型:(必填項)若為“*”,表示返回值類型任意?
- 全限定類名:(可選項)兩個點“..”表示當前包以及子包下的所有類,若省略,就表示所有的類
- 方法名:(必填項)“*”表示所有方法,set * 表示所有的set方法
- 形式參數列表:(必填項)
- ():表示沒有參數的列表
- (..) :參數類型和個數隨意的方法
- (*): 只有一個參數的方法
- (*, String): 第一個參數類型隨意,第二個參數是String的
- 異常:(可選項)省略是表示任意異常
如:
execution(public * com.powernode.mall.service.*.delete*(..))
表示返回值類型任意,處于com.powernode.mall.service包下的所有類的所有參數任意的deleteXxx方法execution(* com.powernode.mall..*(..))
任意修飾符、返回值類型的,處于com.powernode.mall報下的所有方法execution(* *(..))
表示該項目的所有方法
AOP實現方式
Spring對AOP的實現包括以下3種方式:
- Spring框架結合AspectJ框架實現的AOP,基于注解方式。
- Spring框架結合AspectJ框架實現的AOP,基于XML方式。
- Spring框架自己實現的AOP,基于XML配置方式。
在本篇文章中,我們主要講解前兩種方式。
基于AspectJ的AOP注解式開發
定義目標類以及目標方法
// 目標類
public class OrderService {// 目標方法public void generate(){System.out.println("訂單已生成!");}
}
定義切面類
// 切面類
@Aspect
public class MyAspect {
}
注解@Aspect會告訴Spring該類是一個注解類
目標類和切面類都納入Spring bean管理
在目標類OrderService上添加@Component注解
在切面類MyAspect類上添加**@Component**注解
在切面類中添加通知
// 切面類
@Aspect
@Component
public class MyAspect {// 這就是需要增強的代碼(通知)public void advice(){System.out.println("我是一個通知");}
}
再通知上添加切點表達式
// 切面類
@Aspect
@Component
public class MyAspect {// 切點表達式@Before("execution(* com.xxx.spring6.service.OrderService.*(..))")// 這就是需要增強的代碼(通知)public void advice(){System.out.println("我是一個通知");}
}
其中,注解@Before表示前置通知(具體的通知類型在上面有講過,下面也會有方法中的通知注解)
在Spring配置文件中啟用自動代理
<!--開啟組件掃描--><context:component-scan base-package="com.xxx.spring6.service"/><!--開啟自動代理--><aop:aspectj-autoproxy proxy-target-class="true"/>
<aop:aspectj-autoproxy ?proxy-target-class="true"/> 開啟自動代理之后,凡是帶有@Aspect注解的bean都會生成代理對象。
proxy-target-class="true" 表示采用cglib動態代理。
proxy-target-class="false" 表示采用jdk動態代理。默認值是false。即使寫成false,當沒有接口的時候,也會自動選擇cglib生成代理類(AOP的底層就是動態代理,關于動態代理的內容可查看代理機制) ??
通知類型
- 前置通知:@Before 目標方法執行之前的通知
- 后置通知:@AfterReturning 目標方法執行之后的通知
- 環繞通知:@Around 目標方法之前添加通知,同時目標方法執行之后添加通知。
- 異常通知:@AfterThrowing 發生異常之后執行的通知
- 最終通知:@After 放在finally語句塊中的通知
接下來,編寫程序來測試這幾個通知的執行順序
// 切面類
@Component
@Aspect
public class MyAspect {@Around("execution(* com.xxx.spring6.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("環繞通知開始");// 執行目標方法。proceedingJoinPoint.proceed();System.out.println("環繞通知結束");}@Before("execution(* com.xxx.spring6.service.OrderService.*(..))")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("execution(* com.xxx.spring6.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("execution(* com.xxx.spring6.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("異常通知");}@After("execution(* com.xxx.spring6.service.OrderService.*(..))")public void afterAdvice(){System.out.println("最終通知");}}
讀者可自行測試,其中異常通知需要產生異常才能觸發,當發生異常之后,最終通知也會執行,因為最終通知@After會出現在finally語句塊中。出現異常之后,“后置通知”和“環繞通知”的結束部分不會執行
切面的先后順序
我們知道,業務流程當中不一定只有一個切面,可能有的切面控制事務,有的記錄日志,有的進行安全控制,如果多個切面的話,順序如何控制:可以使用@Order注解來標識切面類,為@Order注解的value指定一個整數型的數字,數字越小,優先級越高
優先使用切點表達式
- 上面的切點表達式重復寫了多次,沒有得到復用,同時如果要修改切點表達式,需要修改多處,難維護
- 我們可以將切點表達式單獨的定義出來,在需要的位置引入即可
// 切面類
@Component
@Aspect
@Order(2)
public class MyAspect {@Pointcut("execution(* com.xxx.spring6.service.OrderService.*(..))")public void pointcut(){}@Around("pointcut()")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("環繞通知開始");// 執行目標方法。proceedingJoinPoint.proceed();System.out.println("環繞通知結束");}@Before("pointcut()")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("pointcut()")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("pointcut()")public void afterThrowingAdvice(){System.out.println("異常通知");}@After("pointcut()")public void afterAdvice(){System.out.println("最終通知");}}
使用@Pointcut注解來定義獨立的切點表達式。
注意這個@Pointcut注解標注的方法隨意,只是起到一個能夠讓@Pointcut注解編寫的位置
基于XML配置方式的AOP
編寫目標類
// 目標類
public class VipService {public void add(){System.out.println("保存vip信息。");}
}
編寫切面類,并編寫通知
// 負責計時的切面類
public class TimerAspect {public void time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long begin = System.currentTimeMillis();//執行目標proceedingJoinPoint.proceed();long end = System.currentTimeMillis();System.out.println("耗時"+(end - begin)+"毫秒");}
}
編寫Spring配置文件
<!--納入spring bean管理--><bean id="vipService" class="com.xxx.spring6.service.VipService"/><bean id="timerAspect" class="com.xxx.spring6.service.TimerAspect"/><!--aop配置--><aop:config><!--切點表達式--><aop:pointcut id="p" expression="execution(* com.xxx.spring6.service.VipService.*(..))"/><!--切面--><aop:aspect ref="timerAspect"><!--切面=通知 + 切點--><aop:around method="time" pointcut-ref="p"/></aop:aspect></aop:config>
</beans>