Spring AOP核心注解全解析:從原理到實(shí)戰(zhàn),徹底掌握切面編程
1. 項(xiàng)目概述為什么我們需要Spring AOP如果你寫過一段時(shí)間的Java Web應(yīng)用尤其是用過Spring框架那你肯定遇到過這樣的場(chǎng)景你需要在幾十個(gè)Service方法里都加上同樣的日志記錄邏輯或者你需要在所有涉及數(shù)據(jù)庫操作的方法前后統(tǒng)一管理事務(wù)的開啟與提交又或者你希望在不修改業(yè)務(wù)代碼的前提下為某些接口添加權(quán)限校驗(yàn)。這時(shí)候如果你還在用“復(fù)制-粘貼”大法或者寫一堆重復(fù)的樣板代碼那不僅效率低下代碼也會(huì)變得臃腫且難以維護(hù)。Spring AOP面向切面編程就是為了解決這類“橫切關(guān)注點(diǎn)”問題而生的利器。它允許你將那些分散在應(yīng)用各處、與核心業(yè)務(wù)邏輯無關(guān)的公共行為如日志、事務(wù)、安全等模塊化形成一個(gè)獨(dú)立的“切面”然后通過聲明的方式將這些行為“織入”到指定的目標(biāo)方法中。簡(jiǎn)單說AOP讓你能“優(yōu)雅地”給代碼“打補(bǔ)丁”而不用侵入原有的業(yè)務(wù)邏輯。今天要聊的這幾個(gè)注解——Aspect、Pointcut、Before、Around、After、AfterReturning、AfterThrowing——就是Spring AOP這套“手術(shù)工具”的核心部件。它們共同定義了“在哪里切”切入點(diǎn)、“切了之后干什么”通知以及“怎么組織這些操作”切面。搞懂它們你就能從“被重復(fù)代碼折磨”的程序員進(jìn)階為“優(yōu)雅解耦”的架構(gòu)師。這篇文章我就結(jié)合自己踩過的坑和實(shí)戰(zhàn)經(jīng)驗(yàn)帶你徹底吃透這七個(gè)注解讓你不僅能看懂更能用得溜。2. 核心概念與設(shè)計(jì)思想拆解在直接上手寫代碼之前我們必須先理解AOP背后的幾個(gè)核心概念。這就像學(xué)開車你得先知道方向盤、油門、剎車是干嘛的而不是直接猛踩油門。2.1 橫切關(guān)注點(diǎn)與核心概念橫切關(guān)注點(diǎn)這個(gè)詞聽起來有點(diǎn)學(xué)術(shù)但其實(shí)很簡(jiǎn)單。它就是那些“貫穿”或“橫跨”多個(gè)模塊的功能需求。比如我們前面說的日志它可能需要在用戶模塊、訂單模塊、支付模塊的許多方法中被調(diào)用。這個(gè)“記錄日志”的需求就像一個(gè)刀片橫向切過了我們縱向分層Controller, Service, Dao的應(yīng)用程序。AOP的核心價(jià)值就是把這些“橫向”的代碼抽離出來集中管理。圍繞這個(gè)核心思想AOP定義了幾個(gè)關(guān)鍵角色切面 這就是抽離出來的那個(gè)“橫向”模塊的實(shí)體。它包含了通知和切入點(diǎn)。在Spring中一個(gè)用Aspect注解的類就是一個(gè)切面。你可以把它理解為一個(gè)“功能增強(qiáng)包”里面定義了要增強(qiáng)什么功能通知以及增強(qiáng)哪些地方切入點(diǎn)。連接點(diǎn) 這是程序執(zhí)行過程中一個(gè)明確的點(diǎn)比如方法調(diào)用、異常拋出、字段修改等。在Spring AOP中連接點(diǎn)特指方法的執(zhí)行。你可以理解為你的代碼里到處都是潛在的“手術(shù)點(diǎn)位”。切入點(diǎn) 這是一個(gè)謂詞表達(dá)式它用來匹配和篩選我們感興趣的連接點(diǎn)。簡(jiǎn)單說它定義了“在哪里切”。Pointcut注解就是用來定義這個(gè)表達(dá)式的。比如“所有com.example.service包下以save開頭的方法”就是一個(gè)切入點(diǎn)表達(dá)式。通知 這是切面在特定的切入點(diǎn)“處”執(zhí)行的動(dòng)作。也就是“切了之后要干什么”。Before、Around等注解就是用來標(biāo)記通知方法的。通知定義了增強(qiáng)行為的具體邏輯比如打印日志、開啟事務(wù)。它們之間的關(guān)系我畫個(gè)簡(jiǎn)單的類比你的應(yīng)用程序是一棟大樓核心業(yè)務(wù)連接點(diǎn)是每一扇門和窗戶方法。切入點(diǎn)是你手里的一張圖紙上面圈出了所有需要安裝防盜網(wǎng)的門窗例如所有朝外的窗戶。切面就是一個(gè)施工隊(duì)隊(duì)里既有圖紙切入點(diǎn)也有具體的施工方案和材料通知。通知就是具體的施工動(dòng)作比如“在安裝前測(cè)量尺寸”Before、“把舊窗戶拆下來裝上新窗戶再把舊窗戶處理掉”Around。2.2 Spring AOP的實(shí)現(xiàn)原理與能力邊界Spring AOP默認(rèn)使用動(dòng)態(tài)代理來實(shí)現(xiàn)。這很重要因?yàn)樗苯記Q定了Spring AOP的能力范圍和限制。對(duì)于實(shí)現(xiàn)了接口的類Spring會(huì)使用JDK動(dòng)態(tài)代理來創(chuàng)建代理對(duì)象。對(duì)于沒有實(shí)現(xiàn)接口的類Spring會(huì)使用CGLIB庫來生成目標(biāo)類的子類作為代理。這個(gè)代理對(duì)象會(huì)包裹著原始的目標(biāo)對(duì)象。當(dāng)你調(diào)用目標(biāo)方法時(shí)實(shí)際上調(diào)用的是代理對(duì)象的方法。代理對(duì)象在調(diào)用目標(biāo)方法的前后就有機(jī)會(huì)執(zhí)行我們定義的切面邏輯通知。注意正因?yàn)槭腔诖淼腟pring AOP只能攔截public方法的調(diào)用。對(duì)于同類內(nèi)部的方法調(diào)用即一個(gè)方法調(diào)用同一個(gè)類里的另一個(gè)方法由于調(diào)用者this是目標(biāo)對(duì)象本身而非代理對(duì)象切面是不會(huì)生效的。這是新手常踩的一個(gè)大坑。解決方案通常是將方法拆分到不同的類或者使用更底層的AspectJ。理解了這些我們?cè)偃タ茨瞧邆€(gè)注解就會(huì)清晰很多Aspect用來組建施工隊(duì)Pointcut用來畫施工圖紙其余五個(gè)Xxx注解則是具體的施工工序。3. 核心注解深度解析與實(shí)操要點(diǎn)接下來我們逐個(gè)拆解這七個(gè)核心注解。我會(huì)先講清楚每個(gè)注解是干什么的、怎么用然后立刻配上代碼示例和最重要的注意事項(xiàng)與實(shí)操心得。3.1 Aspect聲明切面組建你的“施工隊(duì)”Aspect注解用于聲明一個(gè)類是一個(gè)切面。Spring容器在掃描到帶有Aspect注解的類時(shí)就知道這個(gè)類里定義了一組橫切關(guān)注點(diǎn)的邏輯。import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; Aspect // 聲明這是一個(gè)切面類 Component // 同時(shí)需要讓Spring管理它的生命周期所以通常也加上Component或其衍生注解Service等 public class LoggingAspect { // 這里面會(huì)定義切入點(diǎn)和通知 }實(shí)操要點(diǎn)必須與Component等注解搭配使用 僅僅有AspectSpring是不會(huì)把它當(dāng)成一個(gè)Bean來創(chuàng)建和管理的。你必須同時(shí)加上Component、Service等注解或者通過Bean方法在配置類中顯式聲明。切面Bean默認(rèn)必須是單例的 切面通常是無狀態(tài)的Spring默認(rèn)將其作為單例Bean。如果你的切面中定義了有狀態(tài)的成員變量比如一個(gè)計(jì)數(shù)器并且你的應(yīng)用是多線程的那么你需要考慮線程安全問題或者調(diào)整Bean的作用域但這種情況很少見設(shè)計(jì)上應(yīng)避免。3.2 Pointcut定義切入點(diǎn)繪制精準(zhǔn)的“施工圖紙”Pointcut注解用于定義一個(gè)可重用的切入點(diǎn)表達(dá)式。它的值是一個(gè)AspectJ的切入點(diǎn)表達(dá)式。定義好的切入點(diǎn)可以通過方法名來引用。Aspect Component public class LoggingAspect { // 定義一個(gè)切入點(diǎn)匹配com.example.demo.service包及其子包下所有類的所有方法 Pointcut(execution(* com.example.demo.service..*.*(..))) public void serviceLayer() {} // 方法體通常為空它只是一個(gè)標(biāo)識(shí)符 // 定義另一個(gè)切入點(diǎn)匹配所有帶有Transactional注解的方法 Pointcut(annotation(org.springframework.transaction.annotation.Transactional)) public void transactionalMethod() {} }切入點(diǎn)表達(dá)式語法詳解 最常用的是execution指示器其語法為execution(修飾符? 返回類型 聲明類型? 方法名(參數(shù)列表) 異常類型?)其中?表示可選部分。* com.example.demo.service..*.*(..)解讀第一個(gè)* 匹配任意返回類型。com.example.demo.service.. 匹配service包及其所有子包。第二個(gè)* 匹配任意類名。第三個(gè)* 匹配任意方法名。(..) 匹配任意數(shù)量、任意類型的參數(shù)。其他常用指示器within 匹配指定類型內(nèi)的方法。如within(com.example.service.*)。annotation 匹配帶有指定注解的方法。如上例所示非常靈活。within 匹配帶有指定注解的類里的所有方法。bean Spring特有的匹配Bean名稱。如bean(*Service)匹配所有名字以Service結(jié)尾的Bean的所有方法。實(shí)操心得與避坑指南切入點(diǎn)表達(dá)式要盡可能精確 不要使用過于寬泛的表達(dá)式如execution(* *.*(..))匹配所有方法這會(huì)導(dǎo)致不必要的性能開銷和意料之外的攔截。精確匹配你的目標(biāo)包或注解。善用Pointcut組合 你可以使用與、||或、!非來組合多個(gè)切入點(diǎn)表達(dá)式實(shí)現(xiàn)更復(fù)雜的匹配邏輯。// 匹配service包下所有方法但排除掉get開頭的方法 Pointcut(execution(* com.example.service..*.*(..)) !execution(* com.example.service..*.get*(..))) public void serviceLayerExcludingGetters() {}將通用切入點(diǎn)定義在公共類 如果多個(gè)切面需要使用相同的切入點(diǎn)可以將其定義在一個(gè)公共的Aspect類中其他切面通過全限定名來引用例如Before(com.example.aspect.CommonPointcuts.serviceLayer())這有利于維護(hù)。3.3 Before前置通知在目標(biāo)方法“開工前”做準(zhǔn)備Before注解聲明一個(gè)前置通知。該通知會(huì)在匹配的切入點(diǎn)方法執(zhí)行之前運(yùn)行。它通常用于權(quán)限檢查、參數(shù)校驗(yàn)、日志記錄等場(chǎng)景。Aspect Component public class LoggingAspect { Pointcut(execution(* com.example.demo.service.*.*(..))) public void serviceMethod() {} Before(serviceMethod()) // 引用定義好的切入點(diǎn) public void logBefore(JoinPoint joinPoint) { // JoinPoint對(duì)象包含了當(dāng)前連接點(diǎn)的信息如方法名、參數(shù)、目標(biāo)對(duì)象等 String methodName joinPoint.getSignature().getName(); Object[] args joinPoint.getArgs(); System.out.println([前置日志] 準(zhǔn)備執(zhí)行方法: methodName , 參數(shù): Arrays.toString(args)); // 這里可以執(zhí)行參數(shù)校驗(yàn)如果校驗(yàn)失敗可以拋出異常來阻止目標(biāo)方法執(zhí)行 // if (args[0] null) { // throw new IllegalArgumentException(參數(shù)不能為空); // } } }關(guān)鍵對(duì)象JoinPoint 任何通知方法除了Around的第一個(gè)參數(shù)都可以聲明為JoinPoint類型用于獲取連接點(diǎn)的上下文信息。Around通知的參數(shù)是ProceedingJoinPoint它是JoinPoint的子類。注意事項(xiàng)無法阻止方法執(zhí)行除非拋異常Before通知不能直接返回一個(gè)值來替代目標(biāo)方法的執(zhí)行。如果你想在某種條件下完全阻止目標(biāo)方法運(yùn)行只能在Before通知里拋出異常。更精細(xì)的控制如替換返回值、修改參數(shù)需要使用Around通知。執(zhí)行順序 如果多個(gè)切面都匹配了同一個(gè)切入點(diǎn)它們的Before通知執(zhí)行順序可以通過Order注解或?qū)崿F(xiàn)Ordered接口來控制數(shù)字小的先執(zhí)行。3.4 After后置通知在目標(biāo)方法“結(jié)束后”進(jìn)行清理After注解聲明一個(gè)后置通知。該通知會(huì)在匹配的切入點(diǎn)方法執(zhí)行之后運(yùn)行無論方法是正常返回還是拋出異常。它類似于try-finally塊中的finally部分常用于資源清理、釋放鎖等場(chǎng)景。Aspect Component public class ResourceCleanupAspect { Pointcut(execution(* com.example.demo.service.FileService.*(..))) public void fileServiceMethod() {} After(fileServiceMethod()) public void cleanupResource(JoinPoint joinPoint) { String methodName joinPoint.getSignature().getName(); System.out.println([資源清理] 方法 methodName 執(zhí)行完畢正在清理臨時(shí)文件或關(guān)閉流...); // 模擬清理操作在實(shí)際應(yīng)用中可能是關(guān)閉IO流、數(shù)據(jù)庫連接、刪除臨時(shí)文件等 // cleanupTemporaryFiles(); } }重要提示After通知無法知道目標(biāo)方法是成功返回還是拋出了異常。如果你需要根據(jù)執(zhí)行結(jié)果進(jìn)行不同的處理應(yīng)該使用AfterReturning或AfterThrowing。3.5 AfterReturning返回后通知只在“成功”時(shí)慶祝AfterReturning注解聲明一個(gè)返回后通知。該通知只在匹配的切入點(diǎn)方法正常執(zhí)行并返回后運(yùn)行。它常用于記錄成功的操作日志、緩存方法返回結(jié)果、發(fā)送成功通知等。Aspect Component public class MonitoringAspect { Pointcut(execution(* com.example.demo.service.OrderService.createOrder(..))) public void createOrderMethod() {} AfterReturning( pointcut createOrderMethod(), returning result // 綁定目標(biāo)方法的返回值到通知方法的result參數(shù) ) public void logSuccess(JoinPoint joinPoint, Object result) { // 返回值類型可以聲明為具體類型如Order String methodName joinPoint.getSignature().getName(); System.out.println([成功監(jiān)控] 方法 methodName 執(zhí)行成功返回結(jié)果: result); // 可以將結(jié)果存入緩存或發(fā)送MQ消息 // cacheService.put(order_key, result); // mqProducer.sendSuccessMessage(result); } }核心參數(shù)returningreturning屬性的值必須與通知方法中對(duì)應(yīng)的參數(shù)名一致。通過它我們可以在通知方法里訪問到目標(biāo)方法的返回值。你可以將參數(shù)類型聲明得更具體如Order result這樣Spring會(huì)做類型檢查如果類型不匹配該通知將不會(huì)被執(zhí)行。實(shí)操心得可以修改返回值嗎理論上在AfterReturning通知中修改result對(duì)象的內(nèi)容是可能的如果它是可變對(duì)象。例如如果返回的是一個(gè)Map或List你可以往里面添加元素。但是你不能替換這個(gè)返回值對(duì)象本身即讓方法返回一個(gè)全新的對(duì)象。如果你需要替換返回值必須使用Around通知。3.6 AfterThrowing異常通知只在“失敗”時(shí)處理AfterThrowing注解聲明一個(gè)異常通知。該通知只在匹配的切入點(diǎn)方法拋出異常后運(yùn)行。它非常適合用于統(tǒng)一的異常日志記錄、錯(cuò)誤報(bào)警、事務(wù)回滾標(biāo)記注意聲明式事務(wù)通常由Transactional處理這里指其他資源操作等。Aspect Component public class ExceptionHandlingAspect { Pointcut(execution(* com.example.demo.service.*.*(..))) public void serviceMethod() {} AfterThrowing( pointcut serviceMethod(), throwing ex // 綁定拋出的異常到通知方法的ex參數(shù) ) public void logException(JoinPoint joinPoint, Exception ex) { // 可以指定具體的異常類型如NullPointerException String methodName joinPoint.getSignature().getName(); String errorMsg String.format([異常警報(bào)] 服務(wù)層方法 %s 執(zhí)行失敗異常類型: %s, 異常信息: %s, methodName, ex.getClass().getSimpleName(), ex.getMessage()); System.err.println(errorMsg); // 發(fā)送報(bào)警郵件、短信或?qū)懭脲e(cuò)誤監(jiān)控平臺(tái) // alertService.sendToDingTalk(errorMsg); // 注意這里不能“處理”掉異常異常仍然會(huì)向上傳播 } }核心參數(shù)throwing 與returning類似throwing屬性的值必須與通知方法中對(duì)應(yīng)的參數(shù)名一致。你可以指定具體的異常類型如NullPointerException ex這樣該通知就只捕獲指定類型的異常。重要限制無法“吞掉”異常AfterThrowing通知執(zhí)行完畢后原始異常仍然會(huì)繼續(xù)向上拋出。你不能在這里通過catch來“處理”掉異常并讓程序正常往下走。它的定位是“異常發(fā)生后的附加處理”而不是“異常處理”。如果你需要改變異常傳播行為如轉(zhuǎn)換異常類型、吞掉特定異常必須使用Around通知。3.7 Around環(huán)繞通知功能最強(qiáng)大的“總指揮”Around注解聲明一個(gè)環(huán)繞通知。它是功能最強(qiáng)大的通知類型因?yàn)樗耆刂屏四繕?biāo)方法的調(diào)用過程。你可以在目標(biāo)方法執(zhí)行前、后插入邏輯甚至可以決定是否執(zhí)行目標(biāo)方法以及修改目標(biāo)方法的參數(shù)和返回值。Aspect Component public class PerformanceAspect { Pointcut(annotation(com.example.demo.annotation.MonitorPerformance)) public void performanceMonitorPointcut() {} Around(performanceMonitorPointcut()) public Object measureExecutionTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime System.currentTimeMillis(); String methodName proceedingJoinPoint.getSignature().getName(); Object result; try { // 1. 前置處理這里可以修改參數(shù) Object[] args proceedingJoinPoint.getArgs(); // 可以對(duì)args進(jìn)行修改例如解密、格式化等 // args[0] someProcessing(args[0]); // 2. 決定是否執(zhí)行以及何時(shí)執(zhí)行目標(biāo)方法 // proceed()方法就是調(diào)用目標(biāo)方法它的返回值就是目標(biāo)方法的返回值 result proceedingJoinPoint.proceed(args); // 可以傳入修改后的參數(shù) // 3. 后置處理這里可以修改返回值 // result somePostProcessing(result); } finally { // 4. 最終處理無論成功失敗都會(huì)執(zhí)行適合做資源清理但注意它與After的區(qū)別 long endTime System.currentTimeMillis(); long duration endTime - startTime; System.out.println([性能監(jiān)控] 方法 methodName 執(zhí)行耗時(shí): duration ms); if (duration 1000) { System.out.println([性能警告] 方法 methodName 執(zhí)行過慢); } } // 5. 返回結(jié)果 return result; } }核心對(duì)象ProceedingJoinPoint 環(huán)繞通知方法的參數(shù)必須是ProceedingJoinPoint它是JoinPoint的子類。它的proceed()方法是整個(gè)環(huán)繞通知的核心調(diào)用它才會(huì)執(zhí)行目標(biāo)方法。環(huán)繞通知的完整控制權(quán)不調(diào)用proceed() 如果你永遠(yuǎn)不調(diào)用proceedingJoinPoint.proceed()那么目標(biāo)方法就根本不會(huì)執(zhí)行。你可以直接返回一個(gè)自定義的結(jié)果。這可以用于實(shí)現(xiàn)緩存、熔斷、Mock等高級(jí)功能。調(diào)用多次proceed() 理論上你可以調(diào)用多次但這通常不是好主意除非你有特殊需求如重試邏輯。修改參數(shù)和返回值 你可以修改傳入proceed()方法的參數(shù)數(shù)組也可以修改proceed()返回的結(jié)果對(duì)象。注意事項(xiàng)與最佳實(shí)踐務(wù)必在finally塊中處理耗時(shí)等最終邏輯 像計(jì)算耗時(shí)、資源清理這類無論成功失敗都要執(zhí)行的邏輯應(yīng)該放在try-finally的finally塊中。如果放在proceed()調(diào)用之后一旦目標(biāo)方法拋出異常這部分邏輯就不會(huì)執(zhí)行。謹(jǐn)慎使用 能力越大責(zé)任越大。Around太過強(qiáng)大濫用會(huì)導(dǎo)致程序流程難以理解。如果Before、AfterReturning等能滿足需求就優(yōu)先使用它們代碼更清晰。處理異常 環(huán)繞通知需要聲明拋出Throwable因?yàn)閜roceed()方法會(huì)拋出它。你可以在通知內(nèi)部try-catch異常進(jìn)行轉(zhuǎn)換或處理然后選擇是否重新拋出。4. 綜合實(shí)戰(zhàn)構(gòu)建一個(gè)完整的日志與監(jiān)控切面光說不練假把式?,F(xiàn)在我們把所有注解組合起來實(shí)現(xiàn)一個(gè)在企業(yè)級(jí)應(yīng)用中非常實(shí)用的綜合切面它記錄方法入?yún)?、出參、耗時(shí)并監(jiān)控異常。package com.example.demo.aspect; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; Aspect Component Slf4j // 使用Lombok的Slf4j注解自動(dòng)生成log對(duì)象 public class ComprehensiveLoggingAspect { private final ObjectMapper objectMapper new ObjectMapper(); /** * 切入點(diǎn)匹配所有Controller層的方法。 * 這里使用within匹配帶有RestController注解的類下的所有方法。 * 你也可以用execution(* com.example.demo.controller..*.*(..)) */ Pointcut(within(org.springframework.web.bind.annotation.RestController)) public void controllerLayer() {} /** * 環(huán)繞通知記錄完整的請(qǐng)求-響應(yīng)過程及耗時(shí)。 * 這是最核心的日志記錄點(diǎn)。 */ Around(controllerLayer()) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long startTime System.currentTimeMillis(); // 獲取請(qǐng)求信息在Web上下文中 String requestInfo ; try { ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes ! null) { HttpServletRequest request attributes.getRequest(); requestInfo String.format([URL: %s, Method: %s, IP: %s], request.getRequestURL().toString(), request.getMethod(), request.getRemoteAddr()); } } catch (Exception e) { // 非Web環(huán)境或上下文不可用忽略 } String className pjp.getTarget().getClass().getSimpleName(); String methodName pjp.getSignature().getName(); Object[] args pjp.getArgs(); // 記錄入?yún)⒆⒁馍a(chǎn)環(huán)境可能需要對(duì)敏感參數(shù)如密碼進(jìn)行脫敏 try { log.info({} -- {}.{} 方法開始執(zhí)行入?yún)? {}, requestInfo, className, methodName, Arrays.toString(args)); // 使用JSON序列化更清晰但注意性能和大對(duì)象 // log.info(入?yún)⒃斍? {}, objectMapper.writeValueAsString(args)); } catch (Exception e) { log.warn(記錄入?yún)⑷罩緯r(shí)發(fā)生序列化異常, e); } Object result; try { // 執(zhí)行目標(biāo)方法 result pjp.proceed(); long endTime System.currentTimeMillis(); long duration endTime - startTime; // 記錄出參和耗時(shí)同樣注意敏感信息脫敏 try { log.info({} -- {}.{} 方法執(zhí)行成功耗時(shí): {}ms, 返回結(jié)果: {}, requestInfo, className, methodName, duration, result); } catch (Exception e) { log.warn(記錄出參日志時(shí)發(fā)生序列化異常, e); } // 慢查詢警告 if (duration 1000) { log.warn({} {}.{} 方法執(zhí)行過慢耗時(shí): {}ms, requestInfo, className, methodName, duration); } } catch (Throwable throwable) { // 記錄異常和耗時(shí) long endTime System.currentTimeMillis(); long duration endTime - startTime; log.error({} !!! {}.{} 方法執(zhí)行異常耗時(shí): {}ms異常信息: , requestInfo, className, methodName, duration, throwable); // 異常繼續(xù)向上拋出由全局異常處理器處理 throw throwable; } return result; } /** * 返回后通知專門用于業(yè)務(wù)成功后的特定處理例如發(fā)送審計(jì)事件。 * 此處的處理不應(yīng)影響主流程通常異步執(zhí)行。 */ AfterReturning(pointcut controllerLayer(), returning result) public void auditLogOnSuccess(JoinPoint joinPoint, Object result) { // 例如將關(guān)鍵操作記錄到審計(jì)日志表或發(fā)送到消息隊(duì)列 // auditService.logOperation(joinPoint, result, SUCCESS); // log.debug(審計(jì)日志記錄成功異步處理); } /** * 異常通知專門用于業(yè)務(wù)異常后的特定處理例如更新監(jiān)控狀態(tài)。 * 注意異常日志已在Around中記錄此處做額外處理。 */ AfterThrowing(pointcut controllerLayer(), throwing ex) public void monitorOnFailure(JoinPoint joinPoint, Exception ex) { // 例如增加錯(cuò)誤計(jì)數(shù)器用于熔斷器或健康檢查 // metricsService.incrementErrorCounter(joinPoint.getSignature().toShortString()); // log.debug(錯(cuò)誤監(jiān)控狀態(tài)已更新); } }這個(gè)實(shí)戰(zhàn)案例的要點(diǎn)解析切入點(diǎn)設(shè)計(jì) 我們使用within來匹配所有RestController下的方法這比用execution表達(dá)式更簡(jiǎn)潔且能自動(dòng)適配所有Controller。日志脫敏 在記錄入?yún)⒑统鰠r(shí)我特別加了注釋提醒敏感信息脫敏。在實(shí)際生產(chǎn)環(huán)境中像密碼、身份證號(hào)、手機(jī)號(hào)等字段絕對(duì)不能直接打印到日志里。你需要編寫自定義的序列化器或是在切面中過濾/替換這些字段。性能考量 使用ObjectMapper將對(duì)象序列化成JSON字符串再打印雖然清晰但對(duì)于大對(duì)象或高頻調(diào)用方法會(huì)有性能損耗。生產(chǎn)環(huán)境需要權(quán)衡有時(shí)只記錄關(guān)鍵字段或類名即可。異常處理 在Around中我們用try-catch包裹了proceed()這樣就能在方法拋出異常時(shí)依然記錄下耗時(shí)并把異常日志級(jí)別定為error。最后再將原異常throw出去保證業(yè)務(wù)異常的正常傳播。職責(zé)分離 我們用了Around做核心日志用AfterReturning和AfterThrowing做額外的成功/失敗后處理如審計(jì)、監(jiān)控。這樣的分離使得切面邏輯更清晰每個(gè)通知各司其職。Web上下文 通過RequestContextHolder獲取當(dāng)前HTTP請(qǐng)求信息這在Web環(huán)境下非常有用。但要注意在非Web上下文如定時(shí)任務(wù)、MQ監(jiān)聽器中調(diào)用時(shí)這部分代碼會(huì)獲取不到request所以需要做好判空處理。5. 高級(jí)技巧、常見問題與排查實(shí)錄掌握了基本用法我們來看看一些進(jìn)階場(chǎng)景和那些年我踩過的坑。5.1 通知執(zhí)行順序當(dāng)多個(gè)切面命中同一個(gè)方法如果一個(gè)方法被多個(gè)切面的多個(gè)通知匹配執(zhí)行順序由兩點(diǎn)決定切面級(jí)別的順序 在切面類上使用Order注解或讓切面類實(shí)現(xiàn)Ordered接口。數(shù)字越小優(yōu)先級(jí)越高越先執(zhí)行。通知類型的默認(rèn)順序 在同一個(gè)切面內(nèi)如果同一個(gè)切入點(diǎn)有多個(gè)不同類型的通知執(zhí)行順序是Around通知的開始部分Before通知目標(biāo)方法執(zhí)行Around通知的結(jié)束部分如果目標(biāo)方法成功執(zhí)行After通知無論成功失敗AfterReturning通知僅成功或AfterThrowing通知僅失敗一個(gè)常見的坑 你定義了一個(gè)事務(wù)切面Around和一個(gè)日志切面Before/After希望日志在事務(wù)內(nèi)外都能記錄。如果事務(wù)切面順序更高那么日志切面實(shí)際上記錄的是事務(wù)代理方法內(nèi)部的執(zhí)行情況。通常我們會(huì)把事務(wù)切面的順序設(shè)得較高Order(Ordered.LOWEST_PRECEDENCE - 1)確保它在最內(nèi)層執(zhí)行。5.2 同類內(nèi)部方法調(diào)用導(dǎo)致AOP失效這是Spring AOP基于代理機(jī)制帶來的最經(jīng)典問題。Service public class MyService { public void outerMethod() { System.out.println(outerMethod called); this.innerMethod(); // 這里調(diào)用的是this即目標(biāo)對(duì)象本身不是代理對(duì)象 } Transactional // 假設(shè)這里有個(gè)事務(wù)注解 public void innerMethod() { System.out.println(innerMethod called); } }當(dāng)你調(diào)用myService.outerMethod()時(shí)innerMethod()上的Transactional其本質(zhì)也是一個(gè)AOP不會(huì)生效。解決方案推薦重構(gòu)代碼 將innerMethod抽取到另一個(gè)Service中然后通過注入調(diào)用。這樣調(diào)用就通過代理進(jìn)行了。自注入 在MyService中注入自己Autowired private MyService self然后通過self.innerMethod()調(diào)用。Spring會(huì)注入代理對(duì)象。但這種方法有點(diǎn)“黑魔法”影響代碼可讀性。使用AspectJ 將Spring AOP的代理模式切換為AspectJ的編譯時(shí)或加載時(shí)織入LTW這樣可以實(shí)現(xiàn)真正的字節(jié)碼增強(qiáng)解決內(nèi)部調(diào)用問題。但配置更復(fù)雜。5.3 切入點(diǎn)表達(dá)式匹配不上的排查步驟你寫好了切面但日志就是不打印按以下步驟排查檢查Bean是否被Spring管理 你的切面類有Component嗎目標(biāo)類被攔截的類是一個(gè)Spring Bean嗎有Service,Controller等注解嗎檢查包掃描路徑 切面類和目標(biāo)類所在的包都在Spring的組件掃描范圍內(nèi)嗎檢查切入點(diǎn)表達(dá)式使用execution(* com.example..*.*(..))這種最寬泛的表達(dá)式試一下如果生效了說明是你的原表達(dá)式寫得太窄。仔細(xì)核對(duì)包名、類名、方法名是否有拼寫錯(cuò)誤。注意訪問修飾符。execution(public * ..*(..))只匹配public方法。檢查同類調(diào)用問題 你是在外部調(diào)用還是在類的另一個(gè)方法內(nèi)部調(diào)用開啟Spring AOP調(diào)試日志 在application.properties中設(shè)置logging.level.org.springframework.aopDEBUGSpring會(huì)打印詳細(xì)的代理創(chuàng)建和匹配信息。5.4 性能優(yōu)化與生產(chǎn)建議切入點(diǎn)表達(dá)式要精確 避免使用*.*(..)。盡量限定到具體的包、類或注解。在環(huán)繞通知中謹(jǐn)慎進(jìn)行IO操作和序列化 像上面的JSON序列化日志在高并發(fā)場(chǎng)景可能成為瓶頸??紤]使用異步日志如Logback的AsyncAppender或采樣記錄。避免在切面中執(zhí)行耗時(shí)或不可靠的遠(yuǎn)程調(diào)用 例如不要在Before通知里調(diào)用一個(gè)慢速的身份驗(yàn)證服務(wù)這會(huì)拖慢所有業(yè)務(wù)方法??紤]異步或緩存。為切面編寫單元測(cè)試 切面也是代碼需要用測(cè)試來保證其正確性。你可以使用Spring Boot Test來啟動(dòng)一個(gè)輕量級(jí)上下文測(cè)試你的切面是否按預(yù)期織入。Spring AOP的這些注解就像一套精密的瑞士軍刀每把都有其特定的用途。Pointcut是定位器Before、After等是執(zhí)行不同階段任務(wù)的工具而Around是那個(gè)功能最全的鉗子。理解它們的本質(zhì)——基于代理的攔截——是避免踩坑的關(guān)鍵。從簡(jiǎn)單的日志記錄開始逐步嘗試事務(wù)管理、緩存、權(quán)限校驗(yàn)等復(fù)雜場(chǎng)景你會(huì)越來越體會(huì)到AOP讓代碼變得清晰、簡(jiǎn)潔和強(qiáng)大的魔力。記住最好的學(xué)習(xí)方式就是動(dòng)手創(chuàng)建一個(gè)測(cè)試項(xiàng)目把上面每個(gè)例子都敲一遍再試著改造一下你現(xiàn)有項(xiàng)目中的重復(fù)代碼感受一下“切面”帶來的整潔與高效。

