Spring AOP 和 Spring 拦截器
Spring AOP 和 Spring 拦截器
- 一、SpringBoot 拦截器
- 1. 拦截器的定义与使用
- 1.1 自定义拦截器
- 1.2 注册拦截器
- 2. 拦截路径配置
- 3. 拦截器执行流程
- 4. 登录校验功能实现
- 5. Controller 示例:RequestMapping 用法
- 6. Spring MVC 源码解析
- 7. 设计模式中的适配器模式
- 7.1 生活中的例子
- 7.2 slf4j / log4j 桥接示例
- 二、Spring AOP
- 1. AOP 基本概念
- 1.1 AOP 的应用场景
- 2. Spring AOP 快速入门
- 2.1 引入依赖
- 2.2 示例:记录方法耗时
- 参数与注解详解
- 3. AOP 核心概念
- 代理类型简述
- 4. 通知类型(Advice)详解
- 4.1 前置通知 - `@Before`
- 4.2 后置通知 - `@After`
- 4.3 返回后通知 - `@AfterReturning`
- 4.4 异常通知 - `@AfterThrowing`
- 4.5 环绕通知 - `@Around`
- 5. 切点表达式详解
- 6. 自定义注解 + @annotation
- 6.1 定义注解
- 6.2 定义切面
- 6.3 使用注解
- 7. 多切面优先级
- 8. 实战案例
- 8.1 方法耗时统计
- 8.2 异常拦截
- 8.3 通用日志打印
- 9. 总结
- 三、Spring AOP 和 Spring 拦截器有什么关系?
- 1、Spring AOP
- 2、Spring 拦截器
- 3、执行链路对比
- 4、关系总结
- 5、结语
一、SpringBoot 拦截器
在日常的 SpringBoot 开发中,我们经常需要对请求做一些“统一功能处理”,比如:
- 登录校验
- 权限验证
- 日志打印
- 统一返回数据
如果每个接口里都写一遍判断逻辑,会非常麻烦且冗余。
这时就可以用 拦截器(Interceptor) 来统一处理。
本文带你从零开始学习 Spring 拦截器,逐步过渡到源码分析,并结合 适配器模式 理解 SpringMVC 的设计思想。
1. 拦截器的定义与使用
拦截器(Interceptor)是 Spring MVC 提供的核心功能之一,主要作用是在 请求到达 Controller 前后 进行拦截和处理。
1.1 自定义拦截器
实现 HandlerInterceptor
接口,重写其方法:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("preHandle - 目标方法执行前");return true; // 返回 true 表示放行,false 表示拦截}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("postHandle - 目标方法执行后");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("afterCompletion - 视图渲染完成后");}
}
1.2 注册拦截器
实现 WebMvcConfigurer
接口,重写 addInterceptors
方法:
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**") // 拦截所有请求.excludePathPatterns("/user/login", "/**/*.js", "/**/*.css", "/**/*.png", "/**/*.html"); // 排除路径}
}
2. 拦截路径配置
常见拦截路径规则如下:
拦截路径 | 含义 | 示例 |
---|---|---|
/* | 一级路径 | /user /book |
/** | 任意级路径 | /user/login /book/add |
/book/* | /book 下的一层路径 | /book/addBook |
/book/** | /book 下任意级路径 | /book/addBook/1 /book/list |
3. 拦截器执行流程
拦截器的执行顺序如下图:
请求进入 → preHandle()↓ (放行=true)
Controller 方法执行↓
postHandle()↓
视图渲染↓
afterCompletion()
- preHandle:Controller 方法执行前
- postHandle:Controller 方法执行后
- afterCompletion:整个请求完成后(包括视图渲染)
如果 preHandle
返回 false
,请求将被拦截,后续方法不会执行。
4. 登录校验功能实现
在实际开发中,常见的需求是 拦截未登录用户。
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute("USER") != null) {return true; // 已登录,放行}response.setStatus(401); // 未认证return false;}
}
401 Unauthorized
:表示请求未通过身份认证。
配置拦截器时,排除 /user/login
接口,避免死循环。
5. Controller 示例:RequestMapping 用法
拦截器配置好以后,我们来写一个简单的 Controller,测试是否生效:
@RestController
@RequestMapping("/user")
public class UserController {// 模拟登录接口,不拦截@PostMapping("/login")public String login(HttpSession session, @RequestParam String username) {session.setAttribute("USER", username);return "登录成功,欢迎 " + username;}// 被拦截的接口@GetMapping("/profile")public String profile(HttpSession session) {String user = (String) session.getAttribute("USER");return "当前登录用户: " + user;}// 普通测试接口@RequestMapping("/hello")public String hello(@RequestParam String name) {return "hello " + name + " , 时间戳: " + System.currentTimeMillis();}
}
👉 访问 /user/hello
时会被拦截器处理;
👉 访问 /user/login
时不会被拦截;
👉 登录成功后再访问 /user/profile
,才能获取到用户信息。
6. Spring MVC 源码解析
所有请求都会先经过 DispatcherServlet,再分发到 Controller。
拦截器就是在 DispatcherServlet#doDispatch()
里生效的。
源码关键流程(简化):
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {// 1. 获取 Handler(执行链)HandlerExecutionChain mappedHandler = getHandler(request);// 2. 获取 HandlerAdapter(适配器)HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// 3. 执行拦截器 preHandleif (!mappedHandler.applyPreHandle(request, response)) {return; // 拦截返回}// 4. 执行目标 Controller 方法ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());// 5. 执行拦截器 postHandlemappedHandler.applyPostHandle(request, response, mv);// 6. 渲染视图 + 执行 afterCompletionprocessDispatchResult(request, response, mappedHandler, mv, null);
}
可以看到:
- 拦截器 →
preHandle
在 Controller 之前执行 - 拦截器 →
postHandle
在 Controller 之后执行 - 拦截器 →
afterCompletion
在请求完成后执行
7. 设计模式中的适配器模式
在 DispatcherServlet
中,为什么要有 HandlerAdapter
?
因为不同的 Handler 类型(Controller、HttpRequestHandler、Servlet 等)需要一个 统一的适配入口。
👉 这就是 适配器模式(Adapter Pattern)。
7.1 生活中的例子
- 出国旅行需要 电源转换插头
- 旧手机耳机(3.5mm)需要 Type-C 转接头
7.2 slf4j / log4j 桥接示例
// slf4j API
interface Slf4jApi {void log(String message);
}// log4j 原始类(不兼容)
class Log4j {void log4jLog(String message) {System.out.println("Log4j打印: " + message);}
}// 适配器
class Slf4jLog4jAdapter implements Slf4jApi {private Log4j log4j;public Slf4jLog4jAdapter(Log4j log4j) {this.log4j = log4j;}@Overridepublic void log(String message) {log4j.log4jLog(message);}
}// 客户端
public class Slf4jDemo {public static void main(String[] args) {Slf4jApi logger = new Slf4jLog4jAdapter(new Log4j());logger.log("使用slf4j打印日志");}
}
通过适配器,不需要改动 log4j 的代码,就能让它兼容 slf4j 的接口。
Spring MVC 的 HandlerAdapter
就是这种思想的应用:
- 目标类:各种 Handler(Controller)
- 适配器:HandlerAdapter
- 调用方:DispatcherServlet
二、Spring AOP
在日常开发中,我们经常遇到一些“横切关注点”的需求,例如:
- 日志打印(记录请求参数、返回值、耗时)
- 权限验证
- 异常拦截与统一处理
- 事务控制
- 性能监控
如果这些代码散落在每一个业务方法里,既冗余又难维护。AOP(面向切面编程) 就是为了解决这类问题而生的。
1. AOP 基本概念
AOP(Aspect Oriented Programming):面向切面编程。
核心思想是将与业务逻辑无关的“共性问题”抽取出来,形成一个“切面(Aspect)”,并在需要的地方织入(Weaving)到业务代码中。
AOP 可以看作是 OOP(面向对象编程)的补充。OOP 关注“对象纵向结构”,AOP 关注“横向切面逻辑”。
1.1 AOP 的应用场景
- 日志记录
- 权限校验
- 缓存处理
- 异常监控
- 性能统计
- 事务管理
2. Spring AOP 快速入门
2.1 引入依赖
在 Spring Boot 项目 pom.xml
中添加:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring Boot 默认开启 AOP(底层基于 AspectJ 动态代理实现)。
2.2 示例:记录方法耗时
@Slf4j
@Aspect
@Component
public class TimeAspect {@Around("execution(* com.example.demo.service.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {long start = System.currentTimeMillis();Object result = pjp.proceed(); // 执行目标方法,可能抛异常long end = System.currentTimeMillis();log.info("方法 {} 执行耗时: {} ms", pjp.getSignature(), (end - start));return result; // 必须返回结果}
}
参数与注解详解
注解/参数 | 作用 |
---|---|
@Slf4j | Lombok 注解,自动生成日志对象 log |
@Aspect | 声明当前类是切面类 |
@Component | 注入 Spring 容器,使切面生效 |
@Around("execution(...)") | 环绕通知,包裹目标方法,可在方法执行前后插入逻辑,可控制目标方法是否执行 |
ProceedingJoinPoint pjp | 环绕通知专用参数,可获取方法签名、参数,并执行目标方法 |
pjp.proceed() | 执行目标方法,必须调用,否则方法不会被执行,可能抛出异常 |
pjp.getSignature() | 获取方法签名(类名.方法名) |
pjp.getArgs() | 获取方法参数 |
3. AOP 核心概念
名称 | 解释 | 示例 |
---|---|---|
切点 (Pointcut) | 定义拦截规则 | execution(* com.example..service.*(..)) |
连接点 (JoinPoint) | 程序运行中的某个方法执行点 | UserService.getUser() |
通知 (Advice) | 增强逻辑,在连接点执行的动作 | @Before 、@After 、@Around 等 |
切面 (Aspect) | 切点 + 通知的组合 | 日志切面、权限切面 |
织入 (Weaving) | 把切面应用到目标对象的过程 | 动态代理(JDK Proxy、CGLIB) |
代理类型简述
- JDK 动态代理:只能拦截接口方法,目标类必须实现接口。
- CGLIB 代理:通过生成子类拦截类方法,也能拦截接口方法,但 final 类/方法无法拦截。
Spring AOP 默认优先 JDK 代理,无接口时自动使用 CGLIB。
4. 通知类型(Advice)详解
4.1 前置通知 - @Before
在方法执行之前执行,无法阻止目标方法执行。
@Before("execution(* com.example.demo.service.*.*(..))")
public void beforeAdvice(JoinPoint jp) {log.info("Before - 方法开始: {}", jp.getSignature());
}
4.2 后置通知 - @After
在方法执行之后执行(无论正常返回或抛异常都会执行),可用于资源清理。
@After("execution(* com.example.demo.service.*.*(..))")
public void afterAdvice(JoinPoint jp) {log.info("After - 方法执行完成: {}", jp.getSignature());
}
4.3 返回后通知 - @AfterReturning
在方法正常返回后执行,可获取返回值。
@AfterReturning(value = "execution(* com.example.demo.service.*.*(..))", returning = "result")
public void afterReturningAdvice(Object result) {log.info("AfterReturning - 返回值: {}", result);
}
🔹 注意:
returning
属性名必须与方法参数名一致,否则 Spring 找不到对应参数。
4.4 异常通知 - @AfterThrowing
在方法抛出异常后执行,可获取异常对象。
@AfterThrowing(value = "execution(* com.example.demo.service.*.*(..))", throwing = "ex")
public void afterThrowingAdvice(Exception ex) {log.error("AfterThrowing - 捕获异常: {}", ex.getMessage());
}
4.5 环绕通知 - @Around
包裹目标方法,可在执行前后添加逻辑,可控制目标方法是否执行。
@Around("execution(* com.example.demo.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {log.info("Around - 方法执行前");Object result = pjp.proceed(); // 执行目标方法,可能抛异常log.info("Around - 方法执行后");return result;
}
🔹 Tip:环绕通知执行顺序前于
@Before
,其“后逻辑”晚于@AfterReturning
,实际日志顺序可能因切面顺序或代理类型不同略有差异。
5. 切点表达式详解
execution
用于匹配方法执行,其基本格式为:
execution([访问修饰符] 返回类型 包名.类名.方法名(参数列表) [异常])
拆解说明:
部分 | 含义 |
---|---|
访问修饰符(可选) | 如 public 、private 等 |
返回类型 | * 表示任意返回类型 |
包名.类名.方法名(方法参数) | 指定要拦截的类和方法,可用 * 或 .. 通配 |
异常(可选) | 匹配方法抛出的异常类型 |
示例:
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
*
→ 任意返回类型com.example.demo.controller.*.*(..)
→ 拦截controller
包下所有类的所有方法,参数不限
🔹 Tip:
..
可匹配任意层级包或任意数量参数*
可匹配任意返回类型或任意方法名
6. 自定义注解 + @annotation
6.1 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
6.2 定义切面
@Slf4j
@Aspect
@Component
public class AnnotationAspect {@Around("@annotation(com.example.demo.annotation.LogExecutionTime)")public Object logTime(ProceedingJoinPoint pjp) throws Throwable {long start = System.currentTimeMillis();Object result = pjp.proceed();long end = System.currentTimeMillis();log.info("方法 {} 执行耗时 {} ms", pjp.getSignature(), (end - start));return result;}
}
6.3 使用注解
@Service
public class UserService {@LogExecutionTimepublic void getUserList() {try { Thread.sleep(200); } catch (InterruptedException ignored) {}}
}
调用 getUserList()
时会自动统计方法耗时。
7. 多切面优先级
使用 @Order
控制执行顺序,数值越小优先级越高:
@Order(1)
@Aspect
@Component
public class FirstAspect { ... }@Order(2)
@Aspect
@Component
public class SecondAspect { ... }
执行顺序:
FirstAspect @Before
SecondAspect @Before
- 方法执行
SecondAspect @After
FirstAspect @After
8. 实战案例
8.1 方法耗时统计
@Around("execution(* com.example.demo.service.*.*(..))")
public Object timeCost(ProceedingJoinPoint pjp) throws Throwable {long start = System.nanoTime();Object result = pjp.proceed();long end = System.nanoTime();log.info("[耗时监控] {} 执行耗时: {} ms", pjp.getSignature(), (end - start)/1_000_000);return result;
}
8.2 异常拦截
@AfterThrowing(pointcut = "execution(* com.example.demo.service.*.*(..))", throwing = "ex")
public void handleException(Exception ex) {log.error("[异常拦截] 捕获到异常: {}", ex.getMessage());
}
8.3 通用日志打印
@Before("execution(* com.example.demo.controller.*.*(..))")
public void logRequest(JoinPoint jp) {log.info("[请求日志] 方法: {}, 参数: {}", jp.getSignature(), Arrays.toString(jp.getArgs()));
}
9. 总结
本文系统介绍了 Spring AOP:
- AOP 概念与应用场景
- Spring AOP 快速入门(耗时统计示例)
- 核心概念:切点、连接点、通知、切面
- 通知类型详解(@Before / @After / @AfterReturning / @AfterThrowing / @Around)
- 切点表达式(execution、@annotation)
- 自定义注解的使用方式
- 多个切面的优先级控制(@Order)
- 实战案例:耗时监控、异常拦截、日志打印
- 日志运行效果示例
三、Spring AOP 和 Spring 拦截器有什么关系?
在实际开发中,我们经常会同时接触 Spring AOP 和 Spring MVC 拦截器,它们都能实现“横切逻辑”,比如日志、权限校验、异常处理。但两者之间经常被混淆。本文简单梳理一下它们的关系和区别。
1、Spring AOP
Spring AOP 属于 Spring 核心功能,本质是通过 动态代理(JDK Proxy / CGLIB) 在 方法调用的前后 织入额外逻辑。
- 作用范围:任意 Spring 管理的 Bean 方法(Controller、Service、DAO)。
- 常见场景:日志打印、事务控制、性能监控、统一异常处理。
- 特点:与 Web 无关,任何 Spring 项目都能用。
2、Spring 拦截器
Spring 拦截器(HandlerInterceptor
)属于 Spring MVC 模块,主要作用在 Web 请求处理链 上。
-
拦截时机:
- 请求进入 Controller 之前(
preHandle
) - Controller 方法执行之后(
postHandle
) - 请求完成后(
afterCompletion
)
- 请求进入 Controller 之前(
-
常见场景:登录校验、权限验证、接口限流、请求日志、跨域处理。
-
特点:依赖 SpringMVC,仅 Web 项目可用。
3、执行链路对比
来看一条典型的请求处理链路:
HTTP 请求↓
Filter (Servlet 过滤器) ← 拦截最早↓
Interceptor (Spring 拦截器) ← 拦截 Controller 前后↓
Controller 方法执行↓ ↑└── AOP 可以在方法前后织入逻辑↓
Service / DAO 方法↓
返回响应
👉 可以看到:
- 拦截器 更偏向 请求级别 的控制;
- AOP 更偏向 方法级别 的增强;
- 两者位置不同,但都能实现“横切逻辑”。
4、关系总结
-
共同点:
- 都能实现非业务逻辑的统一处理。
- 都是“拦截 + 增强”的思想。
-
不同点:
特性 Spring AOP Spring 拦截器 所属模块 Spring AOP Spring MVC 作用范围 Bean 方法(任意层) Web 请求(Controller 前后) 实现原理 动态代理(JDK Proxy/CGLIB) 责任链模式(HandlerInterceptor) 常用场景 日志、事务、异常、性能监控 登录校验、权限验证、接口限流
5、结语
Spring AOP 和 Spring 拦截器没有直接继承或实现关系,它们是 两个平行但互补的机制:
- 想拦截 请求入口 → 用拦截器;
- 想拦截 方法调用 → 用 AOP;
- 想拦截 所有请求(包括静态资源) → 用 Filter。