第27篇-拦截器与过滤器
【Kotlin + Spring Boot 4 从零到架构师】第 27 篇:拦截器与过滤器
本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。
本篇你将学到
- Filter(过滤器)与 Interceptor(拦截器)的区别
HandlerInterceptor的三个生命周期方法OncePerRequestFilter的使用- 执行顺序与使用场景
学完本篇,你将能为 mini-shop 添加请求日志、耗时统计、访问控制等横切功能。
一、Filter vs Interceptor
1.1 执行位置
1.2 核心区别
| 维度 | Filter | Interceptor |
|---|---|---|
| 所属 | Servlet 容器(Tomcat) | Spring MVC |
| 执行时机 | DispatcherServlet 前后 | Controller 前后 |
| 能否访问 Controller | 不能 | 能(知道要执行哪个 Controller 方法) |
| 能否访问 Spring Bean | 需要手动获取 | 可以直接注入 |
| 使用场景 | 编码、CORS、日志、安全 | 权限校验、日志、性能监控 |
| 注册方式 | FilterRegistrationBean | WebMvcConfigurer |
二、HandlerInterceptor
2.1 三个生命周期方法
interfaceHandlerInterceptor{// Controller 执行前funpreHandle(request,response,handler):Boolean// Controller 执行后、视图渲染前funpostHandle(request,response,handler,modelAndView)// 请求完成后(无论成功失败)funafterCompletion(request,response,handler,ex)}| 方法 | 执行时机 | 返回值 | 典型用途 |
|---|---|---|---|
preHandle | Controller 之前 | true:继续;false:中断 | 权限校验、限流 |
postHandle | Controller 之后 | void | 修改 Model、添加 Header |
afterCompletion | 全部完成后 | void | 清理资源、记录日志 |
2.2 实战示例:请求日志 + 耗时统计
packagecom.example.minishop.interceptorimportjakarta.servlet.http.HttpServletRequestimportjakarta.servlet.http.HttpServletResponseimportorg.slf4j.LoggerFactoryimportorg.springframework.stereotype.Componentimportorg.springframework.web.servlet.HandlerInterceptor@ComponentclassRequestLoggingInterceptor:HandlerInterceptor{privatevallog=LoggerFactory.getLogger(RequestLoggingInterceptor::class.java)privatevalstartTimeAttribute="requestStartTime"overridefunpreHandle(request:HttpServletRequest,response:HttpServletResponse,handler:Any):Boolean{valstartTime=System.currentTimeMillis()request.setAttribute(startTimeAttribute,startTime)log.info("→${request.method}${request.requestURI}from${request.remoteAddr}")returntrue// 继续}overridefunafterCompletion(request:HttpServletRequest,response:HttpServletResponse,handler:Any,ex:Exception?){valstartTime=request.getAttribute(startTimeAttribute)asLongvalduration=System.currentTimeMillis()-startTimevalstatus=response.statusvalexceptionInfo=if(ex!=null)" exception=${ex.message}"else""log.info("←${request.method}${request.requestURI}$status${duration}ms$exceptionInfo")}}2.3 注册拦截器
packagecom.example.minishop.configimportcom.example.minishop.interceptor.RequestLoggingInterceptorimportorg.springframework.context.annotation.Configurationimportorg.springframework.web.servlet.config.annotation.InterceptorRegistryimportorg.springframework.web.servlet.config.annotation.WebMvcConfigurer@ConfigurationclassWebConfig(privatevalrequestLoggingInterceptor:RequestLoggingInterceptor):WebMvcConfigurer{overridefunaddInterceptors(registry:InterceptorRegistry){registry.addInterceptor(requestLoggingInterceptor).addPathPatterns("/api/**") // 拦截 /api/ 下所有请求 .excludePathPatterns("/api/health") // 排除健康检查端点 } }2.4 实战示例:简单权限校验
@ComponentclassAuthInterceptor(// 拦截器可以注入 Spring BeanprivatevalauthService:AuthService):HandlerInterceptor{overridefunpreHandle(request:HttpServletRequest,response:HttpServletResponse,handler:Any):Boolean{valtoken=request.getHeader("Authorization")if(token.isNullOrBlank()){response.contentType="application/json;charset=UTF-8"response.status=HttpServletResponse.SC_UNAUTHORIZED response.writer.write("""{"code":401,"message":"未提供认证 Token"}""")returnfalse// 中断请求}valisValid=authService.validateToken(token)if(!isValid){response.contentType="application/json;charset=UTF-8"response.status=HttpServletResponse.SC_UNAUTHORIZED response.writer.write("""{"code":401,"message":"Token 无效或已过期"}""")returnfalse}returntrue// 放行}}注册时指定拦截路径:
overridefunaddInterceptors(registry:InterceptorRegistry){registry.addInterceptor(authInterceptor).addPathPatterns("/api/**") .excludePathPatterns( "/api/auth/login", // 登录接口不拦截 "/api/auth/register", // 注册接口不拦截 "/api/health", // 健康检查不拦截 "/api/products/**" // 商品浏览不拦截(公开接口) ) }三、Filter
3.1 OncePerRequestFilter
Spring 提供了OncePerRequestFilter,确保每个请求只执行一次(避免请求转发时重复执行):
packagecom.example.minishop.filterimportjakarta.servlet.FilterChainimportjakarta.servlet.http.HttpServletRequestimportjakarta.servlet.http.HttpServletResponseimportorg.slf4j.LoggerFactoryimportorg.springframework.stereotype.Componentimportorg.springframework.web.filter.OncePerRequestFilter@ComponentclassRequestIdFilter:OncePerRequestFilter(){privatevallog=LoggerFactory.getLogger(RequestIdFilter::class.java)overridefundoFilterInternal(request:HttpServletRequest,response:HttpServletResponse,filterChain:FilterChain){// 1. 从请求头获取或生成请求 IDvalrequestId=request.getHeader("X-Request-Id")?:generateRequestId()// 2. 设置到响应头中(方便客户端排查)response.setHeader("X-Request-Id",requestId)// 3. 存入请求属性(供后续 Interceptor / Controller 使用)request.setAttribute("requestId",requestId)try{// 4. 继续过滤器链filterChain.doFilter(request,response)}finally{log.debug("请求处理完成:requestId=$requestId")}}privatefungenerateRequestId():String{returnjava.util.UUID.randomUUID().toString().take(8)}}3.2 Filter 的注册
Spring Boot 会自动注册带有@Component的 Filter。如果需要自定义 URL 匹配:
@ConfigurationclassFilterConfig{@BeanfunrequestIdFilterRegistration(filter:RequestIdFilter):FilterRegistrationBean<RequestIdFilter>{valregistration=FilterRegistrationBean(filter)registration.addUrlPatterns("/api/*") // 只拦截 /api/ 路径 registration.order = 1 // 执行顺序(数字越小越先执行) return registration } }四、执行顺序总结
五、选择指南
| 需求 | 选择 | 原因 |
|---|---|---|
| CORS 处理 | Filter | 在最外层处理,不进入 Spring MVC |
| 字符编码设置 | Filter | 最早的时机设置编码 |
| 请求日志 | Interceptor | 能拿到 Controller 信息 |
| 耗时统计 | Interceptor | 精确到 Controller 执行时间 |
| 权限/角色校验 | Interceptor | 能获取 Handler 信息 |
| 请求 ID 追踪 | Filter | 最外层生成,全链路可用 |
| 限流 | Filter 或 Interceptor | 都可以,看是否需要 Spring 上下文 |
本篇小结
| 知识点 | 核心内容 |
|---|---|
| Filter | Servlet 层,DispatcherServlet 之前/之后 |
| Interceptor | Spring MVC 层,Controller 之前/之后 |
preHandle | 返回 false 中断请求 |
postHandle | Controller 之后,可修改 Model |
afterCompletion | 最终清理,无论成功失败 |
OncePerRequestFilter | 确保每个请求只过滤一次 |
WebMvcConfigurer | 注册拦截器 + 配置路径 |
| 执行顺序 | Filter → Interceptor → Controller |
下篇预告
第 28 篇:CORS 与文件上传
前端跨域请求被拦截怎么办?商品图片怎么上传到服务器?下一篇解决这两个常见的 Web 开发问题。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。