相關(guān)新聞

5分鐘徹底解決Mac鼠標(biāo)滾輪卡頓:Mos平滑滾動(dòng)工具的完整指南

5分鐘徹底解決Mac鼠標(biāo)滾輪卡頓:Mos平滑滾動(dòng)工具的完整指南

5分鐘徹底解決Mac鼠標(biāo)滾輪卡頓:Mos平滑滾動(dòng)工具的完整指南 【免費(fèi)下載鏈接】Mos 一個(gè)用于在 macOS 上平滑你的鼠標(biāo)滾動(dòng)效果或單獨(dú)設(shè)置滾動(dòng)方向的小工具, 讓你的滾輪爽如觸控板 | A lightweight tool used to smooth scrolling and set scroll direction independent…

2026/8/2 8:15:18 閱讀更多
社區(qū)網(wǎng)格化居民服務(wù)管理系統(tǒng)源碼 Java+SpringBoot+Vue3 前后分離

社區(qū)網(wǎng)格化居民服務(wù)管理系統(tǒng)源碼 Java+SpringBoot+Vue3 前后分離

一、關(guān)鍵詞社區(qū)網(wǎng)格化居民服務(wù)管理系統(tǒng),城鄉(xiāng)社區(qū)網(wǎng)格化便民服務(wù)管理系統(tǒng),社區(qū)網(wǎng)格居民綜合服務(wù)管理平臺(tái)二、作品包含源碼數(shù)據(jù)庫全套環(huán)境和工具資源本地部署教程三、項(xiàng)目技術(shù)前端技術(shù):Html、Css、Js、Vue3.0、Element-plus后端技術(shù)&#xff1a…

