1. AOP
AOP(Aspect-Oriented Programming), 是一種思想, 面向切面編程。
在前文統一異常處理,統一結果返回就是使用了這一思想(都是在集中處理某一類事情, 但又不影響原有代碼的正常運行),但他們不是AOP,只是應用了這一思想。
共同點就是:不修改目標方法,而達到了對目標方法功能的增強(就像MybatisPlus對Mybatis)?
AOP是一種思想, 實現它的方式有很多: SpringAOP, AspectJ, CGLIB, ....等
Spring 共有兩大核心機制, 一個是 Spring IoC, 一個就是 Spring AOP.
在這句話中,第一個 "Spring" 指的是 Spring Framework,而不是 Spring Boot。
原因是,Spring IoC(控制反轉)和 Spring AOP(面向切面編程)是 Spring Framework 的兩大核心特性。這些特性是 Spring Framework 的基礎,Spring Boot 則是在 Spring Framework 的基礎上進行封裝和簡化配置,使得開發者能更快速地構建應用。
Spring IoC 主要負責對象的管理和依賴注入。
Spring AOP 用于面向切面編程,通過代理機制提供橫切關注點(如日志、事務管理等)。
Spring Boot 作為 Spring Framework 的擴展,簡化了配置和開發流程,但核心機制(如 IoC 和 AOP)依然是基于 Spring Framework 的。所以當提到 "Spring 的核心機制" 時,通常是指 Spring Framework 中的 IoC 和 AOP。?
關于面向切面編程的講解會在應用中體現?
2. Spring AOP 入門
2.1 引入 Spring AOP 依賴
Spring AOP 依賴不能在創建項目時引入, 必須手動引入
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
2.2 Spring AOP 簡單使用
我們使用 Spring AOP 編寫一個程序, 記錄接口的執行時長.
@Slf4j
@Aspect
@Component
public class TimeAspect {//公共切點表達式@Pointcut("@annotation(com.cym.spring_aop.aspect.MyAspect)")private void pt(){}@Around("pt()")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//記錄方法執行前的時間long begin = System.currentTimeMillis();//方法執行Object result = pjp.proceed();//記錄方法執行后的時間long end = System.currentTimeMillis();log.info(pjp.getSignature() + ",耗費時間:{}" , end - begin);return result;}
}
如上圖所示, 通過 Spring AOP, 我們只需在外部通過 Spring AOP 就可以獲取到接口的執行時長. 達到了 "獲取執行時長" 這一功能和業務代碼的解耦
如果不使用 Spring AOP, 我們就需要在每個接口的起始位置和結束位置獲取時間戳, 再計算執行時長, 這樣不僅入侵了業務代碼, 還需要手動對每個要實現這個功能的接口都編寫這些代碼(對于懶人是一定不能接受的吧)
3.Spring AOP 詳解
3.1 核心概念
Spring AOP 有以下 4 個核心概念:
- 切點
- 連接點
- 通知
- 切面
舉個例子:
假如你要在所有 Controller 方法執行前打印日志,那么:
切點(Pointcut) = "所有 Controller 里的方法"(切點可以看做是保存了眾多連接點的?個集合)
連接點(JoinPoint)= "Controller 中某個具體的方法"(滿?切點表達式規則的?法)
通知(Advice) = "方法執行前,打印日志"(對他說:開工吧)
切面(Aspect) = "攔截所有 Controller 方法,在執行前打印日志"
(切?(Aspect) = 切點(Pointcut) + 通知(Advice))
?
3.1.1?切點
切點(Pointcut)本質上只是一個篩選規則, 它不會影響代碼執行, 也不會真正“攔截”任何東西, 它只是告訴 Spring 要對哪些方法進行攔截, 對哪些方法生效.
3.1.1.1 @Pointcut 定義切點
切點 = @Pointcut 注解(聲明切點) + 切點表達式(定義規則)
切點表達式是切點的一部分, 它決定了切點的“篩選規則”.
切點通過切點表達式定義一套規則, 這個規則表名了對哪些方法生效/攔截哪些方法(是一個集合), 描述哪些方法可能成為連接點
可以選擇直接在@Around中寫表達式:
@Around("execution(* com.cym.spring_aop.controller.*.*(..))")
也可以寫在 pt()中:(可以復用,推薦)
//公共切點表達式@Pointcut("execution(* com.cym.spring_aop.controller.*.*(..))")private void pt(){}@Around("pt()")
?還可以在其他切面中使用(使用時, 需要寫出這個切點的全限定名):
com.cym.spring_aop.aspect.TimeAspect.pt()
3.1.1.2 切點表達式
常見的切點表達式有以下兩種:
execution: 根據方法的簽名來匹配 (如上圖所示)
@annotation(......): 根據注解匹配
execution 表達式:
語法:
其中,?訪問限定修飾符和異常可以省略.
以execution(* com.cym.spring_aop.controller.*.*(..))為例:
/*** 1.TestController 下的 public修飾, 返回類型為String ?法名為t1, ?參?法* execution(public String com.example.demo.controller.TestController.t1())* 2.省略訪問修飾符* execution(String com.example.demo.controller.TestController.t1())* 3.匹配所有返回類型* execution(* com.example.demo.controller.TestController.t1())* 4.匹配TestController 下的所有?參?法* execution(* com.example.demo.controller.TestController.*())* 5.匹配TestController 下的所有?法* execution(* com.example.demo.controller.TestController.*(..))* 6.匹配controller包下所有的類的所有?法* execution(* com.example.demo.controller.*.*(..))* 7.匹配所有包下?的TestController* execution(* com..TestController.*(..))* 8.匹配com.example.demo包下, ?孫包下的所有類的所有?法* execution(* com.example.demo..*(..))*/
@annotation 表達式:
通過 execution 定義切點表達式, Spring AOP 攔截的是符合方法簽名規則的方法.
而通過 @annotation 定義切點表達式, Spring AOP 攔截的是標注了特定注解的方法(可以是自定義注解, 也可以是 Spring 提供的注解), 因此更加靈活.
@annotation 中, 需要寫出該注解的完全限定名.
1.第一步定義自定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {}
@Target(ElementType.METHOD):標識該注解只能作用于方法
@Retention(RetentionPolicy.RUNTIME):表示注解只存活在運行時,在編譯好的文件中不存在
2.在需要攔截的方法前加上該注解
3.根據該注解定義切點表達式
@Pointcut("@annotation(com.cym.spring_aop.aspect.MyAspect)")private void pt(){}@Around("pt()")
3.1.2 連接點
包含在切點表達式中的某個具體的方法, 在程序執行過程中實際被執行的那個方法,?就是一個連接點(即目標方法).
幸運兒,在增強中間執行的方法
切點(Pointcut)是一個篩選規則,用來定義哪些方法(連接點)會被 AOP 代理。
連接點(Join Point)是具體的方法,符合切點規則的方法就是連接點。
3.1.3 通知(Advice)
通知(Advice), 就是 AOP 攔截到目標方法(連接點)后, 具體要做的事/具體要執行的邏輯.
簡單來說, 通知就是決定攔截后要做什么事情 .
切點(Pointcut) 只是一個“篩選規則”,它決定哪些方法(連接點)需要被攔截,但它本身不執行任何邏輯.
通知(Advice) 是真正 “干活的人”,它決定攔截后要做什么事情(比如打印日志、權限校驗、事務管理等)
3.1.3.1 通知類型
Spring AOP 提供了 5 種常見的通知,不同的通知類型, 執行的時機不同:
? @Around: 環繞通知, 此注解標注的通知?法在?標?法前, 后都被執?
? @Before: 前置通知, 此注解標注的通知?法在?標?法前被執?
? @After: 后置通知, 此注解標注的通知?法在?標?法后被執?, ?論是否有異常都會執?
? @AfterReturning: 返回后通知, 此注解標注的通知?法在?標?法后被執?,
有異常不會執?
? @AfterThrowing: 異常后通知, 此注解標注的通知?法發?異常后執?
代碼演示:
/*** 前置通知*/@Before("execution(* com.cym.spring_aop.controller.*.*(..))")public void doBefore() {System.out.println("執行doBefore方法");}/*** 后置通知*/@After("execution(* com.cym.spring_aop.controller.*.*(..))")public void doAfter() {System.out.println("執行doAfter方法");}/***拋出異常后通知*/@AfterThrowing("execution(* com.cym.spring_aop.controller.*.*(..))")public void doAfterThrowing() {System.out.println("執行doAfterThrowing方法");}/*** 返回后通知*/@AfterReturning("execution(* com.cym.spring_aop.controller.*.*(..))")public void doAfterReturning() {System.out.println("執行doAfterReturning方法");}/*** 返回后通知*/@Around("execution(* com.cym.spring_aop.controller.*.*(..))")public Object Around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("執行Around方法前..");Object result = pjp.proceed();System.out.println("執行Around方法后..");return result;}
?程序正常運行的情況下, @AfterThrowing 標識的通知方法不會執行, 只有拋出異常時, 該通知方法才會執行.
正常執行:
發現執行順序:@Around? ?>? @doBefore? >? @doAferReturning? >? @doAfter? >? @Around
接下來我們添加一個異常 執行一下:
發現執行順序:@Around? ?>? @doBefore? >? @doAferThrowing? >? @doAfter?
3.1.4 切面優先級
Spring AOP 允許多個切面作用于同一個目標方法.
當多個切面類, 作用于同一個目標方法(連接點)時, 切面之間是有優先級的:
- 先執行優先級高的切面中的通知, 后執行優先級低的切面中的通知.
默認的切面優先級是按照名稱來排序的:
不加優先級:
加了優先級?
@Order(1)
@Slf4j
@Aspect
@Component
public class Aspect2 {
...
}
@Order(2)
@Slf4j
@Aspect
@Component
public class Aspect3 {
...
}
注意:?
對于 JDK 代理. Spring AOP 只對 public 修飾的方法生效,即切點匹配的目標方法必須是 public, 切面的通知才會生效.
對于 CGLib 代理, Spring AOP 對非 private 非 final 修飾的方法生效,即切點匹配的目標方法不能是 private 或者 final 的.
SpringBoot 默認使用的是 CGLib 代理.?
綜上, 如果要對我們項目中的某個方法進行 AOP 攔截通知, 那么這個方法不能是 private 或者 final 修飾的.