技术复盘:从 Interceptor 到 Filter ------ 正确修改 HTTP Request 字段的探索之路
引言
在最近的开发中,我们遇到了一个需求:需要统一处理传入的 HTTP 请求,将请求体(Body)中多个可能的用户名字段(如 userName, user, gitName, name)的值,统一替换为从请求头 Authorization中解析出的真实 Git 用户名。 我们的第一直觉是使用熟悉的 Spring Interceptor  来实现这一预处理逻辑,但发现此路不通。本文将详细复盘这个问题,解释为什么 Interceptor 无法胜任,并阐述最终采用 Servlet Filter 方案的原因和实现要点。
一、问题场景与初步方案
目标:
- 请求体可能为 JSON 或 Form-Data。
 - 需要检查其中是否包含 
"userName","user","gitName","name"等字段。 - 若存在,则用认证信息(如 JWT 解析后得到的 
gitName)覆盖其值,确保后续业务逻辑使用的是可信的用户身份。 
初步方案:使用 Interceptor  我们首先尝试在 preHandle方法中实现这一逻辑:
            
            
              java
              
              
            
          
          @Component
public class UsernameOverrideInterceptor implements HandlerInterceptor {
    private static final String[] SUPPORTED_FIELD_NAMES = {"userName", "user", "gitName", "name"};
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从 Authorization 头解析出 gitName
        String gitName = extractGitNameFromAuthorization(request);
        if (gitName == null) {
            return true; // 无法解析则跳过处理
        }
        // 2. 尝试读取并修改请求体
        // ... (代码逻辑)
        // 3. 发现无法直接修改 request 的 InputStream
        return true;
    }
}
        二、为何 Interceptor 行不通?------ 复盘与深度解析
很快,我们遇到了无法逾越的障碍,根本原因在于 Servlet 架构中 Request/Response 的访问机制。
- 
请求体 (Request Body) 的"一次性"读取:
- HttpServletRequest 的 
InputStream或Reader只能被读取一次 。一旦被 Controller 中的@RequestBody注解参数或其它拦截器读取,流就会到达末尾,无法再次读取。 - 在 Interceptor 中读取流以解析 JSON/Form 数据,会消耗掉这个流,导致后续 Controller 无法再获取到请求体数据,从而引发 
HttpMessageNotReadableException等异常。 
 - HttpServletRequest 的 
 - 
Interceptor 的职责定位:
- Interceptor 是 Spring MVC 层面 的组件,它工作在DispatcherServlet 之后。它的主要职责是进行面向 Handler(Controller 方法)的横切处理,如日志、权限检查、模型加工等。
 - 它并非设计用来对原始的 HTTP 请求报文进行"破坏性"的修改。修改请求体属于更底层、更基础的 HTTP 报文处理范畴。
 
 - 
修改 Request 参数的局限性:
HttpServletRequest提供了setAttribute(String name, Object o)方法,但这设置的是属性(Attribute) ,而非参数(Parameter)。- 请求参数(Parameter)主要来自 URL 查询字符串或 Form-Data,对于 JSON Body 中的内容,
getParameter方法是无法获取的。因此,想通过 Interceptor 的request.setParameter()方法来修改 JSON 数据是完全不可能的(该方法本身也不存在)。 
 
结论复盘: Interceptor 适合处理已经解析好的、存在于 MVC 上下文中的信息(如认证对象、模型数据),但不适合处理原始的、未解析的 HTTP 请求报文。
三、正确方案:使用 Servlet Filter
Filter 是解决此问题的正确位置 ,因为它在 Servlet 容器级别工作,是请求进入 Spring MVC 之前的第一个关卡。 Filter 的工作流程:  HTTP Request -> Filter Chain -> DispatcherServlet -> Interceptor -> Controller 正因为 Filter 最先接触到请求,它可以在流被后续组件读取之前,对其进行读取、包装和替换。
核心实现思路:内容缓存和请求包装
- 缓存请求体 :首先将原始的 
HttpServletRequest中的InputStream读取并缓存到一个字节数组或字符串中。 - 修改内容:解析缓存的数据(如使用 Jackson 解析 JSON),找到目标字段并进行替换。
 - 包装请求 :创建一个新的 
HttpServletRequestWrapper子类,重写getInputStream()和getReader()方法,使其返回包含修改后数据的新流。 - 传递包装器:将包装后的 Request 对象传入 Filter Chain,后续所有组件看到的都将是已经被修改过的请求。
 
代码示例概要
            
            
              java
              
              
            
          
          @Component
