十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring MVC 中 Filter、Interceptor 与 ResponseBodyAdvice 的区别及选型

Spring MVC 中 Filter、Interceptor 与 ResponseBodyAdvice 的区别及选型 在 Spring MVC 项目里经常会遇到三类需求从请求头中读取语言、Token、租户等信息在 Controller 执行前后做鉴权、日志、耗时统计在响应返回客户端前统一修改返回数据例如国际化、脱敏、加密。这三类需求都带有“拦截”的意味因此很容易产生一个问题Filter、Interceptor、ResponseBodyAdvice到底该选哪个结论先说不要按“谁都能拦截”来选而要按“我要处理的数据处于什么状态”来选。本文以统一响应国际化为例梳理这三个组件的职责、执行位置和选型边界。一、先看请求在 Spring MVC 中是怎么流转的一个普通 REST 请求大致会经历以下流程客户端请求 ↓ Filter ↓ Interceptor.preHandle ↓ Controller ↓ ResponseBodyAdvice ↓ HttpMessageConverter对象序列化为 JSON ↓ Interceptor.postHandle / afterCompletion ↓ Filter 返回 ↓ 客户端响应这个流程决定了三者拿到的数据并不一样组件所在层级主要操作对象FilterServlet 容器层HttpServletRequest、HttpServletResponse、输入输出流InterceptorSpring MVC 请求处理层请求、响应、Controller 方法信息ResponseBodyAdviceSpring MVC 响应处理层Controller 返回的 Java 对象理解这一点选型就清晰了我要改什么就去离它最近的那一层处理。二、ResponseBody和ResponseBodyAdvice有什么区别这两个名称很接近但一个是“开关”一个是“增强器”。1.ResponseBody决定返回什么ResponseBody是一个注解它告诉 SpringController 方法的返回值不需要跳转到页面而是直接写入 HTTP 响应体。例如ResponseBody GetMapping(/user) public ResultUser getUser() { return Result.ok(user); }Spring 会把ResultUser交给HttpMessageConverter最终转换为 JSON。RestController可以理解为Controller ResponseBody所以 REST 接口通常不再单独写ResponseBody。2.ResponseBodyAdvice决定返回前是否修改ResponseBodyAdvice是 Spring 提供的响应增强接口。它的执行时机是Controller 已经返回 Java 对象但对象还没有被序列化为 JSON。这意味着我可以直接修改返回对象而不需要解析 JSON 字符串。Result 对象 ↓ ResponseBodyAdvice 可修改对象 ↓ 序列化为 JSON ↓ 返回客户端这正适合做响应国际化统一响应包装手机号、身份证等字段脱敏响应字段加密统一补充时间戳、TraceId 等信息。三、Filter适合处理请求入口和底层能力Filter 属于 Servlet 规范运行位置比 Spring MVC 更靠前。它不关心当前请求最终会进入哪个 Controller只负责拦截 HTTP 请求和响应。我通常会在以下场景使用 FilterCORS字符编码Token 或签名校验请求日志读取语言、租户等请求头初始化和清理ThreadLocal上下文。例如国际化场景中的语言识别就很适合放在 Filter 中Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求头读取语言例如 zh-CN、en-US String lang request.getHeader(lang); // 将当前请求语言保存到 ThreadLocal // 后续 Controller、异常处理器、ResponseBodyAdvice 都可以读取。 LangContext.set(resolveLang(lang)); try { filterChain.doFilter(request, response); } finally { // Web 容器会复用线程必须清理避免语言串到下一次请求。 LangContext.clear(); } }这里的职责很单纯识别当前请求使用什么语言并把它保存下来。但 Filter 不适合直接修改Result.msg。原因是 Filter 在响应阶段通常拿到的是输出流中的内容而不是Result对象。如果强行改响应体往往需要包装HttpServletResponse缓存已经输出的 JSON把 JSON 反序列化为对象修改msg再序列化为 JSON写回原始响应流。这不仅代码复杂还要处理字符集、响应提交、流关闭、文件下载和流式响应等问题。需要注意的是Filter 并不是不能注入 Spring Bean。在 Spring Boot 中将 Filter 交给 Spring 管理后同样可以注入 Service。Filter 不适合响应国际化的核心原因不是注入能力而是它拿到的是“流态数据”而不是“对象态数据”。四、Interceptor适合控制 Controller 的执行流程Interceptor 是 Spring MVC 提供的拦截器它比 Filter 更接近 Controller。它最常用的三个方法是preHandle() // Controller 执行前 postHandle() // Controller 执行后 afterCompletion() // 请求完成后它适合做登录校验权限校验接口访问日志耗时统计设置请求属性Controller 级别的请求拦截。例如记录接口耗时Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 记录请求开始时间供 afterCompletion 计算耗时 request.setAttribute(startTime, System.currentTimeMillis()); return true; }但 Interceptor 不适合修改统一响应对象。关键原因是postHandle拿不到 Controller 的返回值。void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)对于 REST 接口而言Controller 返回的Result?已经被 Spring 的返回值处理器接管Interceptor 的方法参数中没有Result?。此时如果想修改msg同样只能走“缓存响应流、解析 JSON、重新写回”的绕路方案。所以我会这样理解Interceptor 负责“这个请求能不能进、执行得怎么样”ResponseBodyAdvice负责“这个响应最终长什么样”。五、ResponseBodyAdvice专门处理响应对象当需求是统一修改 Controller 返回对象时ResponseBodyAdvice是最直接的选择。以国际化为例Controller 只需返回统一的错误码和资源标识return Result.fail(ResultCode.LOGIN_FAILED);响应增强器在 JSON 序列化前根据当前语言翻译msgRestControllerAdvice Order(Ordered.LOWEST_PRECEDENCE) public class I18nResponseBodyAdvice implements ResponseBodyAdviceResult? { Override public boolean supports(MethodParameter returnType, Class? extends HttpMessageConverter? converterType) { // 只增强统一响应 Result避免误处理文件下载、普通字符串等响应。 return Result.class.isAssignableFrom(returnType.getParameterType()); } Override public Result? beforeBodyWrite(Result? body, MethodParameter returnType, MediaType contentType, Class? extends HttpMessageConverter? converterType, ServerHttpRequest request, ServerHttpResponse response) { if (body null) { return null; } // 当前语言由请求入口的 Filter 写入 ThreadLocal。 String lang LangContext.get(); if (lang null) { lang en-US; } // 使用稳定的资源标识查询翻译文案。 String translated I18nServiceHolder.translate(body.getResId(), lang); // 未配置翻译时保留原始默认消息。 if (translated null || translated.isBlank()) { translated body.getMsg(); } // Result 是不可变对象创建一份只修改 msg 的新对象。 return body.withMsg(translated); } }这段代码的重点不在“翻译”本身而在于它操作的是还没有序列化的Result对象。不需要包装响应流不需要反序列化 JSON也不需要让每个 Controller 手动处理语言。六、为什么要加Order(Ordered.LOWEST_PRECEDENCE)一个项目中可以有多个ResponseBodyAdvice。例如响应脱敏 ↓ 补充公共字段 ↓ 国际化翻译 ↓ JSON 序列化Order用于明确它们的执行顺序。Order(Ordered.LOWEST_PRECEDENCE)LOWEST_PRECEDENCE表示最低优先级通常会靠后执行。将国际化放在后面意味着前面的 Advice 可以先完成脱敏、字段补充等工作国际化最后再决定msg应该展示成哪种语言避免后续逻辑把翻译结果覆盖掉。不过Order并不是每个 Advice 都必须添加。只有多个 Advice 共存并且它们可能互相影响时才需要认真设计顺序。七、实际项目中应该怎么选需求推荐组件读取语言、Token、租户、请求链路信息Filter设置和清理ThreadLocalFilter鉴权、日志、接口耗时统计Interceptor修改 Controller 返回的统一响应对象ResponseBodyAdvice响应国际化、脱敏、加密、统一包装ResponseBodyAdvice改写二进制流、处理原始 HTTP 输出Filter最终可以记住一句话Filter 处理请求和响应流。Interceptor 控制 Controller 执行流程。ResponseBodyAdvice 修改 Controller 的响应对象。在统一响应国际化这个场景中Filter 负责确定“用户使用什么语言”ResponseBodyAdvice负责决定“最终返回什么语言的消息”。两者不是替代关系而是一次完整请求中的上下游协作。
返回列表