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

相關新聞

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

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

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

2026/8/2 8:15:18 閱讀更多
ODYSSEY-X86J41x5 GPIO控制實戰(zhàn):從硬件映射到Python編程

ODYSSEY-X86J41x5 GPIO控制實戰(zhàn):從硬件映射到Python編程

1. 項目概述:解鎖單板計算機的物理世界接口如果你手頭有一塊 ODYSSEY - X86J41x5 這樣的高性能 x86 架構單板計算機,除了用它跑服務器、做軟路由或者當個迷你主機,有沒有想過讓它也能像樹莓派那樣,直接操控LED燈、讀取傳感器數(shù)據(jù)、…

2026/8/2 10:35:22 閱讀更多
一臺Mac也能跑Kimi K3!128GB內(nèi)存硬塞1.6TB權重

一臺Mac也能跑Kimi K3!128GB內(nèi)存硬塞1.6TB權重

Kimi K3 開放完整權重后,不少人都躍躍欲試。2.8 萬億參數(shù)、官方 MXFP4 權重,擁有了這些,你就可以自己搗鼓這個規(guī)模巨大的模型,可以部署到本地?,F(xiàn)實情況真有這么簡單嗎?有人總結,運行 Kimi K3 其實很容易&a…

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

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

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

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

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

3分鐘搞定!QQ空間歷史說說完整備份終極指南 【免費下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說說 項目地址: 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板是應用材料(Applied Materials)公司生產(chǎn)的一款用于半導體設備的I/O信號分配電路板。該型號(0100-02186)的核心特點如下:專用于Endura等半導體工藝腔室。集成信號路由與分配功能。連接控制…

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

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動機

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

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