Spring(四) 關于AOP的源碼解析與思考
每種語言都有其獨特的機制和特點,那么說到Java
你可能會首先想到反射,反射是Java
語言提供的一種能夠在程序運行時動態操作類或對象的能力,比如獲取某個對象的類定義、獲取類聲明的屬性和方法、調用方法或構造對象、動態修改屬性值等,這是因為JVM
內存方法區中保存了加載的類元數據信息,并且每個對象實例的對象頭中都包含指向類元數據的指針,這為Java
提供了強大的靈活性和創造性,幾乎所有Java
技術和框架中都有反射的影子,通過反射獲取Class
類對象的方式包括 具體類.class
、對象實例.getClass()
、Class.forName()
、ClassLoader.loadClass()
。
除此之外,Java
是面向對象的語言,即支持OOP
(Object Oriented Programming
)面向對象編程,這是一種技術或思想,OOP
的核心是抽象模型、反應客觀事物的普遍規律和行為特征、模擬人類在現實世界中的思維認知,比如類與對象(整體與個體)、屬性與方法(特征與行為)、對象實例間的通信(實體間的聯系)等,其核心原則我們也耳熟能詳即封裝、繼承、多態,可以看出OOP
強調的是多樣性、靈活性。與之相對的是AOP
(Aspect Oriented Programming
)面向切面編程,切面(Aspect
)可以理解為關注點、在程序中要切入的方面,AOP
的核心是將跨越多個業務或模型的橫切關注點分離出來并進行模塊化、模板化、流程化,這些關注點通常與核心業務邏輯無關,它要解決的是業務交叉、代碼糾纏問題,比如日志、事務、安全、監控、審計等,AOP
強調的是統一性、復用性。需要注意的是,OOP
和AOP
代表了兩種不同的代碼組織和模塊化方式,但OOP
和AOP
不是對立關系而是互補關系,OOP
通過對象封裝和層次關系在縱向構建了應用程序的核心結構和業務邏輯(主要范式),而AOP
則通過分離橫切關注點在橫向解決了業務交叉或跨越模型上的復雜性和代碼糾纏問題。
上述內容可能比較抽象,但該部分已經把OOP
與AOP
的核心觀念與設計思想說得非常清楚,接下來我們將主要通過幾個實例和實踐來講解下AOP
思想/技術在Spring
中的應用與實現。本篇純干貨!
1. 動態代理
首當其沖的就是動態代理,我們知道靜態代理和動態代理都是代理模式的實現方式,該設計模式可以簡單看作是對目標方法(關注點)做增強,它本質上也是AOP
思想的實現。相比靜態代理來說動態代理能在運行時動態生成代理類,這依賴于字節碼生成技術,Spring
常用的兩種動態代理技術分別是JDK Proxy
和CGLIB
,其中JDK Proxy
是基于接口實現的,即其只能代理實現了接口的類或直接代理接口;而CGLIB
則是基于ASM
直接修改.class
文件生成對應子類,因此其無需接口即可代理任意普通類,Spring AOP
默認優先使用JDK
代理,無接口時使用CGLIB
代理。當然,在動態代理中也有反射的結合,代理方法中通常需要通過反射來調用目標方法,但動態代理和反射兩種機制不可混淆。
2. AspectJ
AspectJ
是Java
生態系統中完全獨立、完整且強大、功能豐富的專業AOP框架,AspectJ
有著對AOP
中切點、通知、切面等概念的完整實現與拓展,并支持編譯時織入(在編譯時將切面邏輯織入到目標類的字節碼中)、編譯后織入(在編譯后織入到現有class
類文件或jar
文件中)、加載時織入(在Java
類被類加載器加載到JVM
時進行織入)三種織入方式,AspectJ
可以攔截任何Java
對象的執行(不受Spring
容器限制),其可以攔截方法執行、構造器調用、字段讀寫、靜態初始化、異常處理等幾乎所有連接點。這里我們再看下AOP
中的幾個核心概念:
- 連接點:程序執行過程中可以插入切面代碼、實現增強的位置,在
SpringAOP
中通常為可調用方法的執行; - 切點:決定在哪些連接點應用通知(切點可以看作是連接點的子集),通常表現為能夠匹配連接點的表達式(
AspectJ
切點表達式); - 通知:在切點處執行插入增強的具體邏輯
- 前置通知:目標方法執行前;
- 后置通知:目標方法執行后(無論成功/失敗都會執行);
- 環繞通知:包圍目標方法執行;
- 返回通知:目標方法成功返回后;
- 異常通知:目標方法拋出異常后;
- 切面:切點+通知的封裝組成切面,表示一個完整模塊化、解耦化的橫切關注點(比如日志、事務等)
- 織入:將切面應用到目標對象的實現過程,比如
Spring
通過運行時動態代理實現切面;
3. AOP 實現
Spring AOP
本身是一個簡化版的、基于代理實現的AOP
框架(更加輕量),其核心實現方式與完整的AspectJ
框架不同,它是在運行時通過動態代理機制(JDK Proxy
、CGLIB
)實現切面功能,因此Spring AOP
僅支持作用于Spring IoC
容器管理的Bean
,而無法攔截非Spring
管理的對象或AOP Bean
對自身方法的調用。雖然Spring AOP
與AspectJ
在底層實現的本質上是不同的,但Spring AOP
直接使用了AspectJ
項目的注解風格(AspectJ
定義的@Aspect
、@Pointcut
、@Before
、@After
、@Around
等核心注解)和切點表達式語法,可以認為Spring AOP
利用了AspectJ
的方言和概念,提供了與AspectJ
兼容的編程模型。
3.1 引入AOP依賴
首先需要在項目中添加Spring Boot Starter AOP
依賴,該依賴包含了AspectJ
中的核心注解以及切點表達式語法。
<!-- AOP -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 定義AOP切面
3.2.1 業務實現
package com.example.aop.service;@Service
public class EverythingService {// 我們的業務邏輯public void processOrders(int type){if(type == 1){// 模擬拋出異常System.out.println("Processing orders error...");throw new RuntimeException("Error processing orders");}else{System.out.println("Processing orders success...");}}public void dealPayments(){System.out.println("Deal payments success...");}
}
3.2.2 切面實現
- 定義切面:@Aspect+@Component
- 定義切點:@Pointcut+切點表達式
- 定義通知:@Before、@Around、@After、@AfterReturning、@AfterThrowing+通知增強邏輯
/*** 0.定義切面 Spring AOP* - @Aspect: 聲明切面以實現匹配和織入* - @Component: 將切面對象加入到Spring管理*/
@Aspect
@Component
public class LogAop {/*** 1.定義切點: @Pointcut(切點表達式)* 1.1 匹配方法 execution(<訪問修飾符-可選> <返回類型> <全限定包名.類名.?法(?法參數列表)> <異常-可選>)* 1.1.1 通配符* - *: 匹配任意字符元素(返回類型、類名、方法名)* - ..: 匹配任意子包或多級目錄、任意數量參數* 1.1.2 例子* - execution(* com.example.aop.service.*.*(..)) 匹配com.example.aop.service包下所有類的所有方法* - execution(* com.example.aop.service..*.find*(..)) 匹配com.example.aop.service包及其所有子包下所有類中以find開頭的所有方法* 1.2 匹配注解 @annotation(注解全限定名稱)* - 例子: @annotation(com.example.aop.annotation.Loggable) 匹配帶有自定義@Loggable注解的方法*/@Pointcut("execution(* com.example.aop.service..*.process*(..))")public void logPointCut() {}/*** 2.定義通知-前置通知: @Before(切點/切點表達式)* @param joinPoint 切點對象(可選)*/@Before("logPointCut()")public void beforePointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【前置通知】" + methodName);}/*** 2.定義通知-后置通知: @After(切點/切點表達式)* @param joinPoint 切點對象(可選)*/@After("logPointCut()")public void afterPointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【后置通知】" + methodName);}/*** 2.定義通知-環繞通知: @Around(切點/切點表達式)* @param joinPoint 切點調用器(必須)* @return* @throws Throwable 異常*/@Around("logPointCut()")public Object aroundPointCut(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().getName();System.out.println("【環繞通知-前環繞】" + methodName);Object result = joinPoint.proceed(); //執行攔截器鏈上的后續目標(通知或目標方法)System.out.println("【環繞通知-后環繞】" + methodName);return result;}/*** 2.定義通知-返回通知: @AfterReturning(切點/切點表達式)* @param joinPoint 切點對象(可選)*/@AfterReturning("logPointCut()")public void afterReturningPointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【返回通知】" + methodName);}/*** 2.定義通知-異常通知: @AfterThrowing(切點/切點表達式)* @param joinPoint 切點對象(可選)*/@AfterThrowing("logPointCut()")public void afterThrowingPointCut(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("【異常通知】" + methodName);}}
3.2.3 切面織入
@SpringBootTest
class AopDemoApplicationTests {@ResourceEverythingService everythingService;@Testvoid contextLoads() {// 1.processOrders匹配切點表達式無異常everythingService.processOrders(0);System.out.println("==============================");try{// 2.processOrders匹配切點表達式有異常everythingService.processOrders(1);}catch (Exception e){}System.out.println("==============================");// 3.dealPayments不匹配切點表達式everythingService.dealPayments();}}
由下面的執行結果可以看出,聲明的切面僅匹配到了processOrders()
方法,并且不同類型通知之間具有特定執行順序;需要注意的是在目標方法拋出異常時會由異常通知代替返回通知,但后置通知無論是否出現異常都會執行。
【環繞通知-前環繞】processOrders
【前置通知】processOrders
Processing orders success...
【返回通知】processOrders
【后置通知】processOrders
【環繞通知-后環繞】processOrders
==============================
【環繞通知-前環繞】processOrders
【前置通知】processOrders
Processing orders error...
【異常通知】processOrders
【后置通知】processOrders
==============================
Deal payments success...
4. 原理淺析
4.1 通知器執行順序
Spring AOP
是基于Spring
動態代理(JDK Proxy
或CGLIB
)動態生成目標代理對象,該代理對象的作用是攔截目標方法調用,并根據切面匹配的通知Advisor
進行邏輯增強。在代理類中,首先會將匹配切點Pointcut
的所有通知器Advisor
轉化為攔截器類型MethodInterceptor
并構建攔截器鏈(包含所有通知的列表),在實際執行代理方法時會按照攔截器鏈中的順序依次調用攔截執行。切點對應攔截器鏈的構造順序如下:
- 不同切面
@Aspect
按優先級排序:多個切面之間可以通過@Order
注解來指定切面優先級(越小優先級越高),優先級越高則切面下的通知在攔截器鏈中就越靠前; - 相同切面下按通知類型排序:相同切面下的多種通知按照
Around
->Before
->After
->AfterReturning
->AfterThrowing
的類型順序在攔截器鏈排序 - 相同類型通知再按字符串升序排序:相同類型的按照通知的全限定名稱(包名+類名+方法名)即字符串升序排序;
public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable {private static final Comparator<Method> adviceMethodComparator;static {// 類型排序器 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.classComparator<Method> adviceKindComparator = new ConvertingComparator<>(new InstanceComparator<>(Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),(Converter<Method, Annotation>) method -> {AspectJAnnotation<?> ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);return (ann != null ? ann.getAnnotation() : null);});// 字符串排序器 Method::getName 升序Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);// 先按類型排序,然后通知器類型相同再按方法名稱字符串升序排序adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator);}// ...
}
需要注意的是: 盡管后置通知@After
在返回通知@AfterReturning
和異常通知@AfterThrowing
之前排序,但@After
方法實際上會在@AfterReturning
和@AfterThrowing
方法之后被調用(如上面切面織入時的輸出效果),這是因為@After
實際上只在相應的finally
塊中起作用,這里我們后面具體再看它的源碼分析。
4.2 通知器執行原理
我們首先來看下基于JDK
動態代理的Spring AOP
切面織入源碼JdkDynamicAopProxy.invoke()
如下,可以看到在攔截器鏈不為空時會創建方法調用器ReflectiveMethodInvocation
對象,來啟動攔截器鏈上通知的調用執行:
// 基于JDK動態代理實現的Spring AOP,用于創建動態代理增強
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {// 目標方法增強的織入過程public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// ...try {// ...target = targetSource.getTarget();Class<?> targetClass = (target != null ? target.getClass() : null);// 篩選出匹配當前方法的切面通知,并將Advisor轉換成MethodInterceptor類型的攔截器鏈(已經按照上述規則排好序)List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);// 如果攔截器鏈為空直接反射執行目標方法if (chain.isEmpty()) {Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);}else {// 創建方法調用器,并傳入需要執行的攔截器鏈 chainMethodInvocation invocation =new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);// 執行攔截器鏈,類似洋蔥模型的鏈上遞歸調用retVal = invocation.proceed();}// ...}}// ...
}
我們這里再看下ReflectiveMethodInvocation
的源碼,重點是其proceed()
方法是如何處理傳入的攔截器鏈List<Object> interceptorsAndDynamicMethodMatchers
的:
public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {// ...protected ReflectiveMethodInvocation(Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments,@Nullable Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {this.proxy = proxy;this.target = target;this.targetClass = targetClass;this.method = BridgeMethodResolver.findBridgedMethod(method);this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);// 傳入的攔截器鏈this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;}public Object proceed() throws Throwable {// 1.攔截器鏈都執行完畢,執行最終目標方法if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {// 調用切點return invokeJoinpoint();}// 2.否則繼續執行攔截器通知:獲取列表中當前要執行的攔截器 ++this.currentInterceptorIndexObject interceptorOrInterceptionAdvice =this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {// 適用于動態切點匹配: 在方法執行時根據參數動態判斷目標方法是否可以應用當前攔截器并執行InterceptorAndDynamicMethodMatcher dm =(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());// 匹配成功: 執行當前攔截器if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {return dm.interceptor.invoke(this);}else {// 匹配失敗: 跳過當前攔截器繼續proceed()判斷下個return proceed();}}else {// 適用于靜態切點匹配: 根據先前靜態創建的攔截器鏈應用當前攔截器(默認已經匹配)return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);}}// ...
}
其總體邏輯就是從攔截器鏈列表的首個通知器開始執行(已經排好序),直到最后的通知器然后執行最終目標方法。需要注意的是,在執行每個攔截器具體通知方法.invoke(this)
時,都會將方法調用器本身this
作為參數傳遞過去,這點非常重要! 攔截器鏈正是通過方法調用器MethodInvocation
的proceed()
方法作為觸發錨點,使得攔截器鏈能夠不斷遞歸調用下去。接下來我們分別看下這五類通知器的代理攔截邏輯實現(類似于棧壓入、彈出的方式)。
4.2.1 @Before前置通知 MethodBeforeAdviceInterceptor
public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {private final MethodBeforeAdvice advice;// MethodInvocation 正是傳遞的方法調用器public Object invoke(MethodInvocation mi) throws Throwable {// 執行前置通知方法this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());// 調用錨點-方法調用器的proceed()方法執行下個攔截器通知return mi.proceed();}
}
4.2.2 @After后置通知 AspectJAfterAdvice
在@After
后置通知中,是優先執行下個攔截器通知,最后在finally
塊中才執行后置通知邏輯。因此這也是為什么后置通知@After
在返回通知@AfterReturning
和異常通知@AfterThrowing
之前排序,但@After
方法實際上會在@AfterReturning
和@AfterThrowing
方法之后被調用的原因。
public class AspectJAfterAdvice extends AbstractAspectJAdviceimplements MethodInterceptor, AfterAdvice, Serializable {public Object invoke(MethodInvocation mi) throws Throwable {try {// 調用錨點-方法調用器的proceed()方法執行下個攔截器通知return mi.proceed();}finally {// 執行后置通知方法invokeAdviceMethod(getJoinPointMatch(), null, null);}}
}
4.2.3 @Around環繞通知 AspectJAroundAdvice
@Around
環繞通知的攔截器邏輯比較特殊,其在invoke
方法中是沒有主動調用MethodInvocation
的proceed()
方法來觸發下個攔截器的,因此需要我們在環繞通知的實現中手動決定方法調用器的觸發,才能使得攔截器鏈繼續執行下去,這也是環繞的概念或者說洋蔥模型的由來。
public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable {public Object invoke(MethodInvocation mi) throws Throwable {if (!(mi instanceof ProxyMethodInvocation)) {throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);}ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);JoinPointMatch jpm = getJoinPointMatch(pmi);// 執行環繞通知方法return invokeAdviceMethod(pjp, jpm, null, null);}}
4.2.4 @AfterReturning返回通知 AfterReturningAdviceInterceptor
public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable {private final AfterReturningAdvice advice;public Object invoke(MethodInvocation mi) throws Throwable {// 調用proceed()方法執行下個攔截器通知,并暫存結果Object retVal = mi.proceed();// 執行返回通知方法: 前面沒有異常時this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());// 返回結果給上層return retVal;}
}
4.2.5 @AfterThrowing異常通知 AspectJAfterThrowingAdvice
public class AspectJAfterThrowingAdvice extends AbstractAspectJAdviceimplements MethodInterceptor, AfterAdvice, Serializable {public Object invoke(MethodInvocation mi) throws Throwable {try {// 調用proceed()方法執行下個攔截器通知return mi.proceed();}catch (Throwable ex) {// 執行異常通知方法: 前面出現異常時if (shouldInvokeOnThrowing(ex)) {invokeAdviceMethod(getJoinPointMatch(), null, ex);}// 繼續拋出異常throw ex;}}}