文章目錄
- 攔截器快速入門
- 攔截器詳解
- 攔截路徑
- 攔截器執行流程
- 全局控制器增強機制(@ControllerAdvice)
- 統一數據返回格式(@ControllerAdvice + ResponseBodyAdvice)
- ??全局異常處理機制??(@ControllerAdvice + @ExceptionHandler)
- 全局數據預處理(@ControllerAdvice + @InitBinder)
- AOP
- Spring AOP 快速入門
- Spring AOP 詳解
- Spring AOP 核心概念
- 通知類型
- @PointCut
- 切面優先級(@Order)
- 切點表達式
- Spring AOP 原理
- 靜態代理
- 動態代理
攔截器、@ControllerAdvice、Spring AOP 都是 Spring 中實現 “統一功能處理” 的工具,但它們是從不同層面解決問題:
- 攔截器專注于 HTTP 請求生命周期的增強(如登錄驗證、日志記錄)
- @ControllerAdvice 專門針對控制器層的統一增強
- Spring AOP 是最底層、最通用的統一處理方案,覆蓋全應用, 更適合對普通 Bean 的方法進行橫切增強
三者均通過抽取橫切關注點并進行統一處理,既實現了代碼復用(避免重復開發),又完成了業務邏輯與通用功能的解耦,其核心優勢在于無侵入性增強—— 在程序運行期間,無需修改原有業務代碼。最終簡化開發流程并顯著提升系統的可維護性
攔截器快速入門
攔截器是 Spring MVC 提供的核心功能,用于攔截 Web 請求,允許在請求處理前后執行預定義邏輯,甚至拒絕請求。它是 Spring MVC 框架層面對 AOP 思想的實現,但其底層不依賴 Spring AOP 的動態代理機制,而是基于 DispatcherServlet 的請求處理鏈和 Java 反射實現
攔截器的使用步驟分為三步:
- 定義攔截器(具體做什么):實現 HandlerInterceptor 接口,重寫 preHandle(請求處理前)、postHandle(請求處理后視圖渲染前)、afterCompletion(整個請求完成后),編寫具體攔截邏輯
- 注冊攔截器(注冊到框架中):創建配置類實現 WebMvcConfigurer 接口,重寫 addInterceptors 方法,通過 InterceptorRegistry 注冊自定義攔截器
- 配置攔截規則(攔截哪些請求):在注冊時通過 addPathPatterns 定義需要攔截的請求路徑,通過 excludePathPatterns 定義需要排除的請求路徑(如靜態資源、登錄頁等)
示例
自定義攔截器:實現 HandlerInterceptor 接口,并重寫其所有方法
@Slf4j
@Component
public class UserInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("UserInterceptor 目標方法執行前執行..");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("UserInterceptor 目標方法執行后執行");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("UserInterceptor 視圖渲染完畢后執行,最后執行");}
}
- preHandle():在目標方法(Controller 方法)執行前被調用。返回 true 表示允許請求繼續向下執行(會調用后續攔截器和目標方法);返回 false 則會中斷請求流程,后續操作(包括其他攔截器和目標方法)都不會執行
- postHandle():在目標方法執行完成后、視圖渲染之前被調用。此時可以對模型數據或視圖進行修改
- afterCompletion():在整個請求處理完成(視圖渲染完畢)后執行,是攔截器中最后執行的方法,通常用于資源清理等操作
注冊配置攔截器:實現 WebMvcConfigurer 接口,并重寫 addInterceptors 方法
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate UserInterceptor userInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(userInterceptor) //注冊自定義攔截器對象.addPathPatterns("/**"); //設置攔截器攔截的請求路徑(/** 表示攔截所有請求)}
}
啟動服務,試試訪問任意請求,觀察后端日志
@Slf4j
@RestController
public class UserController {@RequestMapping("/sayHi")public String sayHi() {log.info("打印日志");return "Hi";}
}
可以看到 preHandle 方法執行之后放行了,開始執行目標方法,目標方法執行完后執行 postHandle 和 afterCompletion 方法
我們把攔截器中 preHandle 方法的返回值改為 false,再觀察運行結果
可以看到,攔截器攔截了請求,沒有進行響應
攔截器詳解
接下來介紹攔截器的使用細節。主要介紹兩個部分:
- 配置攔截路徑
- 攔截器執行流程
攔截路徑
攔截路徑是指:我們定義的這個攔截器對哪些請求生效
我們可以通過 addPathPatterns () 方法指定要攔截哪些請求,也可以通過excludePathPatterns () 指定不攔截哪些請求,excludePathPatterns 的優先級高于 addPathPatterns
除了可以設置 /** 匹配所有請求外,還有一些常見的攔截路徑設置:
攔截路徑 | 含義 | 舉例 |
---|---|---|
/* | 僅匹配一級路徑 | 能匹配/user,/book,/login,不能匹配/user/login |
/** | 能匹配任意層級路徑 | 能匹配/user,/user/login,/book/addBook/1 |
/user/* | 匹配/user下的一級子路徑 | 能匹配/user/addUser,不能匹配/user/addUser/1,/user |
/user/** | 匹配/user下的任意級路徑 | 能匹配/user(包括自身),/user/addBook,/user/addUser/1 |
上述代碼中,我們配置的是 /** ,表示攔截所有的請求。如果有用戶登錄接口,我們希望可以排除它不被攔截,還有此項目的靜態文件 (圖片文件,JS 和 CSS 等文件)也得排除,不然頁面樣式亂了、交互沒了,頁面沒法看了
示例(登錄校驗)
- 定義攔截器
如果能從 Session 中獲取用戶信息,返回true;否則返回false, 并設置 http 狀態碼為 401
@Component
public class UserInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute(Constants.SESSION_USER_KEY) != null) {// Session 存在且用戶標識屬性存在,放行return true;}// 未登錄,設置響應狀態碼為 401(未授權),不放行response.setStatus(401);return false;}
}
- 注冊配置攔截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate UserInterceptor userInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(userInterceptor) //注冊自定義攔截器對象.addPathPatterns("/**") //攔截所有請求.excludePathPatterns("/user/login", //排除用戶登錄接口"/static/**"); //排除靜態資源}// // 另一種寫法
// registry.addInterceptor(userInterceptor)
// .addPathPatterns("/**")
// .excludePathPatterns("/user/login")
// .excludePathPatterns("/static/**");
}
也可以改成
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate UserInterceptor userInterceptor;private List<String> excludePaths = Arrays.asList("/user/login","/static/**");@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(userInterceptor) //注冊自定義攔截器對象.addPathPatterns("/**") //攔截所有請求.excludePathPatterns(excludePaths); //排除攔截的路徑}
}
攔截器執行流程
觀察第一次有請求進入應用時打印的日志:
Spring MVC 中請求處理與攔截器的執行流程由 DispatcherServlet 統一調度,整體順序如下:
- 請求發起:前端發送接口調用等請求,首先抵達 DispatcherServlet,由其通過 doDispatch 方法啟動調度流程
- 攔截器 preHandle:在 Controller 處理請求前,按攔截器注冊順序依次執行 preHandle 方法(前置處理)。該方法用于登錄校驗、權限檢查等前置邏輯,返回 false 則中斷后續流程(包括 Controller),直接進入后續清理步驟;返回 true 則繼續執行
- Controller 業務處理:所有攔截器 preHandle 均通過后,Controller 開始處理具體業務,調用 Service、Mapper 等完成數據操作
- 攔截器 postHandle:Controller 處理完成但未返回響應前,按攔截器注冊的逆序執行 postHandle 方法(后置處理),可在此修改響應內容(如統一處理 JSON 返回值)或操作 ModelAndView
- 響應渲染與返回:根據項目類型(前后端分離 / 服務端渲染),將結果轉換為 JSON 或渲染為 HTML,通過 DispatcherServlet 返回給前端
- 攔截器 afterCompletion:響應返回后,按攔截器注冊的逆序執行 afterCompletion 方法(完成后處理),主要用于資源清理(如記錄請求完成的日志、關閉連接等)
多攔截器特殊規則:
先按攔截器注冊順序依次執行各攔截器的 preHandle 方法
- 若某攔截器 preHandle 返回 false,后續攔截器的 preHandle 及所有后續流程(包括 Controller)均不執行,直接從當前攔截器開始按逆序執行已通過 preHandle 的攔截器的 afterCompletion,最后由 DispatcherServlet 直接返回響應給瀏覽器
- 所有攔截器的 preHandle 均返回 true 時,postHandle 和 afterCompletion 會按逆序執行,確保攔截器的 "前置"順序 與 "后置 / 清理"順序 形成對稱
整個過程由 DispatcherServlet 全程管控,保證攔截器與請求生命周期各階段的有序銜接
全局控制器增強機制(@ControllerAdvice)
@ControllerAdvice
用于定義全局控制器通知類,可以對所有控制器(@Controller
或 @RestController
標注的類)進行全局增強,統一處理一些橫切關注點(如異常處理、數據綁定、全局數據預處理等)
攔截器決定是否讓請求進入 Controller,如果請求被攔截(preHandle返回 false),@ControllerAdvice
不會生效;如果請求未被攔截(preHandle返回 true),@ControllerAdvice
的邏輯會正常執行
@ControllerAdvice
默認對所有控制器生效,若需指定范圍,可通過屬性限制:
- basePackages:指定一個或多個包路徑,僅對這些包下的控制器生效,例如:
@ControllerAdvice(basePackages = {"org.example.controller.user", "org.example.controller.order"})
- assignableTypes:指定具體的控制器類,僅對這些類生效,例如:
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
- annotations:指定特定注解,僅對標注了該注解的控制器生效,例如:
@ControllerAdvice(annotations = {RestController.class})
統一數據返回格式(@ControllerAdvice + ResponseBodyAdvice)
先??自定義通用返回結果封裝類
@Data
public class Result<T> {private Integer code;private String errMsg;private T data;public static <T> Result<T> success(T data) {Result result = new Result<>();result.setCode(200);result.setErrMsg("");result.setData(data);return result;}//其他錯誤public static <T> Result<T> fail(String errMsg) {Result result = new Result();result.setCode(-1);result.setErrMsg(errMsg);result.setData(null);return result;}
}
再創建標注 @ControllerAdvice
的 ResponseAdvice 類,讓其實現 ResponseBodyAdvice 接口
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {//獲取執行的類Class<?> declaringClass = returnType.getMethod().getDeclaringClass();//獲取執行的方法Method method = returnType.getMethod();log.info("執行的類和方法: {}.{}", declaringClass.getName(), method.getName());return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//獲取執行的類Class<?> declaringClass = returnType.getMethod().getDeclaringClass();//獲取執行的方法Method method = returnType.getMethod();log.info("執行的類和方法: {}.{}", declaringClass.getName(), method.getName());return Result.success(body);}
}
- supports方法用于判斷是否需要執行 beforeBodyWrite(返回 true則執行,false則跳過)。可以通過 MethodParameter的信息(如返回值類型、方法名等)過濾不需要處理的返回值
- beforeBodyWrite的核心職責是處理返回值(如包裝成統一格式)
訪問http://127.0.0.1:8080/sayHi
,注意排除一下攔截器的路徑,發現發生了內部錯誤
如果返回類型是String,而統一返回時會把String包裝進對象再返回,這會導致類型不匹配,拋出 ClassCastException,其他類型則不會,這是因為String和別的類型用的不是一個處理器,處理過程中將對象強轉成String的時候會報錯,所以可以把String轉成JSON格式
解決方案:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {private static ObjectMapper objectMapper = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 如果返回結果為String類型,使用SpringBoot內置的Jackson來實現信息的序列化if (body instanceof String) {return objectMapper.writeValueAsString(Result.success(body));}return Result.success(body);}
}
重新測試,結果返回正常。但看著正常,實際是JSON格式的 String,而不是 JSON 對象,所以拿不到里面的鍵值對
如果不想全局修改處理器,可在返回 String 的 Controller 方法中,聲明當前接口返回的響應數據格式為 JSON 類型
@Slf4j
@RestController
public class UserController {@RequestMapping(value = "/sayHi", produces = "application/json")public String sayHi() {log.info("打印日志");return "Hi";}
}
也可以直接返回包裝對象(而非原始 String),從源頭避免類型問題
@Slf4j
@RestController
public class UserController {@RequestMapping("/sayHi")public Result sayHi() {log.info("打印日志");return Result.success("Hi");}
}
如果返回的結果已經是 Result 類型了,那就直接返回 Result 類型的結果即可
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {private static ObjectMapper objectMapper = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 如果返回結果為String類型,使用SpringBoot內置的Jackson來實現信息的序列化if (body instanceof String) {return objectMapper.writeValueAsString(Result.success(body));}if (body instanceof Result){return body;}return Result.success(body);}
}
優點:
- 提升前端數據處理效率
- 增強系統可維護性
- 推動技術規范標準化
- 優化異常排查與問題定位
注意:當 Controller 方法拋出異常時,異常會被 Spring 的異常處理機制(如@ExceptionHandler)捕獲并處理,導致異常響應的格式可能與正常響應不一致
??全局異常處理機制??(@ControllerAdvice + @ExceptionHandler)
任何 Controller 方法拋出異常??時,Spring 會匹配 @ExceptionHandler方法??(基于異常類型),并執行該方法??,生成最終的錯誤響應
可以把 @ControllerAdvice
看作一個 ??"全局警察局"??,而 @ExceptionHandler
是里面的 ??"案件處理專員"??:
- ??@ControllerAdvice??:負責管轄所有 Controller 的異常(相當于警察局的轄區)
- @ExceptionHandler??:負責處理特定類型的案件(相當于警察局里的不同部門,如刑事案件組、民事案件組)
異常處理的優先級??
Spring 會按照以下順序查找異常處理器:
- ??當前 Controller 內部的 @ExceptionHandler??(最優先)
- @ControllerAdvice中的 @ExceptionHandler??(全局處理)
- Spring 默認的異常處理器??(如 DefaultHandlerExceptionResolver)
示例
模擬異常(注意排除攔截器路徑):
@RestController
public class TestController {@RequestMapping("/t1")public String t1(){return "t1";}@RequestMapping("/t2")public boolean t2(){int a = 10/0; //拋出ArithmeticExceptionreturn true;}@RequestMapping("/t3")public Integer t3(){String a = null;System.out.println(a.length()); //拋出NullPointerExceptionreturn 200;}
}
全局處理異常的類名,方法名和返回值可以自定義,重要的是注解
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {@ExceptionHandlerpublic Result<?> handler(Exception e) {return Result.fail(e.getMessage());}
}
@ExceptionHandler 方法的返回值默認會被視為 “視圖名”(用于頁面跳轉)。添加 @ResponseBody 后,或者直接使用 @RestControllerAdvice 才能將返回的 Result 對象序列化為 JSON 響應體。而上面的 ResponseBodyAdvice 是 Spring 提供的用于增強響應體處理的接口,其核心方法 beforeBodyWrite 的返回值會直接作為控制器方法的最終響應體,無需額外通過 @ResponseBody 標記
我們還可以針對不同的異常,返回不同的結果:
@RestControllerAdvice
public class ErrorAdvice {// 兜底異常處理:當沒有更具體的異常處理器時,處理所有 Exception 及其子類(優先級最低)@ExceptionHandlerpublic Result<?> handler(Exception e) {return Result.fail(e.getMessage());}// 顯式指定處理 NullPointerException@ExceptionHandler(value = NullPointerException.class)public Result<?> handleNullPointerException(NullPointerException e) {return Result.fail("空指針異常:" + e.getMessage());}// 同時處理 ArithmeticException 和 IndexOutOfBoundsException@ExceptionHandler(value = {ArithmeticException.class, IndexOutOfBoundsException.class})public Result<?> handleMultipleExceptions(Exception e) {return Result.fail("數值或索引異常:" + e.getMessage());}// 未指定 value,Spring 會根據參數推斷處理RuntimeException及其子類(優先級低于更具體的異常)@ExceptionHandlerpublic Result<?> handleRuntimeException(RuntimeException e) {return Result.fail("運行時異常:" + e.getMessage());}
}
有多個異常通知時,@ExceptionHandler 對異常的匹配遵循 最具體異常優先 的核心原則,具體規則如下:
- 精確類型匹配
Spring 會優先查找與拋出異常類型完全一致的 @ExceptionHandler 方法
例如:若拋出 NullPointerException,則優先匹配參數為 NullPointerException 的處理器,而非其祖先類的處理器
- 父類異常匹配
若沒有精確匹配的處理器,Spring 會向上搜索異常的繼承鏈,匹配離該異常最近的父類異常對應的處理器
例如:IndexOutOfBoundsException 是 RuntimeException 的子類,RuntimeException 又是 Exception 的子類。若僅定義了@ExceptionHandler(RuntimeException .class) 和 @ExceptionHandler(Exception .class)這兩個,則 IndexOutOfBoundsException 會優先匹配 RuntimeException 處理器
- 全局與局部的優先級
若同一異常在當前 Controller 內部和 @ControllerAdvice 全局類中都有 @ExceptionHandler 方法,則局部處理器(當前 Controller 內)優先于全局處理器
總結:匹配順序本質是 “從具體到抽象”—— 越精確的異常類型(或越近的父類)對應的處理器優先級越高。這種設計確保了特定異常能被針對性處理,而通用異常(如 Exception)可作為 “兜底” 處理器,避免未捕獲的異常導致系統崩潰
全局數據預處理(@ControllerAdvice + @InitBinder)
@InitBinder
的作用:在 Controller 方法綁定請求參數前,通過自定義參數轉換規則,將請求中的原始參數轉換為目標數據類型
@InitBinder
的作用范圍??:
- ??局部作用域:當 @InitBinder 直接定義在某個 @Controller 類中時,其配置的參數綁定規則僅對當前 Controller 內的請求參數生效
- 全局作用域:當 @InitBinder 與 @ControllerAdvice 結合時,規則會對所有 @Controller 生效,成為全局共享的參數綁定規則(如日期格式化、自定義類型轉換等)
注意:
- 即使 Controller 方法的參數不添加任何注解,@InitBinder 定義的參數綁定規則仍然生效,只要該參數的類型與 @InitBinder 中配置的轉換規則匹配
- 對 @RequestParam(簡單類型)、@ModelAttribute(對象綁定) 生效,對 @RequestBody(JSON 格式參數)不生效
@ControllerAdvice
public class GlobalBinderAdvice {@InitBinderpublic void initDateBinder(WebDataBinder binder) {// 注冊自定義編輯器binder.registerCustomEditor(// 目標類型: 將請求參數綁定到 Date 類型時生效Date.class, new CustomDateEditor(// 日期格式只接受 "yyyy-MM-dd" new SimpleDateFormat("yyyy-MM-dd"),// 如果請求中沒有該日期參數,不會報錯(允許為 null)true ));}
}
當前端傳遞日期字符串(如 2025-07-17)時,Spring MVC 會自動通過該規則將其轉換為 java.util.Date 對象,無需在每個控制器中重復配置。若前端傳遞的日期格式不符合 yyyy-MM-dd,會拋出 TypeMismatchException
AOP
AOP 是 Spring 框架的第二大核心 (第一大核心是 IoC),是對某一類事情的集中處理
全稱是 Aspect Oriented Programming(面向切面編程),這里的切面就是指某一類特定問題,所以 AOP 也可以理解為面向特定方法編程。比如上面的 "登錄校驗"就是一類特定問題,登錄校驗攔截器就是對 “登錄校驗” 這類問題的統一處理
什么是 Spring AOP?
AOP 是一種思想,它的實現方法有很多,Spring AOP 是其中的一種實現方式
Spring AOP 快速入門
我們先通過下面的程序體驗下 AOP 的開發,并掌握開發步驟
需求:統計業務接口執行耗時
引入 AOP 依賴,在 pom.xml 文件中添加配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
編寫AOP程序,記錄每個方法的執行時間
我的目錄結構如下,切點表達式會涉及到:
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class TimeRecordAspect {//記錄方法耗時@Around("execution(* org.example.j20250715.*.*(..))")public Object timeRecordAspect(ProceedingJoinPoint joinPoint) throws Throwable {//1.記錄開始時間,在切點方法執行前執行long start = System.currentTimeMillis();//2.執行目標方法Object result = joinPoint.proceed();//3.記錄耗時,在切點方法執行后執行log.info(joinPoint.getSignature()+"耗時: {}ms", System.currentTimeMillis()-start);return result;}
}
運行程序,訪問http://127.0.0.1:8080/sayHi
,觀察日志
對程序進行簡單的講解:
- @Aspect:標識這是一個切面類
- @Around:聲明環繞通知,可在目標方法執行前后添加邏輯。后面的切點表達式用于指定 “切入點”,表示對哪些方法進行增強
- ProceedingJoinPoint: 封裝當前被攔截的目標方法的詳細信息,proceed()觸發被攔截的目標方法執行
Spring AOP 詳解
Spring AOP 核心概念
- 連接點(JoinPoint):程序運行中??可以被 AOP 攔截并增強的所有時機 / 位置(比如方法調用、方法執行、異常拋出、字段訪問等),在 Spring AOP 中,連接點通常指的是??目標對象中的方法執行
- 切點 (Pointcut):從所有可能的連接點中,篩選出需要被切面攔截的具體連接點,通過表達式定義,上面的表達式
execution(* org.example.j20250715.*.*(..))
就是切點表達式 - 通知 (Advice):在切點匹配的連接點上具體要執行的增強邏輯,也就是共性功能 (最終體現為一個方法),比如上述程序中記錄業務方法的耗時并打印就是通知
- 切面 (Aspect): 切點 (要攔截的位置) + 通知 (攔截后的具體操作) 的組合,描述了當前 AOP 程序要針對哪些方法,在什么時候執行什么樣的操作。切面所在的類,我們一般稱為切面類 (被 @Aspect 注解標識的類)
用一句話串聯:切面通過切點鎖定要攔截的連接點,然后在程序運行到被切點選中的連接點時觸發通知邏輯
通知類型
上面講了什么是通知,接下來學習通知的類型,Spring AOP 的通知類型有以下幾種:
@Around
:環繞通知,它包裹目標方法,能在目標方法執行前后都插入邏輯,甚至可以控制目標方法是否執行(通過 ProceedingJoinPoint.proceed() 控制)@Before
:前置通知,在目標方法執行之前執行。常用于準備資源、參數校驗等場景@After
:后置通知,在目標方法執行之后執行,無論是否發生異常都會執行。類似于 try…finally 中的 finally 塊,常用于釋放資源、清理操作等@AfterReturning
:正常返回后通知,在目標方法正常執行完成并返回結果后執行,如果發生異常則不會執行。可以獲取目標方法的返回值(通過 returning 屬性指定)@AfterThrowing
:異常通知,在目標方法拋出異常后執行,正常執行時不會觸發。可以捕獲異常信息(通過 throwing 屬性指定異常變量)
注意事項:
@Around作為環繞通知,其核心作用是包裹目標方法執行過程。當調用proceed()時,會觸發目標方法的執行并返回其結果 —— 這個結果需要被@Around方法捕獲后原樣返回,否則調用方將無法獲取目標方法的真實返回值(可能得到null或異常)
因此,@Around方法的返回值類型必須聲明為Object(以兼容所有可能的返回類型),或者嚴格指定為目標方法的返回類型(適用于切入點明確的場景)。在切面需要處理多種返回類型(如Result、Boolean等)的場景中更應聲明為Object,并避免對返回值進行強制類型轉換,而是根據實際類型動態處理
JoinPoint 與 ProceedingJoinPoint
- JoinPoint:僅用于獲取連接點信息,沒法控制目標方法的執行
- ProceedingJoinPoint 是 JoinPoint 的子接口,因此它擁有 JoinPoint 的所有方法,并額外提供了 proceed() 方法,用于觸發目標方法的執行。這是環繞通知的專屬參數,因為環繞通知需要控制目標方法的執行時機(甚至可以決定是否執行)
接下來我們通過代碼來加深這幾個通知的理解,為了防止干擾,每用新的切面類,最好把之前用的注釋掉:
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class AspectDemo {//環繞通知@Around("execution(* org.example.j20250715.*.*(..))")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {// 獲取目標方法所在類名String className = joinPoint.getTarget().getClass().getName();// 獲取目標方法名String methodName = joinPoint.getSignature().getName();// 獲取方法參數Object[] args = joinPoint.getArgs();log.info("環繞通知 - 開始執行方法: {}.{},參數: {}", className, methodName, Arrays.toString(args));Object result = joinPoint.proceed();log.info("環繞通知 - 方法: {}.{} 執行完成,返回結果: {}", className, methodName, result);return result;}//前置通知@Before("execution(* org.example.j20250715.*.*(..))")public void doBefore(JoinPoint joinPoint) {// 獲取目標方法所在類名String className = joinPoint.getTarget().getClass().getSimpleName();// 獲取目標方法名String methodName = joinPoint.getSignature().getName();// 獲取方法參數Object[] args = joinPoint.getArgs();log.info("前置通知 - 準備執行 {}.{} 方法,參數: {}",className,methodName,Arrays.toString(args));}//后置通知@After("execution(* org.example.j20250715.*.*(..))")public void doAfter() {log.info("執行After方法");}//正常返回后通知@AfterReturning("execution(* org.example.j20250715.*.*(..))")public void doAfterReturning() {log.info("執行AfterReturning方法");}//拋出異常后通知@AfterThrowing("execution(* org.example.j20250715.*.*(..))")public void doAfterThrowing() {log.info("執行AfterThrowing方法");}
}
直接用 TestController 里的方法測試,運行程序,觀察日志:
- 正常運行的情況(
http://127.0.0.1:8080/t1
)
程序正常運行的情況下,@AfterThrowing
標識的通知方法不會執行
- 拋出異常的情況 (
http://127.0.0.1:8080/t2
),如果異常被捕獲并不繼續拋出就是上面那種正常情況
程序拋出異常的情況下:
@AfterReturning
標識的通知方法不會執行,@AfterThrowing
標識的通知方法執行了@Around
中 proceed()之后的代碼(即 “環繞后” 的邏輯)不會執行
@PointCut
上述代碼存在大量重復的切點表達式,Spring 提供了 @PointCut 注解把公共的切點表達式提取出來,需要用時引用該切點表達式即可
注意:一個切面類可以有多個切點和多個通知,本文就不細舉了
上述代碼就可以修改為:
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class AspectDemo {//定義切點(公共的切點表達式)@Pointcut("execution(* org.example.j20250715.*.*(..))")private void pt() {}//環繞通知@Around("pt()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {log.info("目標方法執行前");Object result = joinPoint.proceed();log.info("目標方法執行后");return result;}//前置通知@Before("pt()")public void doBefore() {log.info("執行Before方法");}//后置通知@After("pt()")public void doAfter() {log.info("執行After方法");}//正常返回后通知@AfterReturning("pt()")public void doAfterReturning() {log.info("執行AfterReturning方法");}//拋出異常后通知@AfterThrowing("pt()")public void doAfterThrowing() {log.info("執行AfterThrowing方法");}
}
當切點方法(被@Pointcut標注的方法)使用private修飾時,其作用域僅限于當前切面類內部,其他切面類無法訪問和引用。若需要讓其他切面類復用當前切點定義,需將權限修飾符改為public(或protected,但public更通用),此時該切點可被外部切面類引用
其他切面類引用時,需通過 全限定類名.切點方法名()
的格式指定,例如:
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class AspectDemo2 {//前置通知@Before("org.example.j20250715.AspectDemo.pt()")public void doBefore() {log.info("執行 AspectDemo2 的 Before 方法");}
}
切面優先級(@Order)
當項目中存在多個切面類,且它們的切點都匹配到同一個目標方法時。當目標方法執行,那么這幾個通知方法的執行順序是什么樣的呢?
我們還是通過程序來求證:
定義多個切面類,之前的切面類注釋掉不要忘了
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class AspectDemoOrder {@Pointcut("execution(* org.example.j20250715.*.*(..))")private void pt(){}//前置通知@Before("pt()")public void doBefore() {log.info("執行 AspectDemoOrder -> Before 方法");}//后置通知@After("pt()")public void doAfter() {log.info("執行 AspectDemoOrder -> After 方法");}
}
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class AspectDemoOrder2 {@Pointcut("execution(* org.example.j20250715.*.*(..))")private void pt(){}//前置通知@Before("pt()")public void doBefore() {log.info("執行 AspectDemoOrder2 -> Before 方法");}//后置通知@After("pt()")public void doAfter() {log.info("執行 AspectDemoOrder2 -> After 方法");}
}
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class AspectDemoOrder3 {@Pointcut("execution(* org.example.j20250715.*.*(..))")private void pt(){}//前置通知@Before("pt()")public void doBefore() {log.info("執行 AspectDemoOrder3 -> Before 方法");}//后置通知@After("pt()")public void doAfter() {log.info("執行 AspectDemoOrder3 -> After 方法");}
}
運行程序,訪問http://127.0.0.1:8080/sayHi
,觀察日志:
可以看出當存在多個切面類且未顯式指定優先級時,默認按切面類名的 ASCII 碼順序決定通知執行順序,具體表現為:
- @Before 通知: ASCII 碼靠前的先執行
- @After 通知:ASCII 碼靠前的后執行
這種順序呈現「前置順排,后置逆排」的特點
但這種方式不方便管理,而且我們的類名更多還是具備一定含義的,Spring 為我們提供了@Order
注解來控制這些切面通知的執行順序
使用方式如下:
@Slf4j
@Order(2)
@Component
@Aspect //表示這是一個切面類
public class AspectDemoOrder {//....代碼省略
}
@Slf4j
@Order(1)
@Component
@Aspect //表示這是一個切面類
public class AspectDemoOrder2 {//....代碼省略
}
@Slf4j
@Order(3)
@Component
@Aspect //表示這是一個切面類
public class AspectDemoOrder3 {//....代碼省略
}
通過上述程序的運行結果得出 ,@Order(n) 中 n 的數值越小,切面優先級越高。對于匹配同一目標方法的多個切面,優先級高的切面會先執行目標方法前的邏輯(如@Before、@Around的前置代碼),而在目標方法執行完成后,會后執行目標方法后的邏輯(如@After、@Around的后置代碼),整體遵循「先進后出」的棧式邏輯
切點表達式
Spring AOP 中,切點表達式最常用的兩種核心類型是:
- execution (…):根據方法簽名匹配目標方法
- @annotation(…):通過方法上是否標注特定注解來匹配
execution 表達式
execution 是最常用的切點表達式,語法為:
execution([訪問修飾符] 返回類型 [包名.類名.]方法名(參數列表) [throws 異常類型])
支持的通配符表達:
*(單元素通配符)
僅匹配單個任意元素 (返回類型、單個包名、類名、方法名或單個參數類型),不能跨層級..(層級通配符)
匹配??零個或多個連續的任意元素或層級?,可以標識此包以及此包下的所有子包,或任意個任意類型的參數
語法各部分說明:
-
訪問修飾符(可選):如 public、private 等,省略時匹配任意修飾符。例:
execution(public * *(..))
匹配所有 public 修飾的方法 -
返回類型(必填):指定方法返回值類型
-
包名.類名(可選):限定方法所在的類或包,可以不指定包名和類名,此時表達式會匹配所有類中符合條件的方法(不限包和類的范圍)。例:
execution(* ..TestController.*(..))
匹配所有包下面的TestController的所有方法 -
方法名(必填):指定方法名
-
參數列表(必填):指定參數列表,例:
execution(* ..update*(Long, ..))
匹配所有方法名以 update 開頭,第一個參數為 Long ,后續任意參數的方法 -
throws 異常類型(可選):指定方法聲明拋出的異常,省略時匹配任意異常(包括不拋異常的方法)
@annotation
execution表達式更適用有規則的,如果我們要匹配多個無規則的方法,我們可以用「自定義注解 + @annotation 切點表達式」
實現步驟:
- 編寫自定義注解
- 使用 @annotation 表達式來描述切點
- 在目標方法(連接點)上添加自定義注解
自定義一個注解類 @MyAspect ,和創建 Class 文件一樣的流程,選擇 Annotation 就可以了
@Target(ElementType.METHOD) // 僅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 運行時保留,確保AOP能識別
public @interface MyAspect {
}
@Target
和 @Retention
是 Java 中定義注解時的兩個核心元注解,它們分別控制注解的適用范圍和生命周期,具體說明如下:
@Target
用于指定當前注解可以標注在哪些程序元素上(如類、方法、參數等),若沒有則默認注解可用于所有元素,但實際開發中通常會精確限定,避免濫用,常用取值:- ElementType.TYPE:可標注在類、接口、注解類或枚舉類上
- ElementType.METHOD:可標注在方法上
- ElementType.PARAMETER:可標注在方法參數上
- ElementType.FIELD:可標注在字段(成員變量)上
- ElementType.TYPE_USE:可標注在任意類型聲明處(如變量類型、返回值類型等,是 Java 8 新增的靈活選項)
支持多值組合,需用 {} 包裹,例如:
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {}
@Retention
用于指定注解在代碼的哪個階段保留(是否被編譯器或 JVM 保留),決定了程序運行時能否獲取到注解信息,如果自定義注解不添加 @Retention 元注解,Java 會采用默認的生命周期規則,即默認使用 RetentionPolicy.CLASS。取值有三:- RetentionPolicy.SOURCE: 源代碼注解。表示注解僅存在于源代碼中,編譯成字節碼后會被丟棄。這意味著僅對編譯器可見。比如:lombok 提供的注解 @Data, @Slf4j
- RetentionPolicy.CLASS:編譯時注解。表示注解存在于源代碼和字節碼中,但在運行時會被丟棄。所以在實際運行時無法獲取
- RetentionPolicy.RUNTIME:運行時注解。表示注解存在于源代碼,字節碼和運行時中。這意味著實際運行時可以通過反射獲取到該注解的信息。通常用于一些需要在運行時處理的注解,如 Spring 的 @Controller @ResponseBody
使用 @annotation 切點表達式定義切點,匹配所有標注了 @MyAspect 注解的方法,切面類代碼如下:
@Slf4j
@Component
@Aspect //表示這是一個切面類
public class MyAspectDemo {// 前置通知@Before("@annotation(org.example.j20250715.MyAspect)")public void before(){log.info("MyAspect -> before ...");}// 后置通知@After("@annotation(org.example.j20250715.MyAspect)")public void after(){log.info("MyAspect -> after ...");}
}
在TestController中的t1()上添加自定義注解 @MyAspect
@MyAspect@RequestMapping("/t1")public String t1(){return "t1";}
運行程序,測試接口http://127.0.0.1:8080/t1
,觀察日志看到,切面通知被執行了
Spring AOP 原理
上面我們學習了 Spring AOP 的應用,接下來我們來學習 Spring AOP 的原理,也就是 Spring 是如何實現 AOP 的
Spring AOP 的實現基礎是動態代理,主要通過兩種方式實現:JDK 動態代理和 CGLIB 動態代理,具體使用哪種方式取決于項目配置和被代理的對象特性
這兩種動態代理也是 Java 中常見的動態代理實現方式,各自具有不同的特點:
- JDK 動態代理:是 Java 原生提供的動態代理實現,它的使用有一定限制,只能對實現了接口的類進行代理
- CGLIB 動態代理:作為一種第三方實現的動態代理方式,它的適用范圍更廣泛,既可以代理實現了接口的類,也可以直接代理沒有實現接口的類
Spring AOP 正是基于這兩種動態代理方式構建,根據被代理對象是否實現接口等情況,選擇合適的代理方式來完成 AOP 的功能
代理模式
定義:為其他對象提供一種代理以控制對這個對象的訪問。它的作用就是通過提供一個代理類,讓我們在調用被代理對象的目標方法的時候,不再是直接對目標方法進行調用,而是通過代理類間接調用。代理模式可以在不修改被代理對象的基礎上,通過擴展代理類,進行一些功能的附加與增強
在某些情況下,一個對象不適合或者不能直接引用另一個對象,而代理對象可以在調用方和目標對象之間起到中介的作用
代理模式的主要角色:
- Subject(不一定有):業務接口類,定義代理類和目標類共同遵循的接口或抽象類,規定了核心業務方法
- RealSubject:業務實現類,也就是目標對象(被代理對象),負責實現具體的業務邏輯
- Proxy:代理類,持有目標對象的引用,在調用目標方法前后可以添加額外邏輯,最終會調用目標對象的方法
比如房屋租賃:
- Subject:提前定義的租房服務規范,交給中介代理
- RealSubject:房東
- Proxy:中介
通過中介(代理),客戶無需直接接觸房東(目標對象),卻能完成租房流程(目標方法),同時中介還能提供附加價值
根據代理的創建時期,代理模式分為靜態代理和動態代理
- 靜態代理:代理類在編譯期就已確定(由程序員編寫或工具生成),并且會生成對應的.class文件
- 動態代理:代理類在程序運行時動態生成,無需提前編寫代理類代碼,動態代理正是 Spring AOP 實現的基礎,通過動態生成代理對象,實現了對目標方法的增強(如日志記錄、事務管理等),而無需修改目標類本身的代碼
靜態代理
我們通過代碼來加深理解,以房屋租賃為例:
- 定義接口 (定義房東要做的事情,也是中介需要做的事情)
public interface HouseSubject {void rentHouse();
}
- 實現接口 (房東出租房子)
public class RealHouseSubject implements HouseSubject{@Overridepublic void rentHouse() {System.out.println("我是房東,我出租房子");}
}
- 代理 (中介幫房東出租房子)
public class HouseProxy implements HouseSubject{//將被代理對象聲明為成員變量private HouseSubject houseSubject;public HouseProxy(HouseSubject houseSubject) {this.houseSubject = houseSubject;}@Overridepublic void rentHouse() {//開始代理System.out.println("我是中介,開始代理");//代理房東出租房子houseSubject.rentHouse();//代理結束System.out.println("我是中介,代理結束");}
}
- 使用
public class StaticMain {public static void main(String[] args) {//創建代理類HouseProxy proxy = new HouseProxy(new RealHouseSubject());//通過代理類訪問目標方法proxy.rentHouse();}
}
運行結果:
上面這個代理實現方式就是靜態代理 (仿佛啥也沒干)
從上述程序可以看出,雖然靜態代理也完成了對目標對象的代理,但是由于代碼都寫死了,對目標對象的每個方法的增強都是手動完成的,非常不靈活,所以日常開發幾乎看不到靜態代理的場景
接下來中介又新增了代理房屋出售的業務
- 接口定義修改
public interface HouseSubject {void rentHouse();void saleHouse();
}
- 接口實現修改
public class RealHouseSubject implements HouseSubject{@Overridepublic void rentHouse() {System.out.println("我是房東,我出租房子");}@Overridepublic void saleHouse() {System.out.println("我是房東,我出售房子");}
}
- 代理類修改
public class HouseProxy implements HouseSubject{//將被代理對象聲明為成員變量private HouseSubject houseSubject;public HouseProxy(HouseSubject houseSubject) {this.houseSubject = houseSubject;}@Overridepublic void rentHouse() {//開始代理System.out.println("我是中介,開始代理");//代理房東出租房子houseSubject.rentHouse();//代理結束System.out.println("我是中介,代理結束");}@Overridepublic void saleHouse() {//開始代理System.out.println("我是中介,開始代理");//代理房東出售房子houseSubject.saleHouse();//代理結束System.out.println("我是中介,代理結束");}
}
可以看出,當我們修改接口(Subject)時,業務實現類(RealSubject)和代理類(Proxy)往往都需要隨之修改;同樣地,新增接口(Subject)時,也必須對應新增業務實現類(RealSubject)和代理類(Proxy)
不難發現,所有代理類的核心流程其實是相似的 —— 都是在調用目標方法前后添加增強邏輯,最終再執行目標方法。既然如此,是否存在一種方式,能讓一個代理機制自動適配不同的接口和目標對象,而無需為每個接口單獨編寫代理類呢?
這正是動態代理技術要解決的問題:它能在程序運行時根據接口動態生成代理對象,無論接口如何變化或新增,都無需手動編寫對應的代理類,從而極大地提升了代碼的靈活性和可維護性
動態代理
相比于靜態代理,動態代理更加靈活
我們不需要針對每個目標對象都單獨創建一個代理對象,而是把這個創建代理對象的工作推遲到程序運行時由 JVM 來實現。也就是說動態代理在程序運行時,根據需要動態創建生成
靜態代理就像 “專屬中介”—— 比如專門對接 “租房” 業務的中介、專門對接 “賣房” 業務的中介,每新增一種業務(比如 “房屋托管”),就必須再培訓一個對應的新中介,成本高且不夠靈活
而動態代理更像 “全能中介”—— 不需要提前知道會有哪些業務(租房、賣房、托管等),來了任何業務都能當場根據需求 “動態適配”,用同一套邏輯框架處理不同的業務場景。就像 JVM 在運行時根據接口動態生成代理對象,無需提前為每個目標對象編寫代理類,完美解決了靜態代理中 “一對一綁定” 的局限性
JDK 動態代理類實現步驟
- 定義接口及其實現類 (靜態代理中的 HouseSubject 和 RealHouseSubject)
- 創建動態代理類實現 InvocationHandler 接口 并重寫 invoke 方法,在 invoke 方法中我們會調用目標方法 (被代理類的方法) 并自定義一些處理邏輯,當代理對象的方法被調用時,會自動觸發invoke方法的執行
- 通過 Proxy.newProxyInstance 方法創建代理對象,該方法需要三個參數:類加載器、目標對象實現的接口數組、自定義的 InvocationHandler 實例
定義 JDK 動態代理類并實現 InvocationHandler 接口
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class JDKInvocationHandler implements InvocationHandler {//目標對象(被代理的原始對象)private Object target;public JDKInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 代理增強:方法執行前的邏輯(前置增強)System.out.println("我是中介,開始代理");// 通過反射調用目標對象的實際方法Object retVal = method.invoke(target, args);// 代理增強:方法執行后的邏輯(后置增強)System.out.println("我是中介,代理結束");// 返回目標方法的執行結果return retVal;}
}
創建一個代理對象并使用
import java.lang.reflect.Proxy;public class DynamicMain {public static void main(String[] args) {HouseSubject target= new RealHouseSubject();// 創建代理對象HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(), // 目標對象的類加載器new Class[]{HouseSubject.class}, // 代理需要實現的接口new JDKInvocationHandler(target) // 綁定調用處理器,傳入目標對象);// 調用代理對象的方法// 此時并不會直接調用目標對象的方法,而是先觸發處理器的invoke方法// 在invoke方法中會執行增強邏輯,并通過反射調用目標對象的對應方法proxy.rentHouse();}
}
CGLIB 動態代理
JDK 動態代理有一個最致命的問題是其只能代理實現了接口的類。而有些場景下,我們的業務代碼是直接實現的,并沒有接口定義。為了解決這個問題,我們可以用 CGLIB 動態代理機制來解決
CGLIB (Code Generation Library) 是一個基于 ASM 的字節碼生成庫,它允許我們在運行時對字節碼進行修改和動態生成,并通過繼承方式實現代理
- Spring 中的 AOP,如果目標對象實現了接口,則默認采用 JDK 動態代理,否則采用 CGLIB 動態代理
- Spring Boot 中的 AOP,2.0 之前和 Spring 一樣;2.0 及之后為了解決使用 JDK 動態代理可能導致的類型轉化異常而默認使用 CGLIB。如果需要默認使用 JDK 動態代理可以通過配置項
spring.aop.proxy-target-class=false
來進行修改
CGLIB 動態代理類實現步驟
- 定義一個類 (被代理類)
- 實現 MethodInterceptor 并重寫 intercept 方法,intercept 用于增強目標方法,和 JDK 動態代理中的 invoke 方法類似
- 通過 Enhancer 類的 create () 創建代理類
接下來看具體實現:
和 JDK 動態代理不同,CGLIB 屬于一個開源項目,如果要使用它的話,需要手動添加相關依賴
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
實現 MethodInterceptor(方法攔截器)接口
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class CGLIBInterceptor implements MethodInterceptor {private Object target;public CGLIBInterceptor(Object target){this.target = target;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 代理增強:方法執行前的邏輯(前置增強)System.out.println("我是中介,開始代理");// 通過CGLIB的MethodProxy直接調用目標方法(比反射更快)Object retVal = methodProxy.invoke(target, objects);// 代理增強:方法執行后的邏輯(后置增強)System.out.println("我是中介,代理結束");// 返回目標方法的執行結果return retVal;}
}
創建代理類并使用
import org.springframework.cglib.proxy.Enhancer;// 動態代理測試類,使用CGLIB創建代理對象并調用方法
public class DynamicMain {public static void main(String[] args) {// 1. 創建目標對象(被代理的真實對象)HouseSubject target = new RealHouseSubject();// 2. 使用CGLIB的Enhancer創建代理對象// - 第一個參數:目標對象的Class類型// - 第二個參數:MethodInterceptor攔截器實例,用于增強目標方法HouseSubject proxy = (HouseSubject) Enhancer.create(target.getClass(),new CGLIBInterceptor(target));// 3. 通過代理對象調用方法,此時會觸發攔截器中的增強邏輯proxy.rentHouse();}
}