2026/8/2 8:15:18 閱讀更多
普本生進(jìn)廠指南:從技術(shù)員到工程師的制造業(yè)職業(yè)進(jìn)階路徑

普本生進(jìn)廠指南:從技術(shù)員到工程師的制造業(yè)職業(yè)進(jìn)階路徑

1. 從“進(jìn)廠打螺絲”說起:一個(gè)被誤解的職業(yè)起點(diǎn)“普本畢業(yè),準(zhǔn)備進(jìn)廠打螺絲了!!!”——這句話在社交媒體上,常常帶著一絲自嘲、無奈,甚至是對(duì)未來的迷茫。作為一個(gè)在制造業(yè)一線摸爬滾打多年的“過…

2026/8/2 9:35:20 閱讀更多
IPv6折騰記——光貓?jiān)O(shè)置

IPv6折騰記——光貓?jiān)O(shè)置

書接上文,我們可以ping通家里的網(wǎng)絡(luò)設(shè)備,但是依舊無法訪問家里的網(wǎng)絡(luò)設(shè)備(比如NAS,監(jiān)控設(shè)備),因?yàn)樗麄儌鬏斔玫膮f(xié)議用的是TCP/UDP。而這兩種協(xié)議會(huì)被光貓或者路由器的防火墻所攔截到,傳輸不到…