public class UsernameOverrideFilter extends OncePerRequestFilter {
    private static final String[] SUPPORTED_FIELD_NAMES = {"userName", "user", "gitName", "name"};
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 缓存原始请求
        CachedBodyHttpServletRequest cachedBodyRequest = new CachedBodyHttpServletRequest(request);
        // 2. 从缓存中获取请求体字符串
        String requestBody = IOUtils.toString(cachedBodyRequest.getInputStream(), StandardCharsets.UTF_8);
        // 3. 从 Authorization 头解析 gitName
        String gitName = extractGitNameFromAuthorization(request);
        // 4. 如果请求体非空且成功解析出 gitName,则进行字段替换
        if (StringUtils.isNotBlank(requestBody) && gitName != null) {
            ObjectNode jsonNode = (ObjectNode) JsonUtils.parse(requestBody);
            for (String field : SUPPORTED_FIELD_NAMES) {
                if (jsonNode.has(field)) {
                    jsonNode.put(field, gitName);
                }
            }
            // 获取修改后的 JSON 字符串
            requestBody = jsonNode.toString();
        }
        // 5. 创建新的请求包装器,使用修改后的 body
        ModifiedBodyHttpServletRequestWrapper modifiedRequestWrapper = new ModifiedBodyHttpServletRequestWrapper(cachedBodyRequest, requestBody);
        // 6. 继续执行过滤器链,传入包装后的请求
        filterChain.doFilter(modifiedRequestWrapper, response);
    }
    // Helper 方法:从 Authorization 头提取信息...
    private String extractGitNameFromAuthorization(HttpServletRequest request) {
        // ... 实现逻辑,例如解析 JWT
        return extractedName;
    }
}
// 自定义 RequestWrapper,用于提供修改后的 Body
class ModifiedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final String modifiedBody;
    private final byte[] modifiedBodyBytes;
    public ModifiedBodyHttpServletRequestWrapper(HttpServletRequest request, String modifiedBody) {
        super(request);
        this.modifiedBody = modifiedBody;
        this.modifiedBodyBytes = modifiedBody.getBytes(StandardCharsets.UTF_8);
    }
    @Override
    public ServletInputStream getInputStream() {
        // 返回一个包含修改后数据的 ServletInputStream
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(modifiedBodyBytes);
        return new ServletInputStream() {
            @Override
            public int read() {
                return byteArrayInputStream.read();
            }
            // ... 其他需要实现的方法
            @Override
            public boolean isFinished() { return false; }
            @Override
            public boolean isReady() { return true; }
            @Override
            public void setReadListener(ReadListener listener) { }
        };
    }
    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}
        注意 :上述示例中的 CachedBodyHttpServletRequest是另一个用于首次缓存请求体的包装器,实践中常用 ContentCachingRequestWrapper(Spring)或自行实现。完整实现需考虑性能、异常处理以及非 JSON Content-Type 的情况。
四、总结与最佳实践
| 特性 | Servlet Filter | Spring Interceptor | 
|---|---|---|
| 工作层面 | Servlet 容器层面,更底层 | Spring MVC 层面,更上层 | 
| 请求体访问 | 可以在流被消耗前读取和修改 | 不可以,流通常已被消耗或无法安全修改 | 
| 主要职责 | 日志、压缩、加解密、修改请求/响应内容、全局权限校验 | 业务逻辑权限校验、日志、模型加工、预处理 Handler | 
| 执行顺序 | 最先执行 | 在 DispatcherServlet 之后执行 | 
复盘后的最佳实践:
- 明确边界 :需要修改原始 HTTP 报文 (Header、Body)时,优先考虑 Filter 。需要处理MVC 上下文 信息(
@RequestBody对象、模型、会话等)时,使用 Interceptor。 - 性能考量:读取和缓存请求体会带来额外的内存和 CPU 开销。应在 Filter 中条件性地执行此操作(例如,只对特定 URL 模式的请求进行处理)。
 - 健壮性:务必做好异常处理。如果修改过程中发生错误(如 JSON 解析失败),应决定是直接抛出错误响应,还是原封不动地传递原始请求。
 - 使用成熟组件 :对于简单的操作,可以考虑使用 Spring 提供的 
ContentCachingRequestWrapper作为基础。对于复杂的 API 网关类操作,则可研究更专业的组件,如 Netflix Zuul、Spring Cloud Gateway 的 Filter 机制。 
通过这次"踩坑"和复盘,我们更加深刻地理解了 Servlet 规范中 Filter 和 Interceptor 的职责划分,这对于设计出正确、高效的 Web 应用程序至关重要。