什么是AOP
Aspect Oriented Programming(面向切面編程)
-
什么是面向切面編程呢? 切?就是指某?類特定問題, 所以AOP也可以理解為面向特定方法編程.
-
什么是面向特定方法編程呢? 比如對于"登錄校驗", 就是?類特定問題. 登錄校驗攔截器, 就是對"登錄校驗"這類問題的統?處理. 所以, 攔截器也是AOP的?種應?. AOP是?種思想, 攔截器是AOP思想的?種實現. Spring框架實現了這種思想, 提供了攔截器技術的相關接?.
-
同樣的, 統?數據返回格式和統?異常處理, 也是AOP思想的?種實現.
-
簡單來說AOP是一種思想,是對某一類事情的集中處理,實現的方式有很多,有SpringAOP,AspectJ,CGLIB等
SpringAOP快速入門
我們通過一個經典的“方法耗時統計”案例來快速上手 Spring AOP。
引入AOP依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
編寫切面代碼
一個切面包含了我們要執行的操作(通知) 和指定在何處執行(切點)。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;@Aspect // 聲明這是一個切面類
@Component // 將其作為Bean交由Spring容器管理
@Slf4j
public class TimeRecordAspect {// 1. 使用 @Pointcut 定義一個可重用的切點// 匹配 com.example.service 包及其子包下的所有類的所有方法@Pointcut("execution(* com.example.service..*.*(..))")public void serviceMethods() {}// 2. 定義通知(Advice),并引用上面的切點@Around("serviceMethods()")public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {// joinPoint 代表被攔截的目標方法String methodName = joinPoint.getSignature().toShortString();long startTime = System.currentTimeMillis();log.info("==> 開始執行 [{}], 參數: {}", methodName, joinPoint.getArgs());// 3. 調用 proceed() 執行原始的目標方法Object result = joinPoint.proceed();long endTime = System.currentTimeMillis();log.info("<== 執行 [{}] 結束, 耗時: {} ms, 返回值: {}", methodName, (endTime - startTime), result);return result;}
}
代碼解析:
@Aspect
: 標志著這個類是一個切面。@Pointcut
: 用于聲明一個可重用的切點表達式。這樣,當多個通知需要應用在相同的切點時,我們就不需要重復書寫冗長的execution
表達式了。@Around
: 環繞通知。這是功能最強大的通知類型,它能完全控制目標方法的執行,你可以在方法執行前后添加自定義邏輯,甚至可以決定是否執行目標方法。ProceedingJoinPoint
: 連接點對象,只在@Around
通知中使用。它代表了被攔截的目標方法,通過調用其proceed()
方法來執行原始方法。
[!INFO] SpringAOP只是使用了Aspect的注解
注解分為兩個步驟:1. 聲明 2. 實現
Spring中AOP的通知類型有以下幾種:
@Around
: 環繞通知。在目標方法執行前后都可執行,可以控制目標方法的執行。@Before
: 前置通知。在目標方法執行前執行。@After
: 后置通知。在目標方法執行后執行,無論方法是正常返回還是拋出異常,它都會執行(類似于finally
塊)。@AfterReturning
: 返回后通知。在目標方法成功執行并返回結果后執行,如果方法拋出異常則不會執行。@AfterThrowing
: 異常后通知。在目標方法拋出異常后執行。
@PointCut
如果有大量的方法,那么就會存在大量的切點表達式,此時可以使用@PointCut
把公共的切點表達式提取出來,需要時引入即可
@Aspect
@Component
@Slf4j
public class TimeRecordAspect { @Pointcut("execution(* com.doublez.springbook.controller.*.*(..))") private void pt(){} @Around("pt()") public Object timeRecordAspect(ProceedingJoinPoint joinPoint) throws Throwable { //... }@Around("pt()")public Object timeRecordAspect1(ProceedingJoinPoint joinPoint) throws Throwable {//...}@Around("pt()")public Object timeRecordAspect2(ProceedingJoinPoint joinPoint) throws Throwable {//...}@Around("pt()")public Object timeRecordAspect3(ProceedingJoinPoint joinPoint) throws Throwable {//...}
}
當切點定義使用private修飾時, 僅能在當前切?類中使用, 當其他切面類也要使用當前切點定義時, 就需要把private改為public. 引用方式為: 全限定類名.方法名()
切點表達式
常見的有兩種:@execution
@annotation
execution
-
通配符
*
的用法:- 匹配任意字符,但僅匹配一個元素,如返回類型、包名、類名、方法名或方法參數。
*
在包名中表示任意包(一層包)。*
在類名中表示任意類。*
在返回值中表示任意返回值類型。*
在方法名中表示任意方法。*
在參數中表示一個任意類型的參數。
-
通配符
..
的用法:- 匹配多個連續的任意符號,可以通配任意層級的包,或任意類型、任意個數的參數。
..
配置包名時,標識此包及其所有子包。..
配置參數時,表示任意個任意類型的參數。
-
切點表達式示例:
execution(public String com.example.demo.controller.TestController.t1())
:匹配 TestController 下的 public 修飾,返回類型為 String,方法名為 t1,無參方法。execution(String com.example.demo.controller.TestController.t1())
:省略訪問修飾符的情況。execution(* com.example.demo.controller.TestController.t1())
:匹配所有返回類型。execution(* com.example.demo.controller.TestController.*())
:匹配 TestController 下的所有無參方法。execution(* com.example.demo.controller.TestController.*(..))
:匹配 TestController 下的所有方法。execution(* com.example.demo.controller.*.*(..))
:匹配 controller 包下所有類的所有方法。execution(* com..TestController.*(..))
:匹配所有包下面的 TestController。execution(* com.example.demo...(..))
:匹配 com.example.demo 包下,子孫包下的所有類的所有方法。
AOP可以識別類的私有方法嗎,可以的話推薦嗎? ->here
@annotation
用于自定義類的實現:[[@annotation自定義注解實現|here]]
切面優先級@Order
- 數字越大,優先級越低(可以有負數)
@Aspect
@Component
@Order(1)
public class Demo {
}
@Aspect
@Component
@Order(3)
public class Demo2 {
}
AOP原理
Spring AOP 并沒有在編譯期修改你的代碼,而是在運行時通過動態代理技術實現的。當你從 Spring 容器中獲取一個 Bean 時,如果這個 Bean 需要被 AOP 增強,那么你拿到的其實是一個代理對象,而不是原始對象。所有對方法的調用都會先經過這個代理對象,由它來決定何時執行切面邏輯、何時執行原始方法。
[[代理模式]]
-
定義:為其他對象提供?種代理以控制對這個對象的訪問. 它的作用就是通過提供?個代理類, 讓我們在調用目標方法的時候, 不再是直接對目標方法進行調用, 而是通過代理類間接調用.
-
在某些情況下, ?個對象不適合或者不能直接引用另?個對象,而代理對象可以在客戶端和目標對象之間起到中介的作用
代理模式的主要角色
- Subject: 業務接口類,可以是抽象類或者接口(不一定有)
- RealSubject:業務實現類,具體的業務執行,也就是被代理對象
- Proxy:代理類。RealSubject的代理
- 根據代理的創建時期,代理模式可以分為靜態代理和動態代理
靜態代理
靜態代理需要我們手動為每個被代理的類創建一個代理類,代理類和被代理類實現相同的接口。
- 優點:簡單明了,容易理解。
- 缺點:非常不靈活。如果接口增加一個方法,被代理類和代理類都需要修改。并且,每個業務類都需要一個對應的代理類,會導致類的數量急劇膨脹。
讓我們用一個發短信的例子來說明:
// 1. 業務接口
interface SmsService {void send(String message);
}// 2. 業務實現類(被代理對象)
class SmsServiceImpl implements SmsService {@Overridepublic void send(String message) {System.out.println("發送短信: " + message);}
}// 3. 靜態代理類
class SmsServiceStaticProxy implements SmsService {private final SmsService target;public SmsServiceStaticProxy(SmsService target) {this.target = target;}@Overridepublic void send(String message) {System.out.println("[靜態代理] 發送短信前,進行日志記錄...");// 調用原始對象的方法target.send(message);System.out.println("[靜態代理] 發送短信后,操作完成。");}
}// 使用
public static void main(String[] args) {SmsService smsService = new SmsServiceImpl();SmsServiceStaticProxy proxy = new SmsServiceStaticProxy(smsService);proxy.send("Hello, Static Proxy!");
}
動態代理
由于靜態代理的局限性,動態代理應運而生。它不需要我們手動創建代理類,而是在程序運行時動態地生成代理對象。Spring 主要使用兩種動態代理技術:
JDK 動態代理
這是 Java 官方提供的代理方式,它要求被代理的類必須實現至少一個接口。它的核心是 java.lang.reflect.Proxy
類和 InvocationHandler
接口。
定義 JDK 動態代理類 (JDKInvocationHandler
):
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// JDK代理工廠
class JdkProxyFactory {public static Object getProxy(Object target) {return Proxy.newProxyInstance(target.getClass().getClassLoader(), // 目標類的類加載器target.getClass().getInterfaces(), // 目標類實現的接口new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("[JDK動態代理] 方法 " + method.getName() + " 執行前...");Object result = method.invoke(target, args); // 執行目標方法System.out.println("[JDK動態代理] 方法 " + method.getName() + " 執行后...");return result;}});}
}// 使用
public static void main(String[] args) {SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());smsService.send("Hello, JDK Proxy!");
}
代碼簡單講解:
InvocationHandler
:
InvocationHandler
接口是 Java 動態代理的關鍵接口之一,它定義了一個單一方法invoke()
,用于處理被代理對象的方法調用。public interface InvocationHandler {// proxy: 代理對象// method: 代理對象調用的實際方法,即其中需要增強的方法// args: 方法的參數Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
Proxy
:
Proxy
類中使用的最高頻率的方法是newProxyInstance()
,這個方法主要用來生成一個代理對象。public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
loader
: 類加載器,用于加載代理對象。interfaces
: 被代理類實現的一組接口(這個參數的定義,也決定了 JDK 動態代理只能代理實現了接口的類)。h
: 實現InvocationHandler
接口的對象。
CGLIB 動態代理
JDK 動態代理有一個最致命的缺陷是其只能代理實現了接口的類。在有些場景下,業務代碼是直接實現的類,并沒有實現接口。為了解決這個問題,可以使用 CGLIB 動態代理機制來解決。
CGLIB 動態代理的特點:
CGLIB (Code Generation Library) 是一個基于 ASM 的字節碼生成庫,它允許在運行時對字節碼進行修改和動態生成。CGLIB 通過繼承方式實現代理,很多知名的開源框架都使用了 CGLIB,例如 Spring 中的 AOP 模塊中:如果目標對象實現了接口,默認采用 JDK 代理,否則采用 CGLIB 代理。
CGLIB 動態代理實現步驟:
- 定義一個類(被代理類)。
- 自定義
MethodInterceptor
并重寫intercept
方法,intercept
用于增強目標方法。 - 通過
Enhancer
類的create()
創建代理類(子類)。 - 因此,它不要求被代理類實現接口,但要求該類不能是
final
的,方法也不能是final
的。
接下來看下實現:
JDK 動態代理不同,CGLIB (Code Generation Library) 實際是屬于一個開源項目,如果需要使用它的話,需要手動添加相關依賴。
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
自定義 MethodInterceptor
(方法攔截器) (CGLIBInterceptor
):
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;// CGLIB代理工廠
class CglibProxyFactory {public static Object getProxy(Object target) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(target.getClass()); // 設置父類(被代理類)enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("[CGLIB動態代理] 方法 " + method.getName() + " 執行前...");Object result = method.invoke(target, args); // 執行目標方法System.out.println("[CGLIB動態代理] 方法 " + method.getName() + " 執行后...");return result;}});return enhancer.create(); // 創建代理對象}
}// 使用(假設 SmsServiceImpl 沒有實現接口)
public static void main(String[] args) {SmsServiceImpl smsService = (SmsServiceImpl) CglibProxyFactory.getProxy(new SmsServiceImpl());smsService.send("Hello, CGLIB Proxy!");
}
Spring 如何選擇代理方式?
- 如果目標對象實現了接口,Spring AOP 默認會使用 JDK 動態代理。
- 如果目標對象沒有實現接口,Spring AOP 會使用 CGLIB 動態代理。
- 在 Spring Boot 2.x 之后,默認的代理方式改為了 CGLIB。你也可以通過配置文件
spring.aop.proxy-target-class=false
來強制使用 JDK 動態代理(前提是目標類實現了接口)。
代碼簡單講解:
MethodInterceptor
:
MethodInterceptor
和 JDK 代理中的InvocationHandler
類似,它只定義了一個方法intercept()
,用于增強目標方法。public interface MethodInterceptor extends Callback {/*** 參數說明:* o: 被代理的對象* method: 目標方法(被攔截的方法,也就是需要增強的方法)* objects: 方法入參* methodProxy: 用于調用原始方法*/Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable; }
Enhancer.create()
:
Enhancer.create()
用來生成一個代理對象。public static Object create(Class type, Callback callback) {// ...省略 }
type
: 被代理類的類型(類或接口)。callback
: 自定義方法攔截器MethodInterceptor
。
AOP 的一個重要“陷阱”:方法內部調用失效
當一個被 AOP 增強的 Bean,其內部的一個方法 methodB()
調用了另一個被增強的方法 methodA()
時,methodA()
的增強(如 @Transactional
或我們自定義的切面)會失效。
@Service
public class OrderService {@Transactional // 我們希望這個方法有事務public void createOrder() {// ... 業務邏輯 ...}public void processOrders() {// ... 其他邏輯 ...// 這種調用方式,createOrder() 的事務會失效!this.createOrder();}
}
原因:AOP 是通過代理對象實現的。外部調用 orderService.processOrders()
時,調用的是代理對象的方法。但是,當 processOrders()
內部執行 this.createOrder()
時,這里的 this
指向的是原始的 OrderService 對象,而不是代理對象。這次調用繞過了代理,直接訪問了原始對象的方法,因此所有的切面邏輯都不會被觸發。
如何解決?
-
注入自己:將自身的代理對象注入進來,通過代理對象來調用。
@Service public class OrderService {@Autowiredprivate OrderService self; // 注入自身的代理對象@Transactionalpublic void createOrder() { ... }public void processOrders() {// 通過代理對象調用self.createOrder();} }
注意:這可能會導致循環依賴問題,需要 Spring Boot 2.6+ 或額外配置來解決
-
使用
AopContext
:更優雅的方式是使用AopContext
來獲取當前的代理對象。@Service public class OrderService {@Transactionalpublic void createOrder() { ... }public void processOrders() {// 獲取當前代理對象并調用((OrderService) AopContext.currentProxy()).createOrder();} }
為了使
AopContext.currentProxy()
生效,需要在啟動類上添加@EnableAspectJAutoProxy(exposeProxy = true)
。
總 結
- AOP是一種思想,是對某一類事情的集中處理。Spring框架實現了AOP,稱之為SpringAOP
- Spring AOP常見實現方式有兩種:1. 基于注解@Aspect來實現 2. 基于自定義注解來實現,還有一些更原始的方式,比如基于代理,基于xml配置的方式,但目標比較少見
- Spring AOP 是基于動態代理實現的,有兩種方式:1. 基本JDK動態代理實現 2. 基于CGLIB動態代理實現。運行時使用哪種方式與項目配置和代理的對象有關。