2026/8/2 9:35:20 閱讀更多
Godot狀態(tài)圖開發(fā)實(shí)戰(zhàn):從概念到應(yīng)用,解決復(fù)雜狀態(tài)管理難題

Godot狀態(tài)圖開發(fā)實(shí)戰(zhàn):從概念到應(yīng)用,解決復(fù)雜狀態(tài)管理難題

1. 項(xiàng)目概述:為什么我們需要State Charts?如果你正在用Godot做游戲,尤其是涉及到角色行為、UI流程或者任何有復(fù)雜狀態(tài)切換的邏輯,那你大概率已經(jīng)體會(huì)過傳統(tǒng)狀態(tài)機(jī)(Finite State Machine, FSM)的痛點(diǎn)了。狀態(tài)…

2026/8/2 9:35:20 閱讀更多
TPFanCtrl2 v2.3.3雙風(fēng)扇嵌入式控制器深度配置指南

TPFanCtrl2 v2.3.3雙風(fēng)扇嵌入式控制器深度配置指南

TPFanCtrl2 v2.3.3雙風(fēng)扇嵌入式控制器深度配置指南 【免費(fèi)下載鏈接】TPFanCtrl2 ThinkPad Fan Control 2 (Dual Fan) for Windows 10 and 11 項(xiàng)目地址: https://gitcode.com/gh_mirrors/tp/TPFanCtrl2 TPFanCtrl2是一款基于Windows平臺(tái)的ThinkPad筆記本風(fēng)扇控制解決方案…

