拦截器、@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 {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("UserInterceptor 目标方法执行前执行..");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("UserInterceptor 目标方法执行后执行");
}
@Override
public 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 {
@Autowired
private UserInterceptor userInterceptor;
@Override
public 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 {
@Override
public 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 {
@Autowired
private UserInterceptor userInterceptor;
@Override
public 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 {
@Autowired
private UserInterceptor userInterceptor;
private List<String> excludePaths = Arrays.asList(
"/user/login",
"/static/**"
);
@Override
public 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 {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//获取执行的类
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
//获取执行的方法
Method method = returnType.getMethod();
log.info("执行的类和方法: {}.{}", declaringClass.getName(), method.getName());
return true;
}
@Override
public 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();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public 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();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public 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; //抛出ArithmeticException
return true;
}
@RequestMapping("/t3")
public Integer t3(){
String a = null;
System.out.println(a.length()); //抛出NullPointerException
return 200;
}
}
全局处理异常的类名,方法名和返回值可以自定义,重要的是注解
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler
public Result<?> handler(Exception e) {
return Result.fail(e.getMessage());
}
}
@ExceptionHandler 方法的返回值默认会被视为 “视图名”(用于页面跳转)。添加 @ResponseBody 后,或者直接使用 @RestControllerAdvice 才能将返回的 Result 对象序列化为 JSON 响应体。而上面的 ResponseBodyAdvice 是 Spring 提供的用于增强响应体处理的接口,其核心方法 beforeBodyWrite 的返回值会直接作为控制器方法的最终响应体,无需额外通过 @ResponseBody 标记
我们还可以针对不同的异常,返回不同的结果:
@RestControllerAdvice
public class ErrorAdvice {
// 兜底异常处理:当没有更具体的异常处理器时,处理所有 Exception 及其子类(优先级最低)
@ExceptionHandler
public 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及其子类(优先级低于更具体的异常)
@ExceptionHandler
public 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 {
@InitBinder
public 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{
@Override
public void rentHouse() {
System.out.println("我是房东,我出租房子");
}
}
- 代理 (中介帮房东出租房子)
public class HouseProxy implements HouseSubject{
//将被代理对象声明为成员变量
private HouseSubject houseSubject;
public HouseProxy(HouseSubject houseSubject) {
this.houseSubject = houseSubject;
}
@Override
public 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{
@Override
public void rentHouse() {
System.out.println("我是房东,我出租房子");
}
@Override
public void saleHouse() {
System.out.println("我是房东,我出售房子");
}
}
- 代理类修改
public class HouseProxy implements HouseSubject{
//将被代理对象声明为成员变量
private HouseSubject houseSubject;
public HouseProxy(HouseSubject houseSubject) {
this.houseSubject = houseSubject;
}
@Override
public void rentHouse() {
//开始代理
System.out.println("我是中介,开始代理");
//代理房东出租房子
houseSubject.rentHouse();
//代理结束
System.out.println("我是中介,代理结束");
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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();
}
}