SpringBoot拦截器、过滤器、监听器区别与实战用法
本文从 Servlet 规范底层原理出发,系统性地讲解 Filter(过滤器)、Interceptor(拦截器)、Listener(监听器)三者的核心区别、执行顺序、多种注册方式以及生产级实战用法。包含登录校验、权限控制、XSS防护、编码处理、性能监控、在线人数统计等 10+ 个完整可复用的代码案例。
目录
- 一、三者概述:一张图看懂区别
- 1.1 核心概念速览
- 1.2 三者在请求链路中的位置
- 1.3 核心区别对比表
- 二、过滤器 Filter 详解与实战
- 2.1 Filter 原理与生命周期
- 2.2 注册方式一:@WebFilter + @ServletComponentScan
- 2.3 注册方式二:FilterRegistrationBean(推荐)
- 2.4 注册方式三:实现 Filter 接口 + @Component
- 2.5 实战一:统一字符编码过滤器
- 2.6 实战二:XSS 攻击防护过滤器
- 2.7 实战三:SQL 注入防护过滤器
- 2.8 实战四:请求体可重复读取过滤器(解决流只能读一次)
- 2.9 实战五:请求日志过滤器(记录请求/响应详情)
- 2.10 过滤器执行顺序控制
- 三、拦截器 Interceptor 详解与实战
- 3.1 Interceptor 原理与生命周期
- 3.2 实现方式:HandlerInterceptor 接口
- 3.3 注册配置:WebMvcConfigurer
- 3.4 实战一:登录状态校验拦截器
- 3.5 实战二:接口权限校验拦截器(RBAC)
- 3.6 实战三:接口性能监控拦截器
- 3.7 实战四:接口防重复提交拦截器
- 3.8 实战五:数据权限过滤拦截器(MyBatis 插件配合)
- 3.9 拦截器排除路径与静态资源处理
- 3.10 多个拦截器的执行顺序
- 四、监听器 Listener 详解与实战
- 4.1 Listener 分类与原理
- 4.2 注册方式:@WebListener + @ServletComponentScan
- 4.3 注册方式二:ServletListenerRegistrationBean
- 4.4 注册方式三:@Component 直接注册
- 4.5 实战一:ServletContextListener 应用启动初始化
- 4.6 实战二:HttpSessionListener 在线人数统计
- 4.7 实战三:ServletRequestListener 请求计数与耗时统计
- 4.8 实战四:HttpSessionAttributeListener 监听属性变化
- 4.9 实战五:Spring 自定义事件与监听器(ApplicationEvent)
- 4.10 实战六:Spring 异步事件监听(@Async + @EventListener)
- 五、三者执行顺序与嵌套关系深度解析
- 5.1 请求进入时的完整执行链
- 5.2 响应返回时的完整执行链
- 5.3 异常发生时的执行链变化
- 5.4 代码验证:打印完整执行顺序
- 六、常见问题与解法对比
- 6.1 过滤器不生效的常见原因
- 6.2 拦截器不生效的常见原因
- 6.3 过滤器中无法注入 Bean 的问题
- 6.4 拦截器中无法注入 Bean 的问题
- 6.5 过滤器与拦截器如何选择
- 6.6 静态资源被拦截的问题
- 6.7 跨域配置用过滤器还是拦截器
- 6.8 过滤器中读取请求体后 Controller 拿不到数据
- 七、生产级最佳实践
- 7.1 三者职责划分原则
- 7.2 性能优化建议
- 7.3 安全加固建议
- 7.4 可观测性建议
- 八、完整项目结构与代码汇总
- 8.1 项目目录结构
- 8.2 核心配置类完整代码
- 九、总结
一、三者概述:一张图看懂区别
1.1 核心概念速览
| 过滤器 | Filter | Servlet 规范(JSR-315) | Servlet 容器层(Tomcat/Jetty) | 请求/响应的包装与预处理,如编码、XSS、日志 |
| 拦截器 | Interceptor | Spring MVC 框架 | Spring MVC 层(DispatcherServlet 内部) | Controller 方法的前后置处理,如登录、权限、性能 |
| 监听器 | Listener | Servlet 规范 / Spring 事件机制 | 事件驱动层 | 监听上下文、Session、请求、Spring 事件的创建/销毁/属性变化 |
1.2 三者在请求链路中的位置
客户端请求
│
▼
┌─────────────────────────────────────────────┐
│ Tomcat / Jetty 容器 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Filter1 │──▶│ Filter2 │──▶│ FilterN │ │ ← 过滤器链(Servlet 层)
│ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │Dispatcher- │ │
│ │ Servlet │ │ ← Spring MVC 入口
│ └──────┬──────┘ │
│ │ │
│ ┌──────────▼────────┐│
│ │ Interceptor1 ││
│ │ preHandle() ││ ← 拦截器前置
│ └──────────┬────────┘│
│ │ │
│ ┌──────────▼────────┐│
│ │ Interceptor2 ││
│ │ preHandle() ││
│ └──────────┬────────┘│
│ │ │
│ ┌──────────▼────────┐│
│ │ Controller ││ ← 业务逻辑
│ │ Method ││
│ └──────────┬────────┘│
│ │ │
│ ┌──────────▼────────┐│
│ │ Interceptor2 ││
│ │ postHandle() ││ ← 拦截器后置
│ └──────────┬────────┘│
│ │ │
│ ┌──────────▼────────┐│
│ │ Interceptor1 ││
│ │ postHandle() ││
│ └──────────┬────────┘│
│ │ │
│ ┌──────▼──────┐ │
│ │ 视图渲染 │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────────▼────────┐│
│ │ Interceptor2 ││
│ │ afterCompletion()││ ← 拦截器完成回调
│ └──────────┬────────┘│
│ ┌──────────▼────────┐│
│ │ Interceptor1 ││
│ │ afterCompletion()││
│ └──────────┬────────┘│
│ │ │
│ ┌─────────┐ ┌─────────┐ ┌────▼────┐ │
│ │ Filter1 │◀──│ Filter2 │◀──│ FilterN │ │ ← 过滤器链反向(响应)
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────┘
│
▼
客户端响应
1.3 核心区别对比表
| 规范来源 | Servlet 规范 | Spring MVC 框架 | Servlet 规范 / Spring 事件 |
| 作用层级 | Servlet 容器层 | Spring MVC 层 | 事件驱动层 |
| 执行时机 | 请求进入 Servlet 之前,响应离开 Servlet 之后 | Controller 方法执行前后、视图渲染后 | 特定事件发生时(创建/销毁/属性变化) |
| 依赖容器 | 依赖 Servlet 容器(Tomcat/Jetty) | 依赖 Spring MVC | 依赖 Servlet 容器或 Spring 容器 |
| 能否注入 Bean | 默认不能(需特殊处理) | 能(由 Spring 管理) | 能(由 Spring 管理) |
| 能否获取 Controller 方法信息 | 不能 | 能(HandlerMethod) | 不能 |
| 能否修改请求/响应 | 能(可包装 Request/Response) | 不能直接修改流,但可修改 ModelAndView | 不能 |
| 执行顺序控制 | 通过 @Order 或 registrationBean.setOrder() | 通过 addInterceptor 的注册顺序 | 按注册顺序,同类型按 @Order |
| 能否拦截静态资源 | 能(默认拦截所有) | 默认不拦截(需手动配置) | 不涉及 |
| 典型应用 | 编码设置、XSS防护、请求日志、跨域 | 登录校验、权限控制、性能监控、防重复提交 | 启动初始化、在线人数、请求计数、Spring事件 |
| 中断请求能力 | 能(不调用 chain.doFilter) | 能(preHandle 返回 false) | 不能 |
二、过滤器 Filter 详解与实战
2.1 Filter 原理与生命周期
Filter 是 Servlet 规范定义的组件,在请求到达 Servlet 之前和响应离开 Servlet 之后执行。它基于责任链模式,多个 Filter 组成一条 FilterChain。
Filter 接口源码:
public interface Filter {
/**
* 容器启动时调用,只执行一次,用于初始化
*/
default void init(FilterConfig filterConfig) throws ServletException {
}
/**
* 每次请求被拦截时调用
* @param request 请求对象(ServletRequest)
* @param response 响应对象(ServletResponse)
* @param chain 过滤器链,调用 chain.doFilter() 将请求传递给下一个过滤器
*/
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
/**
* 容器关闭时调用,只执行一次,用于销毁资源
*/
default void destroy() {
}
}
生命周期:
容器启动 → init()(仅一次)
│
▼
请求到达 → doFilter()(每次请求)
│
├── chain.doFilter() → 放行到下一个过滤器/目标资源
│
└── 不调用 chain.doFilter() → 中断请求,直接返回响应
│
▼
容器关闭 → destroy()(仅一次)
2.2 注册方式一:@WebFilter + @ServletComponentScan
这是最简洁的注册方式,适合简单场景。
第一步:在启动类上添加 @ServletComponentScan
@SpringBootApplication
@ServletComponentScan(basePackages = "com.example.filter")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
第二步:编写 Filter 并添加 @WebFilter 注解
package com.example.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Slf4j
@WebFilter(filterName = "myFilter", urlPatterns = "/*")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
log.info("MyFilter 初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
log.info("MyFilter 拦截请求:{}", httpRequest.getRequestURI());
long startTime = System.currentTimeMillis();
chain.doFilter(request, response);
long cost = System.currentTimeMillis() – startTime;
log.info("MyFilter 请求处理完成,耗时:{}ms", cost);
}
@Override
public void destroy() {
log.info("MyFilter 销毁");
}
}
注意:@WebFilter 注解本身不支持通过 @Order 控制执行顺序。如果需要控制顺序,必须使用 FilterRegistrationBean 方式。
2.3 注册方式二:FilterRegistrationBean(推荐)
这是生产环境推荐的注册方式,功能最强大,可以精确控制顺序、URL 模式、初始化参数等。
package com.example.config;
import com.example.filter.EncodingFilter;
import com.example.filter.XssFilter;
import com.example.filter.RequestLogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@Configuration
public class FilterConfig {
/**
* 注册编码过滤器,优先级最高
*/
@Bean
public FilterRegistrationBean<EncodingFilter> encodingFilterRegistration() {
FilterRegistrationBean<EncodingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new EncodingFilter());
registration.addUrlPatterns("/*");
registration.setName("encodingFilter");
// 数字越小优先级越高
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
// 设置初始化参数
registration.addInitParameter("encoding", "UTF-8");
registration.addInitParameter("forceEncoding", "true");
return registration;
}
/**
* 注册 XSS 防护过滤器
*/
@Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new XssFilter());
// 只拦截特定路径
registration.addUrlPatterns("/api/*");
registration.setName("xssFilter");
registration.setOrder(1);
return registration;
}
/**
* 注册请求日志过滤器
*/
@Bean
public FilterRegistrationBean<RequestLogFilter> requestLogFilterRegistration() {
FilterRegistrationBean<RequestLogFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestLogFilter());
registration.addUrlPatterns("/*");
// 排除静态资源
registration.addInitParameter("exclusions", "/static/*,/public/*,*.js,*.css,*.png,*.jpg");
registration.setName("requestLogFilter");
registration.setOrder(2);
return registration;
}
}
2.4 注册方式三:实现 Filter 接口 + @Component
最简单的注册方式,直接加 @Component 注解即可被 Spring 管理。但这种方式默认拦截所有请求(/*),无法精确配置 URL 模式。
package com.example.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Slf4j
@Component
@Order(1) // 控制执行顺序,数字越小优先级越高
public class SimpleFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
log.info("SimpleFilter 拦截:{}", httpRequest.getRequestURI());
chain.doFilter(request, response);
}
}
三种注册方式对比:
| @WebFilter | 注解简洁 | 不支持 @Order 排序,需 @ServletComponentScan | 单个简单过滤器 |
| FilterRegistrationBean | 功能最全,可控制顺序/URL/参数 | 配置类代码较多 | 生产环境推荐 |
| @Component | 最简单 | 默认拦截所有请求,无法配置 URL 模式 | 全局过滤器 |
2.5 实战一:统一字符编码过滤器
虽然 SpringBoot 已经提供了 CharacterEncodingFilter,但了解其实现原理有助于自定义编码处理。
package com.example.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 统一字符编码过滤器
*/
@Slf4j
public class EncodingFilter implements Filter {
private String encoding = "UTF-8";
private boolean forceEncoding = true;
@Override
public void init(FilterConfig filterConfig) {
String encodingParam = filterConfig.getInitParameter("encoding");
String forceParam = filterConfig.getInitParameter("forceEncoding");
if (encodingParam != null && !encodingParam.isEmpty()) {
this.encoding = encodingParam;
}
if (forceParam != null && !forceParam.isEmpty()) {
this.forceEncoding = Boolean.parseBoolean(forceParam);
}
log.info("EncodingFilter 初始化,编码:{},强制编码:{}", encoding, forceEncoding);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 设置请求编码
if (forceEncoding || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
// 设置响应编码
if (forceEncoding) {
response.setCharacterEncoding(encoding);
response.setContentType("text/html;charset=" + encoding);
}
log.debug("EncodingFilter 处理请求:{},编码:{}", httpRequest.getRequestURI(), encoding);
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.info("EncodingFilter 销毁");
}
}
SpringBoot 内置编码过滤器配置(更推荐):
server:
servlet:
encoding:
charset: UTF–8
enabled: true
force: true
2.6 实战二:XSS 攻击防护过滤器
XSS(跨站脚本攻击)防护的核心思路是:对请求参数中的特殊字符进行转义,防止恶意脚本注入。
第一步:自定义 HttpServletRequestWrapper,重写获取参数的方法
package com.example.filter;
import org.apache.commons.text.StringEscapeUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.*;
/**
* XSS 防护请求包装器
* 对所有请求参数进行 HTML 转义
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* 重写 getParameter,对单个参数值进行转义
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return value == null ? null : escapeXss(value);
}
/**
* 重写 getParameterValues,对参数数组进行转义
*/
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
String[] escapedValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
escapedValues[i] = escapeXss(values[i]);
}
return escapedValues;
}
/**
* 重写 getParameterMap,对所有参数进行转义
*/
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> originalMap = super.getParameterMap();
if (originalMap == null || originalMap.isEmpty()) {
return originalMap;
}
Map<String, String[]> escapedMap = new LinkedHashMap<>();
for (Map.Entry<String, String[]> entry : originalMap.entrySet()) {
String[] values = entry.getValue();
if (values != null) {
String[] escapedValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
escapedValues[i] = escapeXss(values[i]);
}
escapedMap.put(entry.getKey(), escapedValues);
} else {
escapedMap.put(entry.getKey(), null);
}
}
return escapedMap;
}
/**
* 重写 getHeader,对请求头进行转义
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return value == null ? null : escapeXss(value);
}
/**
* XSS 转义方法
* 使用 Apache Commons Text 的 StringEscapeUtils
* 如果不想引入依赖,也可以手动实现
*/
private String escapeXss(String value) {
if (value == null || value.isEmpty()) {
return value;
}
// 方式一:使用 Apache Commons Text(推荐,更全面)
return StringEscapeUtils.escapeHtml4(value);
// 方式二:手动实现(不依赖第三方库)
// return manualEscape(value);
}
/**
* 手动 XSS 转义实现
*/
private String manualEscape(String value) {
StringBuilder sb = new StringBuilder(value.length() + 16);
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
switch (c) {
case '<':
sb.append("<");
break;
case '>':
sb.append(">");
break;
case '&':
sb.append("&");
break;
case '"':
sb.append(""");
break;
case '\\'':
sb.append("'");
break;
case '/':
sb.append("/");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
}
第二步:编写 XSS 过滤器
package com.example.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* XSS 攻击防护过滤器
*/
@Slf4j
public class XssFilter implements Filter {
/**
* 不需要 XSS 防护的路径(如文件上传接口)
*/
private static final List<String> EXCLUDE_PATHS = Arrays.asList(
"/api/file/upload",
"/api/import"
);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String uri = httpRequest.getRequestURI();
// 排除不需要防护的路径
if (isExcludePath(uri)) {
chain.doFilter(request, response);
return;
}
// 使用包装后的 Request 传递给后续过滤器和 Controller
XssHttpServletRequestWrapper wrappedRequest = new XssHttpServletRequestWrapper(httpRequest);
chain.doFilter(wrappedRequest, response);
}
private boolean isExcludePath(String uri) {
return EXCLUDE_PATHS.stream().anyMatch(uri::startsWith);
}
}
注意:上面的 XSS 包装只处理了表单参数(getParameter 系列),对于 JSON 请求体(@RequestBody),需要额外处理。可以配合 ContentCachingRequestWrapper 或自定义 RequestWrapper 读取 InputStream 并转义后重新包装。
2.7 实战三:SQL 注入防护过滤器
SQL 注入防护的核心是检测请求参数中是否包含 SQL 关键字和特殊字符。
package com.example.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.regex.Pattern;
/**
* SQL 注入防护过滤器
* 检测请求参数中是否包含 SQL 注入关键字
*/
@Slf4j
public class SqlInjectFilter implements Filter {
/**
* SQL 注入关键字正则
*/
private static final Pattern SQL_INJECT_PATTERN = Pattern.compile(
"(?:')|(?:–)|(/\\\\*(?:.|[\\\\n\\\\r])*?\\\\*/)|" +
"(\\\\b(select|update|and|or|delete|insert|truncate|char|into|" +
"substr|declare|exec|count|master|into|drop|execute|union|create|" +
"alter|grant|revoke|commit|rollback|savepoint|lock|unlock)\\\\b)",
Pattern.CASE_INSENSITIVE
);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String uri = httpRequest.getRequestURI();
// 检查所有请求参数
Enumeration<String> parameterNames = httpRequest.getParameterNames();
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String[] values = httpRequest.getParameterValues(paramName);
if (values == null) continue;
for (String value : values) {
if (value != null && SQL_INJECT_PATTERN.matcher(value).find()) {
log.warn("检测到 SQL 注入攻击,URI: {},参数名: {},参数值: {}",
uri, paramName, value);
// 拒绝请求
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\\"code\\":\\"40003\\",\\"message\\":\\"请求参数包含非法字符\\"}");
return;
}
}
}
chain.doFilter(request, response);
}
}
重要提示:SQL 注入防护的根本解决方案是使用预编译语句(PreparedStatement)和MyBatis 的 #{} 占位符,过滤器只能作为辅助防护手段。不要依赖过滤器作为唯一防护。
2.8 实战四:请求体可重复读取过滤器(解决流只能读一次)
在过滤器中读取请求体(如记录请求日志)后,Controller 中的 @RequestBody 会拿不到数据,因为 HttpServletRequest 的 InputStream 只能读一次。解决方案是自定义 HttpServletRequestWrapper,将请求体缓存到字节数组中。
第一步:可重复读取的 RequestWrapper
package com.example.filter;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* 可重复读取请求体的 RequestWrapper
* 将请求体缓存到字节数组,实现多次读取
*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
/**
* 缓存的请求体字节数组
*/
private final byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// 读取原始请求体并缓存
InputStream requestInputStream = request.getInputStream();
this.cachedBody = requestInputStream.readAllBytes();
}
/**
* 重写 getInputStream,返回基于缓存字节数组的输入流
*/
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(this.cachedBody);
}
/**
* 重写 getReader,返回基于缓存字节数组的字符流
*/
@Override
public BufferedReader getReader() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream, StandardCharsets.UTF_8));
}
/**
* 获取缓存的请求体字符串
*/
public String getCachedBody() {
return new String(this.cachedBody, StandardCharsets.UTF_8);
}
/**
* 获取缓存的请求体字节数组
*/
public byte[] getCachedBodyBytes() {
return this.cachedBody;
}
/**
* 自定义 ServletInputStream,基于字节数组
*/
private static class CachedBodyServletInputStream extends ServletInputStream {
private final InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
return true;
}
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return cachedBodyInputStream.read(b, off, len);
}
}
}
第二步:使用该 Wrapper 的过滤器
package com.example.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 请求体缓存过滤器
* 确保请求体可以被多次读取(过滤器中读取后,Controller 仍能读取)
*/
@Slf4j
public class CachedBodyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 只对 JSON 请求体进行包装(文件上传等不包装)
String contentType = httpRequest.getContentType();
if (contentType != null && contentType.contains("application/json")) {
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
chain.doFilter(cachedRequest, response);
} else {
chain.doFilter(request, response);
}
}
}
2.9 实战五:请求日志过滤器(记录请求/响应详情)
生产环境中,记录完整的请求和响应日志对于排查问题至关重要。
package com.example.filter;
import com.example.filter.CachedBodyHttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 请求日志过滤器
* 记录请求 URL、方法、参数、请求体、响应体、耗时等完整信息
*/
@Slf4j
public class RequestLogFilter implements Filter {
/**
* 最大记录的请求体长度(避免大文件日志爆炸)
*/
private static final int MAX_BODY_LENGTH = 2048;
/**
* 不记录日志的路径前缀
*/
private static final String[] EXCLUDE_PREFIXES = {
"/static", "/public", "/webjars", "/swagger", "/v2/api-docs", "/v3/api-docs"
};
/**
* 不记录日志的文件后缀
*/
private static final String[] EXCLUDE_SUFFIXES = {
".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf"
};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String uri = httpRequest.getRequestURI();
// 排除不需要记录日志的请求
if (shouldSkipLog(uri)) {
chain.doFilter(request, response);
return;
}
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 包装请求和响应,以便读取请求体和响应体
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
ContentCachingResponseWrapper cachedResponse = new ContentCachingResponseWrapper(httpResponse);
try {
// 记录请求日志
logRequest(cachedRequest);
// 执行请求
chain.doFilter(cachedRequest, cachedResponse);
} finally {
stopWatch.stop();
// 记录响应日志
logResponse(cachedRequest, cachedResponse, stopWatch.getTotalTimeMillis());
// 必须将响应体写回原始响应,否则客户端收不到数据
cachedResponse.copyBodyToResponse();
}
}
/**
* 记录请求日志
*/
private void logRequest(CachedBodyHttpServletRequest request) {
StringBuilder sb = new StringBuilder();
sb.append("\\n========== 请求开始 ==========\\n");
sb.append("URL: ").append(request.getRequestURL()).append("\\n");
sb.append("Method: ").append(request.getMethod()).append("\\n");
sb.append("URI: ").append(request.getRequestURI()).append("\\n");
sb.append("QueryString: ").append(request.getQueryString()).append("\\n");
sb.append("RemoteAddr: ").append(request.getRemoteAddr()).append("\\n");
sb.append("ContentType: ").append(request.getContentType()).append("\\n");
// 记录请求头
sb.append("Headers: \\n");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
// 敏感头不记录
if (!"authorization".equalsIgnoreCase(headerName) && !"cookie".equalsIgnoreCase(headerName)) {
sb.append(" ").append(headerName).append(": ").append(request.getHeader(headerName)).append("\\n");
}
}
// 记录请求参数(表单)
Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap != null && !parameterMap.isEmpty()) {
sb.append("Parameters: \\n");
parameterMap.forEach((key, value) ->
sb.append(" ").append(key).append(": ").append(String.join(",", value)).append("\\n"));
}
// 记录请求体(JSON)
String body = request.getCachedBody();
if (body != null && !body.isEmpty()) {
if (body.length() > MAX_BODY_LENGTH) {
body = body.substring(0, MAX_BODY_LENGTH) + "…(truncated)";
}
sb.append("Body: ").append(body).append("\\n");
}
sb.append("========== 请求结束 ==========");
log.info(sb.toString());
}
/**
* 记录响应日志
*/
private void logResponse(HttpServletRequest request, ContentCachingResponseWrapper response, long cost) {
StringBuilder sb = new StringBuilder();
sb.append("\\n========== 响应开始 ==========\\n");
sb.append("URI: ").append(request.getRequestURI()).append("\\n");
sb.append("Status: ").append(response.getStatus()).append("\\n");
sb.append("Cost: ").append(cost).append("ms\\n");
// 记录响应体
byte[] content = response.getContentAsByteArray();
if (content.length > 0) {
String responseBody = new String(content, StandardCharsets.UTF_8);
if (responseBody.length() > MAX_BODY_LENGTH) {
responseBody = responseBody.substring(0, MAX_BODY_LENGTH) + "…(truncated)";
}
sb.append("ResponseBody: ").append(responseBody).append("\\n");
}
sb.append("========== 响应结束 ==========");
log.info(sb.toString());
}
/**
* 判断是否跳过日志记录
*/
private boolean shouldSkipLog(String uri) {
for (String prefix : EXCLUDE_PREFIXES) {
if (uri.startsWith(prefix)) {
return true;
}
}
for (String suffix : EXCLUDE_SUFFIXES) {
if (uri.endsWith(suffix)) {
return true;
}
}
return false;
}
}
2.10 过滤器执行顺序控制
使用 FilterRegistrationBean 控制顺序(推荐):
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<Filter1> filter1Registration() {
FilterRegistrationBean<Filter1> registration = new FilterRegistrationBean<>();
registration.setFilter(new Filter1());
registration.addUrlPatterns("/*");
registration.setOrder(1); // 数字越小,优先级越高
return registration;
}
@Bean
public FilterRegistrationBean<Filter2> filter2Registration() {
FilterRegistrationBean<Filter2> registration = new FilterRegistrationBean<>();
registration.setFilter(new Filter2());
registration.addUrlPatterns("/*");
registration.setOrder(2);
return registration;
}
}
执行顺序验证:
请求进入 → Filter1.doFilter() 前置 → Filter2.doFilter() 前置 → Controller
↓
响应返回 ← Filter1.doFilter() 后置 ← Filter2.doFilter() 后置 ← 视图渲染
注意:@WebFilter 注解的过滤器执行顺序按类名字母顺序排列,不可控。如果需要精确控制顺序,必须使用 FilterRegistrationBean。
三、拦截器 Interceptor 详解与实战
3.1 Interceptor 原理与生命周期
拦截器是 Spring MVC 提供的组件,在 DispatcherServlet 内部执行,用于在 Controller 方法执行前后进行处理。
HandlerInterceptor 接口源码:
public interface HandlerInterceptor {
/**
* 在 Controller 方法执行之前调用
* @return true 继续执行(下一个拦截器或 Controller);false 中断请求
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
return true;
}
/**
* 在 Controller 方法执行之后、视图渲染之前调用
* 可以修改 ModelAndView
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 在整个请求完成之后(视图渲染结束)调用
* 通常用于资源清理
* 只有 preHandle 返回 true 的拦截器才会执行此方法
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
}
}
生命周期与执行时机:
DispatcherServlet.doDispatch()
│
├── 1. 获取 Handler(Controller 方法)
│
├── 2. 执行拦截器链的 preHandle()
│ ├── Interceptor1.preHandle() → true
│ ├── Interceptor2.preHandle() → true
│ └── … 全部返回 true 才继续
│ (如果某个返回 false,执行已通过拦截器的 afterCompletion,然后返回)
│
├── 3. 执行 Controller 方法(ha.handle())
│
├── 4. 执行拦截器链的 postHandle()(倒序)
│ ├── Interceptor2.postHandle()
│ └── Interceptor1.postHandle()
│
├── 5. 渲染视图(processDispatchResult)
│
└── 6. 执行拦截器链的 afterCompletion()(倒序)
├── Interceptor2.afterCompletion()
└── Interceptor1.afterCompletion()
3.2 实现方式:HandlerInterceptor 接口
package com.example.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义拦截器示例
*/
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
log.info("MyInterceptor preHandle – 请求路径: {}", request.getRequestURI());
// handler 通常是 HandlerMethod 对象,可以获取 Controller 方法信息
if (handler instanceof org.springframework.web.method.HandlerMethod) {
org.springframework.web.method.HandlerMethod handlerMethod =
(org.springframework.web.method.HandlerMethod) handler;
log.info("Controller 类: {}", handlerMethod.getBeanType().getSimpleName());
log.info("Controller 方法: {}", handlerMethod.getMethod().getName());
}
// 返回 true 继续执行,返回 false 中断请求
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
log.info("MyInterceptor postHandle – 请求路径: {}", request.getRequestURI());
// 可以修改 ModelAndView,如添加公共数据
if (modelAndView != null) {
modelAndView.addObject("serverTime", System.currentTimeMillis());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
log.info("MyInterceptor afterCompletion – 请求路径: {}", request.getRequestURI());
if (ex != null) {
log.error("请求处理发生异常", ex);
}
// 资源清理,如移除 ThreadLocal 中的数据
}
}
3.3 注册配置:WebMvcConfigurer
package com.example.config;
import com.example.interceptor.LoginInterceptor;
import com.example.interceptor.PerformanceInterceptor;
import com.example.interceptor.PermissionInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC 配置类
* 注册拦截器、配置静态资源、跨域等
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Autowired
private PermissionInterceptor permissionInterceptor;
@Autowired
private PerformanceInterceptor performanceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册性能监控拦截器,优先级最高(最先执行 preHandle,最后执行 postHandle/afterCompletion)
registry.addInterceptor(performanceInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/static/**", "/public/**")
.order(0);
// 注册登录校验拦截器
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**") // 拦截所有 /api 开头的请求
.excludePathPatterns( // 排除不需要登录的路径
"/api/user/login",
"/api/user/register",
"/api/user/sendCode",
"/api/doc/**"
)
.order(1);
// 注册权限校验拦截器
registry.addInterceptor(permissionInterceptor)
.addPathPatterns("/api/admin/**") // 只拦截管理员接口
.order(2);
}
}
路径匹配规则:
- ? 匹配一个字符
- * 匹配零个或多个字符(单层路径)
- ** 匹配零个或多个目录(多层路径)
- 示例:/api/** 匹配 /api/user、/api/user/1、/api/order/list 等
3.4 实战一:登录状态校验拦截器
package com.example.interceptor;
import com.example.common.context.UserContext;
import com.example.common.context.UserInfo;
import com.example.common.result.Result;
import com.example.common.result.ResultCode;
import com.example.util.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登录状态校验拦截器
* 校验请求头中的 Token,解析用户信息并存入 ThreadLocal
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtils jwtUtils;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1. 从请求头获取 Token
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 2. Token 为空,返回未登录
if (token == null || token.isEmpty()) {
writeUnauthorizedResponse(response, "未登录或登录已过期");
return false;
}
// 3. 校验 Token 有效性
if (!jwtUtils.validateToken(token)) {
writeUnauthorizedResponse(response, "Token无效或已过期");
return false;
}
// 4. 解析用户信息
UserInfo userInfo = jwtUtils.parseToken(token);
if (userInfo == null) {
writeUnauthorizedResponse(response, "Token解析失败");
return false;
}
// 5. 将用户信息存入 ThreadLocal,供后续业务使用
UserContext.setUser(userInfo);
log.info("用户登录校验通过,userId: {}, userName: {}", userInfo.getUserId(), userInfo.getUserName());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 必须清理 ThreadLocal,防止内存泄漏
UserContext.clear();
}
/**
* 写入未登录响应
*/
private void writeUnauthorizedResponse(HttpServletResponse response, String message) throws Exception {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
Result<Void> result = Result.fail(ResultCode.UNAUTHORIZED.getCode(), message);
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(result));
}
}
UserContext 工具类:
package com.example.common.context;
/**
* 用户上下文,基于 ThreadLocal 存储当前登录用户信息
*/
public class UserContext {
private static final ThreadLocal<UserInfo> USER_THREAD_LOCAL = new ThreadLocal<>();
public static void setUser(UserInfo userInfo) {
USER_THREAD_LOCAL.set(userInfo);
}
public static UserInfo getUser() {
return USER_THREAD_LOCAL.get();
}
public static Long getUserId() {
UserInfo user = USER_THREAD_LOCAL.get();
return user != null ? user.getUserId() : null;
}
public static void clear() {
USER_THREAD_LOCAL.remove();
}
}
3.5 实战二:接口权限校验拦截器(RBAC)
基于自定义注解 + 拦截器实现细粒度的接口权限控制。
第一步:定义权限注解
package com.example.common.annotation;
import java.lang.annotation.*;
/**
* 接口权限校验注解
* 标注在 Controller 方法上,指定访问该接口所需的权限
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequirePermission {
/**
* 所需权限标识(如 "user:list", "user:delete")
*/
String[] value();
/**
* 权限校验逻辑:AND(全部拥有)/ OR(拥有其一)
*/
Logical logical() default Logical.AND;
enum Logical {
AND, OR
}
}
第二步:权限校验拦截器
package com.example.interceptor;
import com.example.common.annotation.RequirePermission;
import com.example.common.context.UserContext;
import com.example.common.context.UserInfo;
import com.example.common.result.Result;
import com.example.common.result.ResultCode;
import com.example.service.PermissionService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Set;
/**
* 接口权限校验拦截器
* 基于 @RequirePermission 注解进行 RBAC 权限校验
*/
@Slf4j
@Component
public class PermissionInterceptor implements HandlerInterceptor {
@Autowired
private PermissionService permissionService;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 只处理 HandlerMethod(Controller 方法),静态资源等不处理
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 1. 先检查类上的注解
RequirePermission classAnnotation = handlerMethod.getBeanType().getAnnotation(RequirePermission.class);
// 2. 再检查方法上的注解(方法上的注解优先级更高)
RequirePermission methodAnnotation = handlerMethod.getMethodAnnotation(RequirePermission.class);
// 没有权限注解,放行
if (classAnnotation == null && methodAnnotation == null) {
return true;
}
// 方法上的注解覆盖类上的注解
RequirePermission annotation = methodAnnotation != null ? methodAnnotation : classAnnotation;
// 3. 获取当前登录用户
UserInfo userInfo = UserContext.getUser();
if (userInfo == null) {
writeForbiddenResponse(response, "未登录");
return false;
}
// 4. 获取用户拥有的权限列表
Set<String> userPermissions = permissionService.getUserPermissions(userInfo.getUserId());
// 5. 校验权限
String[] requiredPermissions = annotation.value();
boolean hasPermission;
if (annotation.logical() == RequirePermission.Logical.AND) {
// AND:必须拥有全部权限
hasPermission = userPermissions.containsAll(Arrays.asList(requiredPermissions));
} else {
// OR:拥有任一权限即可
hasPermission = Arrays.stream(requiredPermissions).anyMatch(userPermissions::contains);
}
if (!hasPermission) {
log.warn("用户无权限访问,userId: {}, 所需权限: {}, 拥有权限: {}",
userInfo.getUserId(), Arrays.toString(requiredPermissions), userPermissions);
writeForbiddenResponse(response, "权限不足,无法访问该接口");
return false;
}
return true;
}
private void writeForbiddenResponse(HttpServletResponse response, String message) throws Exception {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
Result<Void> result = Result.fail(ResultCode.PERMISSION_DENIED.getCode(), message);
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(result));
}
}
使用示例:
@RestController
@RequestMapping("/api/admin/user")
@RequirePermission("user:manage") // 类级别,所有方法都需要 user:manage 权限
public class UserAdminController {
@GetMapping("/list")
@RequirePermission("user:list") // 方法级别,需要 user:list 权限
public Result<PageResult<UserVO>> list() {
// …
}
@PostMapping("/delete")
@RequirePermission(value = {"user:delete", "user:manage"}, logical = RequirePermission.Logical.OR)
public Result<Void> delete(@RequestParam Long id) {
// …
}
}
3.6 实战三:接口性能监控拦截器
package com.example.interceptor;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
/**
* 接口性能监控拦截器
* 记录每个接口的执行耗时,慢接口告警,Micrometer 指标埋点
*/
@Slf4j
@Component
public class PerformanceInterceptor implements HandlerInterceptor {
/**
* 慢接口阈值(毫秒)
*/
private static final long SLOW_API_THRESHOLD = 3000;
private static final String START_TIME_ATTR = "performance_start_time";
@Autowired
private MeterRegistry meterRegistry;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
// 记录开始时间
request.setAttribute(START_TIME_ATTR, System.currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
// 可以在这里记录 Controller 方法执行耗时(不含视图渲染)
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
Long startTime = (Long) request.getAttribute(START_TIME_ATTR);
if (startTime == null) {
return;
}
long cost = System.currentTimeMillis() – startTime;
String uri = request.getRequestURI();
String method = request.getMethod();
int status = response.getStatus();
// 1. 记录耗时日志
if (cost > SLOW_API_THRESHOLD) {
log.warn("慢接口告警,URI: {},Method: {},Status: {},耗时: {}ms(超过阈值 {}ms)",
uri, method, status, cost, SLOW_API_THRESHOLD);
} else {
log.debug("接口耗时,URI: {},Method: {},Status: {},耗时: {}ms",
uri, method, status, cost);
}
// 2. Micrometer 指标埋点
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
String className = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
Timer.builder("api.request.duration")
.description("接口请求耗时")
.tag("uri", uri)
.tag("method", method)
.tag("class", className)
.tag("methodName", methodName)
.tag("status", String.valueOf(status))
.register(meterRegistry)
.record(Duration.ofMillis(cost));
}
}
}
3.7 实战四:接口防重复提交拦截器
基于 Redis + 自定义注解实现接口防重复提交。
第一步:防重复提交注解
package com.example.common.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 防重复提交注解
* 标注在 Controller 方法上,指定时间内不允许重复提交
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(默认 5 秒)
*/
int interval() default 5;
/**
* 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 提示信息
*/
String message() default "请勿重复提交";
}
第二步:防重复提交拦截器
package com.example.interceptor;
import com.example.common.annotation.RepeatSubmit;
import com.example.common.context.UserContext;
import com.example.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* 防重复提交拦截器
* 基于 Redis 实现,同一用户同一接口在指定时间内只能提交一次
*/
@Slf4j
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String REPEAT_SUBMIT_KEY_PREFIX = "repeat_submit:";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RepeatSubmit annotation = handlerMethod.getMethodAnnotation(RepeatSubmit.class);
// 没有防重复提交注解,放行
if (annotation == null) {
return true;
}
// 1. 构建唯一标识:用户ID + 请求URI + 请求方法
Long userId = UserContext.getUserId();
String userKey = userId != null ? String.valueOf(userId) : request.getRemoteAddr();
String redisKey = REPEAT_SUBMIT_KEY_PREFIX + userKey + ":" +
request.getRequestURI() + ":" + request.getMethod();
// 2. 尝试设置 key(setIfAbsent 等价于 SETNX)
int interval = annotation.interval();
TimeUnit timeUnit = annotation.timeUnit();
Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", interval, timeUnit);
// 3. 如果 key 已存在,说明重复提交
if (Boolean.FALSE.equals(success)) {
log.warn("重复提交拦截,userId: {}, URI: {}, 间隔: {}{}",
userKey, request.getRequestURI(), interval, timeUnit);
writeRepeatSubmitResponse(response, annotation.message());
return false;
}
return true;
}
private void writeRepeatSubmitResponse(HttpServletResponse response, String message) throws Exception {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=UTF-8");
Result<Void> result = Result.fail("40006", message);
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(result));
}
}
使用示例:
@PostMapping("/order/create")
@RepeatSubmit(interval = 3, message = "订单正在处理中,请勿重复提交")
public Result<OrderVO> createOrder(@RequestBody @Valid OrderCreateDTO dto) {
// 业务逻辑
}
3.8 实战五:数据权限过滤拦截器(MyBatis 插件配合)
数据权限是指不同用户只能看到自己权限范围内的数据(如部门经理只能看到本部门数据)。
第一步:数据权限注解
package com.example.common.annotation;
import java.lang.annotation.*;
/**
* 数据权限注解
* 标注在 Controller 方法或 Mapper 方法上,指定数据权限过滤规则
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/**
* 部门表别名(SQL 中部门 ID 所在表的别名)
*/
String deptAlias() default "d";
/**
* 用户表别名(SQL 中用户 ID 所在表的别名)
*/
String userAlias() default "u";
/**
* 是否过滤本人数据
*/
boolean self() default true;
}
第二步:数据权限拦截器(将数据权限信息存入 ThreadLocal)
package com.example.interceptor;
import com.example.common.annotation.DataScope;
import com.example.common.context.DataScopeContext;
import com.example.common.context.DataScopeInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 数据权限拦截器
* 读取 Controller 方法上的 @DataScope 注解,将数据权限信息存入 ThreadLocal,
* 供 MyBatis 插件读取并动态拼接 SQL 条件
*/
@Slf4j
@Component
public class DataScopeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 优先取方法上的注解,其次取类上的注解
DataScope methodAnnotation = handlerMethod.getMethodAnnotation(DataScope.class);
DataScope classAnnotation = handlerMethod.getBeanType().getAnnotation(DataScope.class);
DataScope annotation = methodAnnotation != null ? methodAnnotation : classAnnotation;
if (annotation != null) {
DataScopeInfo info = new DataScopeInfo();
info.setDeptAlias(annotation.deptAlias());
info.setUserAlias(annotation.userAlias());
info.setSelf(annotation.self());
DataScopeContext.set(info);
log.debug("数据权限拦截器设置数据权限,deptAlias: {}, userAlias: {}",
annotation.deptAlias(), annotation.userAlias());
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 清理 ThreadLocal
DataScopeContext.clear();
}
}
第三步:MyBatis 插件(读取 ThreadLocal 中的数据权限,拼接 SQL)
package com.example.plugin;
import com.example.common.context.DataScopeContext;
import com.example.common.context.DataScopeInfo;
import com.example.common.context.UserContext;
import com.example.common.context.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.util.Properties;
/**
* 数据权限 MyBatis 插件
* 在 SQL 执行前,根据数据权限信息动态拼接 WHERE 条件
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class DataScopeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
DataScopeInfo dataScope = DataScopeContext.get();
if (dataScope == null) {
return invocation.proceed();
}
UserInfo currentUser = UserContext.getUser();
if (currentUser == null) {
return invocation.proceed();
}
// 获取 StatementHandler 和 SQL
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
// 构建数据权限 SQL 条件
String dataScopeSql = buildDataScopeSql(dataScope, currentUser);
if (dataScopeSql != null && !dataScopeSql.isEmpty()) {
// 将数据权限条件拼接到原 SQL 中
String newSql = "SELECT * FROM (" + originalSql + ") temp_data_scope WHERE " + dataScopeSql;
metaObject.setValue("delegate.boundSql.sql", newSql);
log.debug("数据权限 SQL 拼接完成,原SQL: {},新SQL: {}", originalSql, newSql);
}
return invocation.proceed();
}
/**
* 构建数据权限 SQL 条件
* 实际项目中应根据用户的数据权限范围(全部/本部门/本部门及以下/本人)来构建
*/
private String buildDataScopeSql(DataScopeInfo dataScope, UserInfo user) {
// 简化示例:只看本人数据
// 实际项目应根据用户角色和数据权限范围来构建
if (dataScope.isSelf()) {
return dataScope.getUserAlias() + ".user_id = " + user.getUserId();
}
return null;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
3.9 拦截器排除路径与静态资源处理
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
// 静态资源
"/static/**",
"/public/**",
"/resources/**",
"/META-INF/resources/**",
// 常用静态资源后缀
"/**/*.js",
"/**/*.css",
"/**/*.png",
"/**/*.jpg",
"/**/*.jpeg",
"/**/*.gif",
"/**/*.ico",
"/**/*.woff",
"/**/*.woff2",
"/**/*.ttf",
"/**/*.svg",
// Swagger / Knife4j
"/swagger-ui.html",
"/swagger-ui/**",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**",
"/doc.html",
// 健康检查
"/actuator/**",
// 错误页面
"/error"
);
}
/**
* 配置静态资源映射(如果关闭了默认静态资源映射)
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
3.10 多个拦截器的执行顺序
假设有三个拦截器,注册顺序为 Interceptor1(order=1)、Interceptor2(order=2)、Interceptor3(order=3):
请求进入
│
▼
Interceptor1.preHandle() → true
│
▼
Interceptor2.preHandle() → true
│
▼
Interceptor3.preHandle() → true
│
▼
Controller 方法执行
│
▼
Interceptor3.postHandle()
│
▼
Interceptor2.postHandle()
│
▼
Interceptor1.postHandle()
│
▼
视图渲染
│
▼
Interceptor3.afterCompletion()
│
▼
Interceptor2.afterCompletion()
│
▼
Interceptor1.afterCompletion()
│
▼
响应返回
关键规则:
- preHandle 按注册顺序(order 从小到大)正序执行
- postHandle 和 afterCompletion 按注册顺序倒序执行
- 如果某个拦截器的 preHandle 返回 false,则只有它之前(已执行 preHandle)的拦截器的 afterCompletion 会被执行
四、监听器 Listener 详解与实战
4.1 Listener 分类与原理
监听器是基于观察者模式的组件,用于监听 Servlet 容器中特定事件的发生。
Servlet 规范中的监听器分类:
| ServletContextListener | ServletContextEvent | 应用启动/关闭 |
| ServletContextAttributeListener | ServletContextAttributeEvent | Context 域属性增删改 |
| HttpSessionListener | HttpSessionEvent | Session 创建/销毁 |
| HttpSessionAttributeListener | HttpSessionBindingEvent | Session 域属性增删改 |
| HttpSessionBindingListener | HttpSessionBindingEvent | 对象绑定/解绑到 Session |
| HttpSessionActivationListener | HttpSessionEvent | Session 钝化/激活 |
| ServletRequestListener | ServletRequestEvent | 请求创建/销毁 |
| ServletRequestAttributeListener | ServletRequestAttributeEvent | Request 域属性增删改 |
三大域对象对比:
| ServletContext(Application) | 整个应用 | 应用启动到关闭 |
| HttpSession | 一次会话 | 用户第一次访问到 session 超时/销毁 |
| ServletRequest | 一次请求 | 请求开始到响应结束 |
4.2 注册方式:@WebListener + @ServletComponentScan
// 启动类
@SpringBootApplication
@ServletComponentScan(basePackages = "com.example.listener")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 监听器
package com.example.listener;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("应用启动,ServletContext 初始化");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("应用关闭,ServletContext 销毁");
}
}
4.3 注册方式二:ServletListenerRegistrationBean
@Configuration
public class ListenerConfig {
@Bean
public ServletListenerRegistrationBean<MyServletRequestListener> servletRequestListenerRegistration() {
ServletListenerRegistrationBean<MyServletRequestListener> registration =
new ServletListenerRegistrationBean<>();
registration.setListener(new MyServletRequestListener());
registration.setOrder(1);
return registration;
}
}
4.4 注册方式三:@Component 直接注册
最简单的方式,直接加 @Component 注解即可被 Spring 管理并注册到 Servlet 容器。
@Slf4j
@Component
public class MyHttpSessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
log.info("Session 创建,sessionId: {}", se.getSession().getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
log.info("Session 销毁,sessionId: {}", se.getSession().getId());
}
}
4.5 实战一:ServletContextListener 应用启动初始化
package com.example.listener;
import com.example.service.DictService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.Map;
/**
* 应用启动初始化监听器
* 在应用启动时加载全局配置、字典数据到 ServletContext
*/
@Slf4j
@Component
public class AppInitListener implements ServletContextListener {
@Autowired
private DictService dictService;
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("========== 应用启动初始化开始 ==========");
long startTime = System.currentTimeMillis();
ServletContext context = sce.getServletContext();
try {
// 1. 加载字典数据到 Application 域
Map<String, String> dictMap = dictService.loadAllDict();
context.setAttribute("dictMap", dictMap);
log.info("字典数据加载完成,共 {} 条", dictMap.size());
// 2. 设置应用上下文路径
context.setAttribute("ctxPath", context.getContextPath());
// 3. 记录应用启动时间
context.setAttribute("appStartTime", System.currentTimeMillis());
long cost = System.currentTimeMillis() – startTime;
log.info("========== 应用启动初始化完成,耗时: {}ms ==========", cost);
} catch (Exception e) {
log.error("应用启动初始化失败", e);
// 初始化失败是否终止启动,根据业务需求决定
throw new RuntimeException("应用启动初始化失败", e);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("========== 应用关闭,资源清理开始 ==========");
try {
// 清理资源,如关闭线程池、释放连接等
ServletContext context = sce.getServletContext();
context.removeAttribute("dictMap");
log.info("资源清理完成");
} catch (Exception e) {
log.error("资源清理失败", e);
}
log.info("========== 应用关闭,资源清理完成 ==========");
}
}
ServletContextListener vs CommandLineRunner vs @PostConstruct
| @PostConstruct | Bean 初始化后 | 能 | 单个 Bean 的初始化 |
| ServletContextListener | Servlet 容器启动后 | 能(需 Spring 管理) | 需要 ServletContext 的初始化 |
| CommandLineRunner | Spring 容器启动完成后 | 能 | 应用启动后的业务初始化 |
| ApplicationRunner | 同 CommandLineRunner,但参数更丰富 | 能 | 需要解析命令行参数 |
4.6 实战二:HttpSessionListener 在线人数统计
package com.example.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 在线人数统计监听器
* 统计当前在线用户数(基于 Session 数量)
*/
@Slf4j
@Component
public class OnlineCountListener implements HttpSessionListener {
/**
* 在线人数(使用原子类保证线程安全)
*/
private static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0);
@Override
public void sessionCreated(HttpSessionEvent se) {
int count = ONLINE_COUNT.incrementAndGet();
log.info("用户上线,sessionId: {},当前在线人数: {}", se.getSession().getId(), count);
// 将在线人数存入 ServletContext,供页面展示
se.getSession().getServletContext().setAttribute("onlineCount", count);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
int count = ONLINE_COUNT.decrementAndGet();
// 防止负数(异常情况下)
if (count < 0) {
count = 0;
ONLINE_COUNT.set(0);
}
log.info("用户下线,sessionId: {},当前在线人数: {}", se.getSession().getId(), count);
se.getSession().getServletContext().setAttribute("onlineCount", count);
}
/**
* 获取当前在线人数
*/
public static int getOnlineCount() {
return ONLINE_COUNT.get();
}
}
提供查询接口:
@RestController
@RequestMapping("/api/online")
public class OnlineController {
@GetMapping("/count")
public Result<Integer> getOnlineCount() {
return Result.success(OnlineCountListener.getOnlineCount());
}
}
注意:基于 Session 的在线人数统计只适用于单体应用。分布式环境下,应使用 Redis 统计在线人数(如维护一个在线用户 Set)。
4.7 实战三:ServletRequestListener 请求计数与耗时统计
package com.example.listener;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.atomic.AtomicLong;
/**
* 请求计数与耗时统计监听器
* 统计总请求数、当前并发请求数、请求耗时
*/
@Slf4j
@Component
public class RequestStatsListener implements ServletRequestListener {
/**
* 总请求数
*/
private static final AtomicLong TOTAL_REQUESTS = new AtomicLong(0);
/**
* 当前并发请求数
*/
private static final AtomicLong CURRENT_CONCURRENCY = new AtomicLong(0);
/**
* 请求开始时间的属性名
*/
private static final String START_TIME_ATTR = "request_start_time";
@Autowired
private MeterRegistry meterRegistry;
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
// 记录开始时间
request.setAttribute(START_TIME_ATTR, System.currentTimeMillis());
// 总请求数 +1
long total = TOTAL_REQUESTS.incrementAndGet();
// 并发数 +1
long concurrency = CURRENT_CONCURRENCY.incrementAndGet();
// Micrometer 指标
meterRegistry.counter("request.total", "uri", request.getRequestURI()).increment();
meterRegistry.gauge("request.concurrency", concurrency);
log.debug("请求开始,URI: {},总请求数: {},当前并发: {}",
request.getRequestURI(), total, concurrency);
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
// 并发数 -1
long concurrency = CURRENT_CONCURRENCY.decrementAndGet();
// 计算耗时
Long startTime = (Long) request.getAttribute(START_TIME_ATTR);
long cost = startTime != null ? System.currentTimeMillis() – startTime : –1;
log.debug("请求结束,URI: {},耗时: {}ms,当前并发: {}",
request.getRequestURI(), cost, concurrency);
}
/**
* 获取总请求数
*/
public static long getTotalRequests() {
return TOTAL_REQUESTS.get();
}
/**
* 获取当前并发数
*/
public static long getCurrentConcurrency() {
return CURRENT_CONCURRENCY.get();
}
}
4.8 实战四:HttpSessionAttributeListener 监听属性变化
package com.example.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
/**
* Session 属性变化监听器
* 监听 Session 中属性的添加、删除、修改
*/
@Slf4j
@Component
public class SessionAttributeListener implements HttpSessionAttributeListener {
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
log.info("Session 属性添加,sessionId: {},属性名: {},属性值: {}",
event.getSession().getId(), event.getName(), event.getValue());
// 典型应用:监听用户登录
if ("currentUser".equals(event.getName())) {
log.info("用户登录成功,用户信息: {}", event.getValue());
// 可以在这里记录登录日志、更新最后登录时间等
}
}
@Override
public void attributeRemoved(HttpSessionBindingEvent event) {
log.info("Session 属性移除,sessionId: {},属性名: {},属性值: {}",
event.getSession().getId(), event.getName(), event.getValue());
// 典型应用:监听用户退出
if ("currentUser".equals(event.getName())) {
log.info("用户退出登录,用户信息: {}", event.getValue());
}
}
@Override
public void attributeReplaced(HttpSessionBindingEvent event) {
log.info("Session 属性修改,sessionId: {},属性名: {},新值: {}",
event.getSession().getId(), event.getName(), event.getValue());
}
}
4.9 实战五:Spring 自定义事件与监听器(ApplicationEvent)
Spring 提供了一套完整的事件机制,基于 ApplicationEvent 和 ApplicationListener,比 Servlet 监听器更强大,支持 Spring 容器内的所有 Bean。
第一步:定义自定义事件
package com.example.event;
import com.example.entity.User;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* 用户注册事件
*/
@Getter
public class UserRegisterEvent extends ApplicationEvent {
private final User user;
private final String registerIp;
public UserRegisterEvent(Object source, User user, String registerIp) {
super(source);
this.user = user;
this.registerIp = registerIp;
}
}
第二步:发布事件
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void register(UserRegisterDTO dto) {
// 1. 用户注册业务逻辑
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(dto.getPassword());
user.setEmail(dto.getEmail());
userMapper.insert(user);
// 2. 发布用户注册事件
// 发布事件后,所有监听该事件的监听器都会被触发
eventPublisher.publishEvent(new UserRegisterEvent(this, user, dto.getRegisterIp()));
log.info("用户注册成功,userId: {}", user.getId());
}
}
第三步:定义监听器(方式一:实现 ApplicationListener 接口)
package com.example.listener;
import com.example.event.UserRegisterEvent;
import com.example.service.EmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* 用户注册事件监听器 – 发送欢迎邮件
*/
@Slf4j
@Component
public class UserRegisterEmailListener implements ApplicationListener<UserRegisterEvent> {
@Autowired
private EmailService emailService;
@Override
public void onApplicationEvent(UserRegisterEvent event) {
log.info("监听到用户注册事件,开始发送欢迎邮件,userId: {}", event.getUser().getId());
try {
emailService.sendWelcomeEmail(event.getUser().getEmail(), event.getUser().getUsername());
log.info("欢迎邮件发送成功,userId: {}", event.getUser().getId());
} catch (Exception e) {
log.error("欢迎邮件发送失败,userId: {}", event.getUser().getId(), e);
// 邮件发送失败不影响主流程
}
}
}
第三步:定义监听器(方式二:使用 @EventListener 注解,更灵活)
package com.example.listener;
import com.example.event.UserRegisterEvent;
import com.example.service.SmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 用户注册事件监听器 – 发送注册短信
*/
@Slf4j
@Component
public class UserRegisterSmsListener {
@Autowired
private SmsService smsService;
/**
* 使用 @EventListener 注解监听事件
* @Order 控制多个监听器的执行顺序(数字越小越先执行)
*/
@EventListener
@Order(1)
public void handleUserRegister(UserRegisterEvent event) {
log.info("监听到用户注册事件,开始发送注册短信,userId: {}", event.getUser().getId());
try {
smsService.sendRegisterSms(event.getUser().getPhone(), event.getUser().getUsername());
log.info("注册短信发送成功,userId: {}", event.getUser().getId());
} catch (Exception e) {
log.error("注册短信发送失败,userId: {}", event.getUser().getId(), e);
}
}
/**
* 同一个类中可以监听多个事件
* 也可以通过 condition 属性条件过滤
*/
@EventListener(condition = "#event.user.username != null")
public void handleUserRegisterLog(UserRegisterEvent event) {
log.info("记录用户注册日志,username: {}, ip: {}",
event.getUser().getUsername(), event.getRegisterIp());
}
}
4.10 实战六:Spring 异步事件监听(@Async + @EventListener)
默认情况下,Spring 事件是同步执行的,发布事件的线程会等待所有监听器执行完成。如果监听器中有耗时操作(如发送邮件、调用第三方接口),会影响主流程性能。可以使用 @Async 实现异步事件监听。
第一步:启用异步
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
第二步:配置异步线程池
@Configuration
public class AsyncConfig {
@Bean("eventExecutor")
public Executor eventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("event-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
第三步:异步事件监听器
package com.example.listener;
import com.example.event.UserRegisterEvent;
import com.example.service.EmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* 异步用户注册事件监听器
* 使用 @Async 注解,监听器方法会在独立线程中执行,不阻塞主流程
*/
@Slf4j
@Component
public class AsyncUserRegisterListener {
@Autowired
private EmailService emailService;
/**
* 异步发送欢迎邮件
* @Async("eventExecutor") 指定使用的线程池
*/
@Async("eventExecutor")
@EventListener
public void sendWelcomeEmail(UserRegisterEvent event) {
log.info("【异步】开始发送欢迎邮件,线程: {},userId: {}",
Thread.currentThread().getName(), event.getUser().getId());
try {
// 模拟耗时操作
Thread.sleep(2000);
emailService.sendWelcomeEmail(event.getUser().getEmail(), event.getUser().getUsername());
log.info("【异步】欢迎邮件发送成功,userId: {}", event.getUser().getId());
} catch (Exception e) {
log.error("【异步】欢迎邮件发送失败,userId: {}", event.getUser().getId(), e);
}
}
/**
* 异步记录注册日志
*/
@Async("eventExecutor")
@EventListener
public void recordRegisterLog(UserRegisterEvent event) {
log.info("【异步】记录注册日志,userId: {}, ip: {}",
event.getUser().getId(), event.getRegisterIp());
}
}
异步事件注意事项:
事务事件监听器示例:
@Component
@Slf4j
public class TransactionalUserRegisterListener {
/**
* 事务事件监听器
* phase = AFTER_COMMIT:在事务提交后执行(默认)
* phase = AFTER_ROLLBACK:在事务回滚后执行
* phase = AFTER_COMPLETION:在事务完成后(提交或回滚)执行
* phase = BEFORE_COMMIT:在事务提交前执行
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(UserRegisterEvent event) {
log.info("【事务提交后】用户注册成功,userId: {}", event.getUser().getId());
// 在这里执行只有在事务提交成功后才做的操作,如发送通知
}
}
五、三者执行顺序与嵌套关系深度解析
5.1 请求进入时的完整执行链
客户端
│
▼
【Tomcat 连接器】接收请求,创建 Request/Response 对象
│
▼
【Filter 链】(Servlet 容器层,正序执行)
│
├── Filter1.init() → 容器启动时执行一次
├── Filter1.doFilter() 前置逻辑
│ │
│ ├── Filter2.doFilter() 前置逻辑
│ │ │
│ │ ├── Filter3.doFilter() 前置逻辑
│ │ │ │
│ │ │ ▼
│ │ │ 【DispatcherServlet】(Spring MVC 入口)
│ │ │ │
│ │ │ ├── 1. 检查是否有 Handler 映射
│ │ │ │
│ │ │ ├── 2. 【Interceptor 链】preHandle(正序)
│ │ │ │ ├── Interceptor1.preHandle() → true
│ │ │ │ ├── Interceptor2.preHandle() → true
│ │ │ │ └── Interceptor3.preHandle() → true
│ │ │ │
│ │ │ ├── 3. 【Controller 方法】执行业务逻辑
│ │ │ │
│ │ │ ├── 4. 【Interceptor 链】postHandle(倒序)
│ │ │ │ ├── Interceptor3.postHandle()
│ │ │ │ ├── Interceptor2.postHandle()
│ │ │ │ └── Interceptor1.postHandle()
│ │ │ │
│ │ │ └── 5. 视图渲染(JSON 序列化 / 模板渲染)
│ │ │
│ │ ├── Filter3.doFilter() 后置逻辑(chain.doFilter() 之后的代码)
│ │ │
│ ├── Filter2.doFilter() 后置逻辑
│ │
├── Filter1.doFilter() 后置逻辑
│
▼
【Tomcat 连接器】将响应发送给客户端
5.2 响应返回时的完整执行链
响应返回时,执行顺序与请求进入时完全相反:
Controller 方法执行完成
│
▼
Interceptor3.postHandle()
│
▼
Interceptor2.postHandle()
│
▼
Interceptor1.postHandle()
│
▼
视图渲染(HttpMessageConverter 转换返回值)
│
▼
Interceptor3.afterCompletion()
│
▼
Interceptor2.afterCompletion()
│
▼
Interceptor1.afterCompletion()
│
▼
Filter3.doFilter() 后置逻辑(chain.doFilter() 之后)
│
▼
Filter2.doFilter() 后置逻辑
│
▼
Filter1.doFilter() 后置逻辑
│
▼
响应发送给客户端
5.3 异常发生时的执行链变化
场景一:Controller 方法中抛出异常
Interceptor1.preHandle() → true
Interceptor2.preHandle() → true
Controller 方法 → 抛出异常
│
▼
异常被 DispatcherServlet 捕获,跳过 postHandle
│
▼
【全局异常处理器】@ExceptionHandler 处理异常,返回 Result
│
▼
Interceptor2.afterCompletion()(ex 参数不为 null)
Interceptor1.afterCompletion()
Filter 后置逻辑正常执行
场景二:Interceptor2.preHandle() 返回 false
Interceptor1.preHandle() → true
Interceptor2.preHandle() → false(中断请求)
│
▼
跳过 Controller 和后续拦截器
│
▼
Interceptor1.afterCompletion()(只有 preHandle 返回 true 的拦截器执行)
(Interceptor2 和 Interceptor3 的 afterCompletion 不执行)
│
▼
Filter 后置逻辑正常执行
场景三:Filter 中抛出异常
Filter1.doFilter() 前置 → 正常
Filter2.doFilter() 前置 → 抛出异常
│
▼
异常向上传播,被 Tomcat 容器捕获
│
▼
Filter1.doFilter() 后置逻辑不执行(因为 chain.doFilter() 抛出了异常)
│
▼
Tomcat 返回错误页面(或 ErrorController 处理)
重要:Filter 中抛出的异常不会被 Spring 的全局异常处理器(@ControllerAdvice)捕获,因为异常发生在 Servlet 层,还没进入 DispatcherServlet。需要在 Filter 内部 try-catch 或使用 ErrorController 处理。
5.4 代码验证:打印完整执行顺序
创建一个测试项目,配置 2 个 Filter 和 2 个 Interceptor,观察执行顺序:
// Filter1
@Slf4j
public class TestFilter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("【Filter1】前置");
chain.doFilter(request, response);
log.info("【Filter1】后置");
}
}
// Filter2
@Slf4j
public class TestFilter2 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("【Filter2】前置");
chain.doFilter(request, response);
log.info("【Filter2】后置");
}
}
// Interceptor1
@Slf4j
@Component
public class TestInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("【Interceptor1】preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
log.info("【Interceptor1】postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
log.info("【Interceptor1】afterCompletion");
}
}
// Interceptor2
@Slf4j
@Component
public class TestInterceptor2 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("【Interceptor2】preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
log.info("【Interceptor2】postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
log.info("【Interceptor2】afterCompletion");
}
}
执行日志输出:
【Filter1】前置
【Filter2】前置
【Interceptor1】preHandle
【Interceptor2】preHandle
→ Controller 方法执行
【Interceptor2】postHandle
【Interceptor1】postHandle
【Interceptor2】afterCompletion
【Interceptor1】afterCompletion
【Filter2】后置
【Filter1】后置
六、常见问题与解法对比
6.1 过滤器不生效的常见原因
| 没有加 @WebFilter 且启动类没有 @ServletComponentScan | 添加注解,或改用 FilterRegistrationBean |
| @WebFilter 的 urlPatterns 配置错误 | 检查路径模式,/* 表示所有请求 |
| Filter 类没有被 Spring 扫描到 | 确保在启动类的扫描包下,或使用 @Import |
| 使用 @Component 注册但路径被其他 Filter 覆盖 | 改用 FilterRegistrationBean 精确配置 |
| 过滤器中抛出异常导致后续不执行 | 在 Filter 中 try-catch,确保 chain.doFilter() 正常调用 |
| 顺序问题导致被前面的 Filter 拦截中断 | 调整 setOrder(),确保目标 Filter 在前 |
6.2 拦截器不生效的常见原因
| 没有实现 HandlerInterceptor 接口 | 实现接口或继承 HandlerInterceptorAdapter(已废弃) |
| 没有在 WebMvcConfigurer 中注册 | 在 addInterceptors 方法中注册 |
| addPathPatterns 配置错误 | 检查路径,/** 匹配所有多层路径 |
| 被 excludePathPatterns 排除了 | 检查排除路径是否包含目标 URL |
| 有多个 WebMvcConfigurer 配置类冲突 | 合并到一个配置类,或确保不冲突 |
| 拦截器中注入 Bean 为 null | 确保拦截器由 Spring 管理(加 @Component),不要 new |
| 静态资源请求不经过拦截器 | 静态资源默认不经过 DispatcherServlet,需确认请求类型 |
6.3 过滤器中无法注入 Bean 的问题
问题原因:Filter 是 Servlet 容器管理的,不是 Spring 容器管理的,所以 @Autowired 不生效。
解法一:使用 FilterRegistrationBean 注册(推荐)
@Configuration
public class FilterConfig {
@Autowired
private UserService userService; // 在配置类中注入
@Bean
public FilterRegistrationBean<MyFilter> myFilterRegistration() {
FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>();
// 通过构造方法传入依赖
registration.setFilter(new MyFilter(userService));
registration.addUrlPatterns("/*");
return registration;
}
}
// Filter 通过构造方法注入
public class MyFilter implements Filter {
private final UserService userService;
public MyFilter(UserService userService) {
this.userService = userService;
}
}
解法二:Filter 加 @Component,由 Spring 管理
@Component
public class MyFilter implements Filter {
@Autowired
private UserService userService; // 可以注入
}
解法三:通过 SpringContextUtils 手动获取 Bean
public class MyFilter implements Filter {
private UserService userService;
@Override
public void init(FilterConfig filterConfig) {
// 手动从 Spring 容器获取
ServletContext servletContext = filterConfig.getServletContext();
ApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
this.userService = context.getBean(UserService.class);
}
}
6.4 拦截器中无法注入 Bean 的问题
问题原因:如果在 WebMvcConfigurer 中使用 new MyInterceptor() 创建拦截器,它不受 Spring 管理,无法注入 Bean。
解法一:拦截器加 @Component,在配置类中注入(推荐)
@Component
public class MyInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService; // 可以注入
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor; // 注入 Spring 管理的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("/**");
}
}
解法二:在配置类中手动创建并注入依赖
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private UserService userService;
@Bean
public MyInterceptor myInterceptor() {
return new MyInterceptor(userService); // 构造方法注入
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor()).addPathPatterns("/**");
}
}
6.5 过滤器与拦截器如何选择
| 统一字符编码 | Filter | 需要在请求进入 Servlet 之前设置编码 |
| 跨域配置(CORS) | Filter | 跨域预检请求(OPTIONS)可能不经过 DispatcherServlet |
| XSS/SQL 注入防护 | Filter | 需要包装 Request,对所有参数进行转义 |
| 请求/响应日志 | Filter | 需要记录完整的请求体和响应体 |
| 登录状态校验 | Interceptor | 需要获取 Controller 方法信息,排除特定接口 |
| 权限校验 | Interceptor | 基于注解的细粒度权限控制 |
| 性能监控 | Interceptor | 需要区分 Controller 方法,精确到接口级别 |
| 防重复提交 | Interceptor | 基于注解,只对特定接口生效 |
| 静态资源处理 | Filter | 静态资源不经过 DispatcherServlet |
| 文件上传大小限制 | Filter | 需要在请求解析前限制大小 |
选择原则:
6.6 静态资源被拦截的问题
问题:配置了 addPathPatterns("/**") 后,静态资源(JS、CSS、图片)也被拦截了。
解法一:在拦截器中排除静态资源路径
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/static/**", "/public/**", "/resources/**",
"/**/*.js", "/**/*.css", "/**/*.png", "/**/*.jpg",
"/**/*.ico", "/webjars/**", "/swagger-ui/**"
);
解法二:只拦截业务接口路径
// 只拦截 /api 开头的请求,不拦截静态资源
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**");
解法三:在拦截器的 preHandle 中判断请求类型
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 静态资源请求的 handler 不是 HandlerMethod,直接放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 业务逻辑…
return true;
}
6.7 跨域配置用过滤器还是拦截器
推荐用 Filter(CorsFilter),原因:
SpringBoot 推荐配置方式:
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许的源
config.addAllowedOriginPattern("*");
// 允许的请求方法
config.addAllowedMethod("*");
// 允许的请求头
config.addAllowedHeader("*");
// 是否允许携带 Cookie
config.setAllowCredentials(true);
// 预检请求缓存时间
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
注意:不要同时使用 CorsFilter 和 WebMvcConfigurer.addCorsMappings(),会导致重复配置。选择一种即可。
6.8 过滤器中读取请求体后 Controller 拿不到数据
问题原因:HttpServletRequest.getInputStream() 只能读一次,过滤器中读取后流的位置已经到末尾,Controller 再读就没有数据了。
解法一:使用 ContentCachingRequestWrapper(Spring 提供)
@WebFilter(urlPatterns = "/*")
public class RequestLogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// Spring 提供的包装类,会缓存请求体
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(httpRequest);
chain.doFilter(wrappedRequest, response);
// 请求处理完成后,可以从 wrapper 中获取缓存的请求体
byte[] content = wrappedRequest.getContentAsByteArray();
String body = new String(content, StandardCharsets.UTF_8);
log.info("请求体: {}", body);
}
}
注意:ContentCachingRequestWrapper 只能在 chain.doFilter() 之后获取请求体(因为它是在 Controller 读取时才缓存的)。如果需要在 chain.doFilter() 之前获取请求体,需要自定义 Wrapper(如本文 2.8 节的 CachedBodyHttpServletRequest)。
解法二:自定义可重复读取的 RequestWrapper(本文 2.8 节)
适用于需要在过滤器前置逻辑中读取请求体的场景。
七、生产级最佳实践
7.1 三者职责划分原则
┌─────────────────────────────────────────────────────┐
│ 请求处理链路 │
├─────────────────────────────────────────────────────┤
│ │
│ Filter(过滤器) │
│ ├── 字符编码设置 │
│ ├── 跨域处理(CORS) │
│ ├── XSS / SQL 注入防护 │
│ ├── 请求/响应日志记录 │
│ ├── 请求体包装(可重复读取) │
│ └── 全局安全防护(如 IP 黑白名单) │
│ │
│ Interceptor(拦截器) │
│ ├── 登录状态校验 │
│ ├── 接口权限校验(RBAC) │
│ ├── 数据权限过滤 │
│ ├── 接口性能监控 │
│ ├── 防重复提交 │
│ ├── 操作日志记录(基于注解) │
│ └── ThreadLocal 上下文管理(设置/清理) │
│ │
│ Listener(监听器) │
│ ├── 应用启动初始化/关闭清理 │
│ ├── 在线人数统计 │
│ ├── 请求计数/并发统计 │
│ ├── Session 属性变化监听(登录/退出日志) │
│ └── Spring 事件驱动(解耦业务逻辑) │
│ │
└─────────────────────────────────────────────────────┘
7.2 性能优化建议
7.3 安全加固建议
7.4 可观测性建议
八、完整项目结构与代码汇总
8.1 项目目录结构
src/main/java/com/example/
├── ExampleApplication.java # 启动类(@ServletComponentScan, @EnableAsync)
│
├── common/
│ ├── annotation/
│ │ ├── RequirePermission.java # 权限校验注解
│ │ ├── RepeatSubmit.java # 防重复提交注解
│ │ └── DataScope.java # 数据权限注解
│ ├── context/
│ │ ├── UserContext.java # 用户上下文(ThreadLocal)
│ │ ├── UserInfo.java # 用户信息
│ │ └── DataScopeContext.java # 数据权限上下文
│ └── result/
│ ├── Result.java # 统一响应封装
│ └── ResultCode.java # 状态码枚举
│
├── config/
│ ├── FilterConfig.java # 过滤器注册配置
│ ├── WebMvcConfig.java # 拦截器注册 + 静态资源 + 跨域
│ ├── ListenerConfig.java # 监听器注册配置
│ ├── AsyncConfig.java # 异步线程池配置
│ └── CorsConfig.java # 跨域配置
│
├── filter/
│ ├── EncodingFilter.java # 字符编码过滤器
│ ├── XssFilter.java # XSS 防护过滤器
│ ├── XssHttpServletRequestWrapper.java # XSS 请求包装器
│ ├── SqlInjectFilter.java # SQL 注入防护过滤器
│ ├── CachedBodyFilter.java # 请求体缓存过滤器
│ ├── CachedBodyHttpServletRequest.java # 可重复读取请求包装器
│ ├── RequestLogFilter.java # 请求日志过滤器
│ └── TraceIdFilter.java # 链路追踪过滤器
│
├── interceptor/
│ ├── LoginInterceptor.java # 登录校验拦截器
│ ├── PermissionInterceptor.java # 权限校验拦截器
│ ├── PerformanceInterceptor.java # 性能监控拦截器
│ ├── RepeatSubmitInterceptor.java # 防重复提交拦截器
│ └── DataScopeInterceptor.java # 数据权限拦截器
│
├── listener/
│ ├── AppInitListener.java # 应用启动初始化监听器
│ ├── OnlineCountListener.java # 在线人数统计监听器
│ ├── RequestStatsListener.java # 请求计数监听器
│ ├── SessionAttributeListener.java # Session 属性变化监听器
│ ├── UserRegisterEmailListener.java # 用户注册邮件监听器
│ ├── UserRegisterSmsListener.java # 用户注册短信监听器
│ ├── AsyncUserRegisterListener.java # 异步用户注册监听器
│ └── TransactionalUserRegisterListener.java # 事务事件监听器
│
├── event/
│ └── UserRegisterEvent.java # 用户注册事件
│
├── plugin/
│ └── DataScopeInterceptor.java # 数据权限 MyBatis 插件
│
├── controller/
│ ├── UserController.java # 用户接口
│ ├── AdminController.java # 管理员接口
│ └── OnlineController.java # 在线人数接口
│
└── service/
├── UserService.java # 用户服务(发布事件)
├── PermissionService.java # 权限服务
├── EmailService.java # 邮件服务
├── SmsService.java # 短信服务
└── DictService.java # 字典服务
8.2 核心配置类完整代码
启动类:
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@ServletComponentScan(basePackages = "com.example.filter")
@EnableAsync
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
过滤器配置类:
package com.example.config;
import com.example.filter.*;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TraceIdFilter> traceIdFilterRegistration() {
FilterRegistrationBean<TraceIdFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TraceIdFilter());
registration.addUrlPatterns("/*");
registration.setName("traceIdFilter");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
@Bean
public FilterRegistrationBean<EncodingFilter> encodingFilterRegistration() {
FilterRegistrationBean<EncodingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new EncodingFilter());
registration.addUrlPatterns("/*");
registration.setName("encodingFilter");
registration.setOrder(1);
return registration;
}
@Bean
public FilterRegistrationBean<CachedBodyFilter> cachedBodyFilterRegistration() {
FilterRegistrationBean<CachedBodyFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CachedBodyFilter());
registration.addUrlPatterns("/*");
registration.setName("cachedBodyFilter");
registration.setOrder(2);
return registration;
}
@Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new XssFilter());
registration.addUrlPatterns("/api/*");
registration.setName("xssFilter");
registration.setOrder(3);
return registration;
}
@Bean
public FilterRegistrationBean<RequestLogFilter> requestLogFilterRegistration() {
FilterRegistrationBean<RequestLogFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestLogFilter());
registration.addUrlPatterns("/*");
registration.setName("requestLogFilter");
registration.setOrder(4);
return registration;
}
}
WebMvc 配置类:
package com.example.config;
import com.example.interceptor.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private PerformanceInterceptor performanceInterceptor;
@Autowired
private LoginInterceptor loginInterceptor;
@Autowired
private PermissionInterceptor permissionInterceptor;
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Autowired
private DataScopeInterceptor dataScopeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 1. 性能监控(最高优先级)
registry.addInterceptor(performanceInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/static/**", "/public/**", "/**/*.js", "/**/*.css")
.order(0);
// 2. 登录校验
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/user/login", "/api/user/register", "/api/user/sendCode"
)
.order(1);
// 3. 数据权限
registry.addInterceptor(dataScopeInterceptor)
.addPathPatterns("/api/**")
.order(2);
// 4. 防重复提交
registry.addInterceptor(repeatSubmitInterceptor)
.addPathPatterns("/api/**")
.order(3);
// 5. 权限校验(管理员接口)
registry.addInterceptor(permissionInterceptor)
.addPathPatterns("/api/admin/**")
.order(4);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
}
九、总结
核心要点回顾
Filter(过滤器) 是 Servlet 规范定义的,在 Servlet 容器层执行,早于 Spring MVC,可以包装 Request/Response,适合做编码、XSS、日志、跨域等全局预处理。
Interceptor(拦截器) 是 Spring MVC 提供的,在 DispatcherServlet 内部执行,可以获取 Controller 方法信息,适合做登录、权限、性能监控、防重复提交等接口级别的处理。
Listener(监听器) 基于观察者模式,用于监听应用启动/关闭、Session 创建/销毁、请求创建/销毁、Spring 事件等,适合做初始化、统计、事件驱动解耦。
执行顺序:Filter 前置 → Interceptor preHandle → Controller → Interceptor postHandle → 视图渲染 → Interceptor afterCompletion → Filter 后置。Filter 和 Interceptor 的后置逻辑都是倒序执行。
选择原则:需要修改 Request/Response 用 Filter;需要获取 Controller 方法信息用 Interceptor;需要监听事件用 Listener。
注意事项:Filter 中抛出的异常不会被全局异常处理器捕获;Filter 中读取请求体后需要包装 Request;ThreadLocal 必须在 afterCompletion 中清理;异步事件中无法获取请求上下文。
一句话总结
Filter 是请求的"第一道门",负责全局安全与预处理;Interceptor 是 Controller 的"贴身护卫",负责业务级别的校验与增强;Listener 是系统的"观察者",负责事件驱动与状态监控。三者各司其职、协同工作,共同构建一个健壮、安全、可观测的 Web 应用。
如果本文对你有帮助,欢迎点赞、收藏、关注!有任何问题或建议,欢迎在评论区交流。
网硕互联帮助中心
评论前必须登录!
注册