建與選型機制)
1. 從一次代理失效的排查說起去年幫一個朋友排查線上問題現(xiàn)象很怪一個訂單服務里明明加了Transactional日志里也看到事務攔截器被調(diào)用了但數(shù)據(jù)庫里就是沒回滾。他一開始懷疑是數(shù)據(jù)庫引擎的問題查了半天 InnoDB 配置又懷疑是異常被吞了加了一堆try-catch打印折騰到凌晨兩點。最后我讓他把斷點打在ProxyFactory的getProxy()方法上一看就明白了——他那個 Bean 是通過new出來的壓根沒走 Spring 容器代理對象根本沒生成事務攔截器自然形同虛設。這個坑其實特別典型。很多人用 Spring AOP 用了好幾年Aspect、Around、Transactional寫得很溜但一旦遇到代理沒生效切面順序不對this 調(diào)用不走切面這類問題就抓瞎了。根因在于大家只記住了注解怎么用卻沒搞清楚這些注解背后到底是誰在干活。而那個真正干活的核心角色就是ProxyFactory。ProxyFactory是 Spring AOP 里最被低估的一個類。它不像Aspect那樣天天出現(xiàn)在業(yè)務代碼里也不像BeanPostProcessor那樣在面試題里被反復背誦但它是整個 Spring AOP 創(chuàng)建代理對象的總調(diào)度室。你寫的每一個切面、每一個通知、每一個Transactional最終都要經(jīng)過它才能變成一個能攔截方法調(diào)用的代理對象。理解ProxyFactory等于拿到了 Spring AOP 源碼的鑰匙——你能看懂代理是怎么選的、通知是怎么排的、攔截器鏈是怎么串起來的遇到問題也能自己定位而不是靠猜。這篇內(nèi)容適合兩類人一類是已經(jīng)會用 Spring AOP但想搞清楚底層原理的中高級開發(fā)者另一類是正在讀 Spring 源碼被ProxyFactory、AdvisedSupport、JdkDynamicAopProxy這些類繞暈的人。我會從ProxyFactory的定位講起拆解它創(chuàng)建代理的完整鏈路重點講清楚 JDK 動態(tài)代理和 CGLIB 的選型邏輯、通知鏈的構(gòu)建過程以及實際使用中最容易踩的幾個坑。全程結(jié)合源碼但不會貼大段看不懂的代碼而是告訴你每一行代碼在解決什么問題。2. ProxyFactory 到底站在 Spring AOP 的哪個位置2.1 它不是給業(yè)務用的是給框架自己用的先糾正一個認知偏差。很多人以為ProxyFactory是給開發(fā)者手動創(chuàng)建代理用的工具類這個理解只對了一半。它確實可以手動用但它的主要服務對象是 Spring 框架自身。Aspect注解的解析、Transactional的代理創(chuàng)建、Cacheable的緩存攔截底層統(tǒng)統(tǒng)都是通過ProxyFactory來生成代理對象的。你可以把ProxyFactory理解成一個代理工廠的標準化流水線。它把我要給某個對象加代理這件事拆成了幾個標準步驟收集通知Advice、確定目標對象TargetSource、選擇代理方式JDK 還是 CGLIB、生成代理對象。Spring 的各種 AOP 功能本質(zhì)上都是在往這條流水線上投喂不同的原料。從類圖上看ProxyFactory繼承自ProxyCreatorSupport而ProxyCreatorSupport又繼承自AdvisedSupport。這條繼承鏈很關鍵AdvisedSupport負責持有目標對象、通知列表、攔截器列表以及一個AopProxyFactory。ProxyCreatorSupport負責管理Advisor列表并提供createAopProxy()方法來決定用哪種代理。ProxyFactory則是最外層的門面把上面這些能力包裝成好用的 API。所以當你調(diào)用proxyFactory.getProxy()的時候?qū)嶋H上是沿著這條繼承鏈一路往上最終由ProxyCreatorSupport決定代理策略由JdkDynamicAopProxy或CglibAopProxy完成實際的字節(jié)碼生成。2.2 和 AspectJ、Aspect 的關系別搞混這里必須澄清一個高頻誤解Spring AOP 和 AspectJ 不是一回事。AspectJ 是一套完整的 AOP 解決方案有自己的編譯器和織入機制能在編譯期、類加載期織入切面。而 Spring AOP 是運行時織入它不修改字節(jié)碼文件而是在運行時動態(tài)生成代理對象。Aspect這個注解雖然名字來自 AspectJ但在 Spring 里它只是一個標記告訴 Spring這個類里定義了切面邏輯。真正解析這個注解、把里面的Before、After、Around轉(zhuǎn)換成Advice對象的是 Spring 自己的AnnotationAwareAspectJAutoProxyCreator。而這個 Creator 最終創(chuàng)建代理時用的還是ProxyFactory。換句話說Aspect是語法糖ProxyFactory才是發(fā)動機。你寫Around的時候覺得很優(yōu)雅但 Spring 在背后做的事情是解析注解、生成AspectJMethodBeforeAdvice之類的通知對象、塞進ProxyFactory的Advisor列表、然后調(diào)用getProxy()生成代理。理解了這條鏈路你就知道為什么有些切面寫法會失效——因為你的寫法根本沒被解析成Advisor自然也就進不了ProxyFactory的流水線。2.3 一個最小可運行的 ProxyFactory 示例光講概念太虛直接上一個能跑的例子。假設我們有一個簡單的服務接口和實現(xiàn)public interface OrderService { void createOrder(String orderId); } public class OrderServiceImpl implements OrderService { Override public void createOrder(String orderId) { System.out.println(創(chuàng)建訂單: orderId); } }現(xiàn)在用ProxyFactory手動給它加一個方法執(zhí)行前的通知import org.springframework.aop.MethodBeforeAdvice; import org.springframework.aop.framework.ProxyFactory; import java.lang.reflect.Method; public class ProxyFactoryDemo { public static void main(String[] args) { OrderServiceImpl target new OrderServiceImpl(); ProxyFactory proxyFactory new ProxyFactory(); proxyFactory.setTarget(target); proxyFactory.addAdvice(new MethodBeforeAdvice() { Override public void before(Method method, Object[] args, Object target) { System.out.println(方法執(zhí)行前攔截: method.getName()); } }); OrderService proxy (OrderService) proxyFactory.getProxy(); proxy.createOrder(ORDER-001); } }運行結(jié)果會先打印方法執(zhí)行前攔截: createOrder再打印創(chuàng)建訂單: ORDER-001。這個例子雖然簡單但它完整走了一遍ProxyFactory的核心流程設置目標、添加通知、生成代理、調(diào)用方法。后面要講的所有源碼細節(jié)都是在這個骨架上展開的。注意這個例子默認走的是 JDK 動態(tài)代理因為OrderServiceImpl實現(xiàn)了接口。如果你把接口去掉只留實現(xiàn)類ProxyFactory會自動切換到 CGLIB。這個切換邏輯就是下一節(jié)要重點講的。3. JDK 動態(tài)代理還是 CGLIBProxyFactory 的選型邏輯3.1 選型發(fā)生在 createAopProxy 方法里代理方式的選擇不是隨機的也不是你隨便配的而是由ProxyCreatorSupport.createAopProxy()方法根據(jù)一組條件計算出來的。核心判斷邏輯在DefaultAopProxyFactory里簡化后的邏輯是這樣的public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class? targetClass config.getTargetClass(); if (targetClass null) { throw new AopConfigException(TargetSource cannot determine target class); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } }這段代碼信息量很大我拆開講。三個條件只要滿足任意一個就會進入可能用 CGLIB的分支isOptimize()是否開啟了優(yōu)化模式這個一般不用除非你明確知道自己在做什么。isProxyTargetClass()是否強制代理目標類。EnableAspectJAutoProxy(proxyTargetClass true)或者 Spring Boot 2.x 之后默認的spring.aop.proxy-target-classtrue都會讓這個值為 true。hasNoUserSuppliedProxyInterfaces()目標對象沒有實現(xiàn)任何用戶自定義接口。進入分支后還要再看一眼目標類本身如果目標類本身就是個接口或者已經(jīng)是個代理類那還是用 JDK 動態(tài)代理否則才用 CGLIB。3.2 為什么 Spring Boot 默認改成了 CGLIB這是很多人困惑的點。Spring 早期默認用 JDK 動態(tài)代理但從 Spring Boot 2.x 開始默認改成了 CGLIB。原因其實很實際JDK 動態(tài)代理要求目標類必須實現(xiàn)接口而現(xiàn)實中有大量業(yè)務類是不實現(xiàn)接口的比如各種XxxService直接就是 class。如果默認用 JDK 代理這些類就沒法被代理開發(fā)者得手動加接口或者改配置體驗很差。CGLIB 通過繼承目標類來生成子類代理不要求接口適用面更廣。代價是它不能代理final類和方法因為繼承不了。所以 Spring Boot 選擇 CGLIB 作為默認是在通用性和限制之間做的權(quán)衡。這里有個實操經(jīng)驗如果你的項目里大量使用了接口 實現(xiàn)類的分層結(jié)構(gòu)其實用 JDK 動態(tài)代理更輕量生成的代理類更小啟動也更快。可以在配置里顯式關掉 CGLIBspring: aop: proxy-target-class: false但要注意一旦關掉所有沒實現(xiàn)接口的 Bean 就無法被 AOP 代理了Transactional會靜默失效。這個坑我見過不止一次排查起來很費勁因為不報錯只是不生效。3.3 兩種代理方式的性能與限制對比對比維度JDK 動態(tài)代理CGLIB代理前提目標類必須實現(xiàn)接口目標類不能是 final方法不能是 final代理機制實現(xiàn)相同接口委托調(diào)用繼承目標類覆寫方法生成速度較快較慢需要生成子類字節(jié)碼調(diào)用速度反射調(diào)用略慢直接調(diào)用略快代理類數(shù)量每個接口一個代理類每個目標類一個代理類典型失效場景目標類無接口目標類是 final 或方法 final關于調(diào)用速度網(wǎng)上很多文章說 CGLIB 比 JDK 快這個結(jié)論要加限定條件。在 JDK 8 之后JDK 動態(tài)代理的性能已經(jīng)大幅優(yōu)化兩者差距很小。而且 Spring 在 CGLIB 代理里也做了緩存實際業(yè)務中這點差異基本可以忽略。選型時更應該關注的是能不能代理而不是哪個快。還有一個容易被忽略的點CGLIB 代理會創(chuàng)建目標類的子類這意味著目標類的構(gòu)造函數(shù)會被調(diào)用兩次一次是原始 Bean一次是代理子類。如果你的構(gòu)造函數(shù)里有副作用比如注冊監(jiān)聽器、啟動線程可能會出問題。這個坑在寫一些有狀態(tài)的服務時特別容易踩。4. 通知鏈是怎么一步步構(gòu)建起來的4.1 從 addAdvice 到攔截器鏈的轉(zhuǎn)換ProxyFactory提供了addAdvice()和addAdvisor()兩個方法看起來差不多但內(nèi)部處理邏輯不同。addAdvice()添加的是裸的Advice對象Spring 會把它包裝成一個DefaultPointcutAdvisorpointcut 是Pointcut.TRUE也就是匹配所有方法。而addAdvisor()添加的是已經(jīng)帶好 pointcut 的Advisor匹配邏輯由你自己控制。真正關鍵的是AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()這個方法。它做的事情是遍歷所有Advisor用AdvisorChainFactory把每個Advisor轉(zhuǎn)換成MethodInterceptor最終形成一個攔截器鏈。這個轉(zhuǎn)換過程有個細節(jié)如果一個Advisor的 pointcut 是動態(tài)的比如基于運行時參數(shù)判斷那它會被包裝成InterceptorAndDynamicMethodMatcher在調(diào)用時還要再做一次匹配。為什么要轉(zhuǎn)成MethodInterceptor因為MethodInterceptor是 AOP 聯(lián)盟定義的標準接口只有一個invoke(MethodInvocation invocation)方法。所有的通知類型——前置、后置、環(huán)繞、異常、返回——最終都會被適配成MethodInterceptor。比如MethodBeforeAdvice會被MethodBeforeAdviceInterceptor包裝AfterReturningAdvice會被AfterReturningAdviceInterceptor包裝。這樣整個鏈路上跑的都是統(tǒng)一類型調(diào)用邏輯就簡化了。4.2 攔截器鏈的執(zhí)行順序與責任鏈模式攔截器鏈構(gòu)建好之后執(zhí)行時用的是責任鏈模式。以JdkDynamicAopProxy.invoke()為例核心邏輯是public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodInvocation invocation new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); return invocation.proceed(); }ReflectiveMethodInvocation.proceed()是責任鏈的核心。它維護了一個currentInterceptorIndex游標每次調(diào)用proceed()就把游標加一取出下一個攔截器執(zhí)行。如果游標已經(jīng)到鏈尾就直接反射調(diào)用目標方法。每個攔截器在執(zhí)行完自己的邏輯后可以選擇繼續(xù)調(diào)用invocation.proceed()也可以直接返回從而中斷鏈路。這個設計帶來的一個重要特性是攔截器的順序決定了通知的執(zhí)行順序。Around通知如果排在前面它的proceed()之前的代碼會在所有后續(xù)通知之前執(zhí)行proceed()之后的代碼會在所有后續(xù)通知之后執(zhí)行。這就是為什么Order注解能控制切面順序——它影響的是Advisor在列表里的排序進而影響攔截器鏈的順序。4.3 一個容易搞錯的順序問題很多人以為Before一定在Around之前執(zhí)行其實不一定。執(zhí)行順序取決于Advisor的排序。如果Around切面的Order值更小它會先執(zhí)行它的proceed()之前的部分會先于Before執(zhí)行。我見過一個案例有人在Around里做了參數(shù)校驗在Before里做了日志記錄結(jié)果發(fā)現(xiàn)日志里記錄的參數(shù)是校驗前的原始參數(shù)而不是校驗后修改的參數(shù)。原因就是Around的 order 更小先執(zhí)行了proceed()之后才輪到Before。要控制這個順序最穩(wěn)妥的方式是顯式給切面加Order并且理解order 越小越靠外這個規(guī)則??客獾那忻嫫鋚roceed()之前的邏輯最先執(zhí)行proceed()之后的邏輯最后執(zhí)行??梢园阉胂蟪裳笫[最外層的是 order 最小的切面。5. 手動使用 ProxyFactory 時最容易踩的四個坑5.1 目標對象為 null 導致的空指針ProxyFactory允許你先設置通知再設置目標但如果在getProxy()之前目標還是 null生成的代理在調(diào)用方法時會拋NullPointerException。更隱蔽的情況是你設置了TargetSource但TargetSource.getTarget()返回 null。這種情況在池化目標對象比如PoolingTargetSource時可能出現(xiàn)池空了就會返回 null。排查這類問題的技巧是在getProxy()之前打印一下proxyFactory.getTargetSource().getTarget()確認目標對象確實存在。如果用的是自定義TargetSource要確保getTarget()方法在目標不可用時拋出明確異常而不是返回 null。5.2 代理對象被再次代理導致的通知重復這個坑在手動使用ProxyFactory時特別常見。假設你有一個已經(jīng)代理過的對象又把它作為目標傳給一個新的ProxyFactory那最終生成的代理會包含兩層攔截器鏈通知會執(zhí)行兩次。在 Spring 容器里AbstractAutoProxyCreator會通過AopUtils.isAopProxy()判斷對象是否已經(jīng)是代理避免重復代理。但手動使用時沒有這層保護。判斷一個對象是否已經(jīng)是代理可以用AopUtils.isAopProxy(obj)或AopUtils.isJdkDynamicProxy(obj)、AopUtils.isCglibProxy(obj)。在手動創(chuàng)建代理前加一道判斷能避免很多詭異問題。5.3 this 調(diào)用不走代理的經(jīng)典問題這個問題在 Spring 容器里也常見但手動用ProxyFactory時更容易理解。假設目標類里有兩個方法a()和b()a()內(nèi)部調(diào)用了this.b()。當你通過代理對象調(diào)用a()時a()的調(diào)用會被攔截但a()內(nèi)部通過this調(diào)用的b()不會走代理因為this指向的是原始目標對象不是代理對象。解決辦法有三種一是把b()的調(diào)用改成通過代理對象調(diào)用注入自身或AopContext.currentProxy()二是用AspectJ的編譯期織入三是重新設計把b()的邏輯抽到另一個 Bean 里。第一種方案需要開啟exposeProxyproxyFactory.setExposeProxy(true);然后在代碼里用AopContext.currentProxy()獲取當前代理。但要注意exposeProxy會把代理對象存到ThreadLocal里用完必須清理否則會有內(nèi)存泄漏風險。Spring 在invoke()方法里用try-finally保證了清理手動使用時也要注意這一點。5.4 通知里拋異常導致的事務回滾失效這個坑和ProxyFactory本身關系不大但和攔截器鏈的執(zhí)行密切相關。如果Around通知里捕獲了異常但沒有重新拋出那后續(xù)的Transactional攔截器就感知不到異常事務不會回滾。因為事務回滾依賴的是異常傳播到TransactionInterceptor如果異常在更外層的切面被吞了事務攔截器就以為一切正常。正確的做法是如果Around里需要處理異常要么處理后重新拋出要么明確知道自己在做什么。我一般建議在Around里只做日志和監(jiān)控不要吞異常。如果確實需要轉(zhuǎn)換異常類型要確保轉(zhuǎn)換后的異常是RuntimeException或者配置了rollbackFor。6. 從源碼看代理對象的生成時機與緩存6.1 getProxy 方法做了什么ProxyFactory.getProxy()最終調(diào)用的是ProxyCreatorSupport.createAopProxy()拿到AopProxy后調(diào)用其getProxy()方法。以JdkDynamicAopProxy為例它的getProxy()邏輯是public Object getProxy(ClassLoader classLoader) { Class?[] proxiedInterfaces AopProxyUtils.completeProxiedInterfaces(this.advised, true); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); }關鍵在completeProxiedInterfaces()它會收集目標對象實現(xiàn)的所有接口再加上 Spring 自己需要的SpringProxy、Advised、DecoratingProxy這幾個接口。為什么要加這幾個接口因為 Spring 需要通過它們來判斷一個對象是不是代理、獲取代理的Advisor列表、獲取最終的目標類。這也是為什么你可以把一個代理對象強轉(zhuǎn)成Advised然后調(diào)用getAdvisors()查看它有哪些通知。6.2 代理類的緩存機制JDK 動態(tài)代理本身有緩存Proxy.newProxyInstance()生成的代理類會被緩存在WeakCache里key 是類加載器和接口數(shù)組。CGLIB 也有緩存Spring 在CglibAopProxy里用MapClass?, Constructor?緩存了代理類的構(gòu)造函數(shù)避免每次都重新生成。但要注意這個緩存是代理類級別的不是代理對象級別的。每次調(diào)用getProxy()都會創(chuàng)建一個新的代理對象只是復用了代理類的字節(jié)碼。所以如果你在循環(huán)里反復調(diào)用getProxy()會創(chuàng)建大量代理對象雖然代理類只生成一次但對象創(chuàng)建本身也有開銷。正確的做法是代理對象創(chuàng)建一次緩存起來復用。在 Spring 容器里這個緩存由AbstractAutoProxyCreator的proxyTypes和SingletonBeanRegistry共同保證每個單例 Bean 只會被代理一次。手動使用時你需要自己保證這一點。6.3 一個驗證代理類型的實用方法調(diào)試 AOP 問題時快速確認一個對象用的是哪種代理能省很多時間??梢詫懸粋€工具方法public static String getProxyType(Object obj) { if (AopUtils.isJdkDynamicProxy(obj)) { return JDK動態(tài)代理; } else if (AopUtils.isCglibProxy(obj)) { return CGLIB代理; } else if (AopUtils.isAopProxy(obj)) { return 其他AOP代理; } else { return 非代理對象; } }在排查為什么事務不生效時先打印一下 Bean 的代理類型如果顯示非代理對象那基本可以確定是代理沒生成問題出在配置或 Bean 的創(chuàng)建方式上而不是事務本身。這個技巧我用了很多次每次都能快速縮小排查范圍。7. 幾個真實場景下的排查思路7.1 切面不生效的通用排查鏈路遇到切面不生效我一般按這個順序排查確認目標 Bean 是否被 Spring 管理。如果是new出來的或者通過Bean方法返回但沒走容器代理不會生成。確認切面類是否被 Spring 掃描到。Aspect類需要是一個 Bean通常加Component。確認代理類型是否匹配。如果目標類沒接口且用了 JDK 代理代理不會生成。確認 pointcut 表達式是否匹配。可以用AopUtils.canApply()手動驗證。確認方法是否是final或static。這兩種方法無法被 CGLIB 代理。確認調(diào)用是否走了代理。this調(diào)用不走代理。這個鏈路覆蓋了 90% 以上的切面失效場景。按順序排查基本都能定位到根因。7.2 循環(huán)依賴與代理的相互影響Spring 解決循環(huán)依賴時會提前暴露一個早期引用。如果這個 Bean 需要被 AOP 代理早期引用暴露的是代理對象還是原始對象答案是如果代理創(chuàng)建發(fā)生在postProcessAfterInitialization階段那早期引用暴露的是原始對象后續(xù)注入到其他 Bean 里的是代理對象這會導致同一個 Bean 在不同地方類型不一致。Spring 通過getEarlyBeanReference()方法解決了這個問題在提前暴露引用時如果 Bean 需要被代理就先創(chuàng)建代理對象并暴露出去。這樣其他 Bean 注入到的就是代理對象保持一致。理解這一點對排查循環(huán)依賴相關的 AOP 問題很有幫助。7.3 性能敏感場景下的代理選擇在高頻調(diào)用的場景下代理帶來的開銷不能忽略。JDK 動態(tài)代理每次調(diào)用都要走反射CGLIB 雖然直接調(diào)用但代理類的生成和類加載也有成本。如果某個方法每秒被調(diào)用幾十萬次代理的開銷可能會顯現(xiàn)出來。優(yōu)化思路有幾個一是盡量縮小切面的匹配范圍只攔截必要的方法減少攔截器鏈的長度二是對于性能極度敏感的方法考慮不用 AOP直接手寫裝飾器三是用Around時避免在通知里做重操作比如每次都查數(shù)據(jù)庫。我見過一個案例有人在Around里每次都打印完整的方法參數(shù)和返回值日志量巨大導致性能下降了一半。把日志級別調(diào)高或者改成采樣打印后性能恢復正常。8. 我個人的幾點使用體會ProxyFactory這個類我建議每個用 Spring 的開發(fā)者都花時間讀一遍源碼。它不長核心邏輯就那么幾百行但它是理解 Spring AOP 的最佳入口。讀懂了它你再去看Transactional、Cacheable、Async這些注解的實現(xiàn)會發(fā)現(xiàn)它們都是同一套模式的變體解析注解生成Advisor塞進ProxyFactory生成代理攔截器鏈執(zhí)行。手動使用ProxyFactory的場景其實不多主要是在寫框架擴展、自定義 AOP 邏輯、或者做單元測試時。但即使不手動用理解它的工作原理也能幫你快速定位問題。我自己的習慣是遇到 AOP 相關的詭異問題先在getProxy()和invoke()兩個方法上打斷點看代理有沒有生成、攔截器鏈里有哪些攔截器、執(zhí)行順序是什么。這兩個斷點基本能解決大部分問題。最后分享一個小技巧如果你想看某個 Bean 最終被哪些Advisor代理了可以把它強轉(zhuǎn)成Advised然后打印getAdvisors()。這個方法在排查為什么這個切面沒生效時特別有用能直接看到實際生效的Advisor列表而不是靠猜。