2026/8/2 9:35:20 閱讀更多
MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案 【免費(fèi)下載鏈接】MoneyPrinterPlus AI一鍵批量生成各類短視頻,自動(dòng)批量混剪短視頻,自動(dòng)把視頻發(fā)布到抖音,快手,小紅書,視頻號(hào)上,賺錢從來沒有這么容易過! 支持本地語音模型chatTTS,fasterwhisper,…

2026/8/2 0:04:00 閱讀更多
3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南 【免費(fèi)下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說說 項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過,那些年發(fā)過的QQ空間說說,那些記錄青春的文字…

2026/8/2 0:04:01 閱讀更多
MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案 【免費(fèi)下載鏈接】MoneyPrinterPlus AI一鍵批量生成各類短視頻,自動(dòng)批量混剪短視頻,自動(dòng)把視頻發(fā)布到抖音,快手,小紅書,視頻號(hào)上,賺錢從來沒有這么容易過! 支持本地語音模型chatTTS,fasterwhisper,…

2026/8/2 0:04:00 閱讀更多
3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南 【免費(fèi)下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說說 項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過,那些年發(fā)過的QQ空間說說,那些記錄青春的文字…

2026/8/2 0:04:01 閱讀更多
AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O分配PCB板是應(yīng)用材料(Applied Materials)公司生產(chǎn)的一款用于半導(dǎo)體設(shè)備的I/O信號(hào)分配電路板。該型號(hào)(0100-02186)的核心特點(diǎn)如下:專用于Endura等半導(dǎo)體工藝腔室。集成信號(hào)路由與分配功能。連接控制…

2026/8/2 2:51:21 閱讀更多
Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)是日本日清(Nissei)品牌的一款工業(yè)用三相異步電機(jī),適用于自動(dòng)化設(shè)備及通用機(jī)械驅(qū)動(dòng)。該型號(hào)(FFMN-32L-10-T0 40AX)的核心特點(diǎn)如下:三相交流異步電動(dòng)機(jī)。額定…

2026/8/2 2:52:49 閱讀更多