云计算百科
云计算领域专业知识百科平台

SpringBoot拦截器、过滤器、监听器区别与实战用法

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 核心区别对比表

对比维度Filter 过滤器Interceptor 拦截器Listener 监听器
规范来源 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: UTF8
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("&lt;");
break;
case '>':
sb.append("&gt;");
break;
case '&':
sb.append("&amp;");
break;
case '"':
sb.append("&quot;");
break;
case '\\'':
sb.append("&#x27;");
break;
case '/':
sb.append("&#x2F;");
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

方式执行时机能否注入 Bean适用场景
@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());
}
}

异步事件注意事项:

  • 异步事件中抛出的异常不会传播到发布事件的线程,需要在监听器内部 try-catch
  • 异步事件中无法获取请求上下文(如 RequestContextHolder、UserContext),因为请求线程已经结束
  • 如果需要事务一致性,不要使用异步事件(异步事件不在事务内)
  • 可以使用 @TransactionalEventListener 实现事务事件监听(在事务提交后执行)
  • 事务事件监听器示例:

    @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 需要在请求解析前限制大小

    选择原则:

  • 需要修改 Request/Response 对象 → 用 Filter
  • 需要获取 Controller 方法信息(注解、参数等) → 用 Interceptor
  • 需要在 Servlet 容器层处理(更早执行) → 用 Filter
  • 需要细粒度的接口级别控制 → 用 Interceptor
  • 需要监听事件(启动、Session、请求生命周期) → 用 Listener
  • 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),原因:

  • 跨域预检请求(OPTIONS)不会经过 DispatcherServlet,拦截器无法处理
  • CORS 规范要求在 Servlet 容器层处理跨域头
  • 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 性能优化建议

  • 减少过滤器数量:每个过滤器都会增加一次方法调用,合并功能相似的过滤器
  • 过滤器中避免耗时操作:过滤器中的逻辑会影响所有请求,耗时操作应异步化
  • 拦截器精确匹配路径:使用 addPathPatterns 精确指定拦截路径,避免 /** 全量拦截
  • 静态资源排除:在拦截器中排除静态资源,避免不必要的拦截
  • ThreadLocal 及时清理:在 afterCompletion 中清理 ThreadLocal,防止内存泄漏
  • 请求体缓存限制大小:缓存请求体时设置最大长度,避免大文件导致内存溢出
  • 异步事件解耦:非核心逻辑(如发送邮件、记录日志)使用 Spring 异步事件
  • 7.3 安全加固建议

  • XSS 防护:使用过滤器对所有请求参数进行 HTML 转义
  • SQL 注入防护:使用 MyBatis #{} 预编译,过滤器作为辅助检测
  • 敏感信息脱敏:请求日志中对密码、Token、身份证等敏感字段脱敏
  • 异常信息不泄露:过滤器/拦截器中返回统一错误信息,不暴露系统内部细节
  • IP 黑白名单:在过滤器层实现 IP 访问控制
  • 请求频率限制:在过滤器层实现接口限流(如基于 Redis 的令牌桶算法)
  • 文件上传校验:在过滤器层校验文件类型、大小,防止恶意文件上传
  • 7.4 可观测性建议

  • 请求耗时统计:在过滤器或拦截器中记录每个请求的耗时
  • 慢接口告警:设置耗时阈值,超过阈值记录 WARN 日志并告警
  • Micrometer 指标埋点:对请求数、耗时、异常数进行指标埋点
  • traceId 链路追踪:在过滤器中生成 traceId 并注入 MDC,贯穿整个请求链路
  • 操作审计日志:基于拦截器 + 注解记录用户操作日志(谁、什么时间、做了什么、结果如何)
  • 在线人数监控:基于 HttpSessionListener 统计在线人数,异常波动时告警

  • 八、完整项目结构与代码汇总

    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 应用。


    如果本文对你有帮助,欢迎点赞、收藏、关注!有任何问题或建议,欢迎在评论区交流。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » SpringBoot拦截器、过滤器、监听器区别与实战用法
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!