SpringBoot过滤器进阶:自定义HttpServletRequestWrapper实现请求体多次读取

SpringBoot过滤器进阶:自定义HttpServletRequestWrapper实现请求体多次读取 SpringBoot过滤器进阶自定义HttpServletRequestWrapper实现请求体多次读取在Web开发中我们经常遇到需要多次读取HTTP请求体的情况比如API签名验证、请求日志记录、数据校验等场景。然而Servlet规范限制我们只能读取请求体一次这给开发带来了不小的挑战。本文将深入探讨如何通过自定义HttpServletRequestWrapper来解决这一难题。1. 请求体读取限制的本质Servlet规范之所以限制请求体只能被读取一次主要出于以下考虑性能优化避免重复读取带来的I/O开销资源管理防止内存泄漏和资源未释放数据一致性确保请求体数据在整个处理流程中保持一致当我们尝试多次调用getReader()或getInputStream()方法时就会遇到java.lang.IllegalStateException: getReader() has already been called for this request异常。这在以下场景尤为常见过滤器(Filter)中读取请求体进行签名验证拦截器(Interceptor)中记录请求日志控制器(Controller)中使用RequestBody注解关键问题一旦请求体被读取原始的输入流就被消耗掉了后续的任何读取尝试都会失败。2. 解决方案设计要解决这个问题我们需要设计一个能够缓存请求体内容的包装器。核心思路是在第一次读取请求体时将内容缓存到内存中后续读取时从缓存中提供数据而非原始流保持原有API接口不变确保兼容性2.1 自定义HttpServletRequestWrapper实现public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper { private final byte[] cachedBody; public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); this.cachedBody readRequestBody(request); } private byte[] readRequestBody(HttpServletRequest request) throws IOException { try (InputStream inputStream request.getInputStream(); ByteArrayOutputStream outputStream new ByteArrayOutputStream()) { byte[] buffer new byte[1024]; int bytesRead; while ((bytesRead inputStream.read(buffer)) ! -1) { outputStream.write(buffer, 0, bytesRead); } return outputStream.toByteArray(); } } Override public ServletInputStream getInputStream() { return new CachedBodyServletInputStream(this.cachedBody); } Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(this.getInputStream())); } private static class CachedBodyServletInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; public CachedBodyServletInputStream(byte[] cachedBody) { this.inputStream new ByteArrayInputStream(cachedBody); } Override public boolean isFinished() { return inputStream.available() 0; } Override public boolean isReady() { return true; } Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException(); } Override public int read() throws IOException { return inputStream.read(); } } }实现要点继承HttpServletRequestWrapper这是Servlet API提供的装饰器类在构造函数中读取并缓存整个请求体重写getInputStream()和getReader()方法返回基于缓存的新流实现自定义的ServletInputStream来处理缓存数据2.2 处理multipart/form-data的特殊情况对于文件上传请求我们通常不需要缓存整个请求体因为文件可能很大缓存会消耗大量内存Spring已经提供了完善的Multipart处理机制public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); if (ServletFileUpload.isMultipartContent(request)) { // 不处理multipart请求 this.cachedBody null; } else { this.cachedBody readRequestBody(request); } }3. 集成到Spring Boot应用3.1 创建过滤器public class RepeatableReadFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest (HttpServletRequest) request; if (!ServletFileUpload.isMultipartContent(httpRequest)) { request new RepeatableReadRequestWrapper(httpRequest); } } chain.doFilter(request, response); } }3.2 注册过滤器Configuration public class FilterConfig { Bean public FilterRegistrationBeanRepeatableReadFilter repeatableReadFilter() { FilterRegistrationBeanRepeatableReadFilter registration new FilterRegistrationBean(); registration.setFilter(new RepeatableReadFilter()); registration.addUrlPatterns(/*); registration.setOrder(Ordered.HIGHEST_PRECEDENCE 1); return registration; } }配置要点设置较高的优先级确保在其他过滤器之前执行对所有非multipart请求生效保持过滤器轻量避免性能瓶颈4. 实际应用场景4.1 API签名验证在微服务架构中API签名验证是常见的安全措施。通过请求体包装器我们可以在过滤器中读取请求体进行签名验证同时不影响后续的业务处理。public class ApiSignFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestBody request.getReader().lines().collect(Collectors.joining()); if (!verifySignature(requestBody, request.getHeader(X-Signature))) { response.sendError(HttpStatus.UNAUTHORIZED.value(), Invalid signature); return; } filterChain.doFilter(request, response); } }4.2 请求日志记录完整的请求日志对于调试和监控至关重要。通过包装器我们可以记录请求体内容而不影响业务逻辑。public class RequestLoggingFilter extends OncePerRequestFilter { private static final Logger logger LoggerFactory.getLogger(RequestLoggingFilter.class); Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestBody request.getReader().lines().collect(Collectors.joining()); logger.info(Request {} {} - Body: {}, request.getMethod(), request.getRequestURI(), requestBody); filterChain.doFilter(request, response); } }4.3 数据校验和转换在某些场景下我们可能需要在进入业务逻辑前对请求数据进行预处理或校验。public class DataValidationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestBody request.getReader().lines().collect(Collectors.joining()); try { JSONObject json JSON.parseObject(requestBody); validateData(json); } catch (Exception e) { response.sendError(HttpStatus.BAD_REQUEST.value(), Invalid request data); return; } filterChain.doFilter(request, response); } }5. 性能优化与注意事项5.1 内存管理缓存整个请求体会增加内存消耗特别是对于大请求体。建议对大请求体(如超过1MB)进行特殊处理考虑使用临时文件而非内存缓存实现流式处理避免全量缓存5.2 异常处理在请求体读取过程中可能发生各种异常需要妥善处理public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); try { this.cachedBody readRequestBody(request); } catch (IOException e) { throw new ServletException(Failed to read request body, e); } }5.3 与Spring MVC的兼容性确保包装器与Spring MVC的各种特性兼容RequestBody注解HttpMessageConverter参数解析器5.4 测试策略针对包装器实现全面的测试SpringBootTest class RepeatableReadRequestWrapperTest { Autowired private WebApplicationContext context; private MockMvc mockMvc; BeforeEach void setup() { mockMvc MockMvcBuilders.webAppContextSetup(context) .addFilter(new RepeatableReadFilter()) .build(); } Test void testMultipleReads() throws Exception { String requestBody {\key\:\value\}; mockMvc.perform(post(/api/test) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isOk()); } }6. 高级应用场景6.1 请求体修改通过包装器我们不仅可以多次读取请求体还可以修改请求体内容public class ModifyRequestBodyWrapper extends RepeatableReadRequestWrapper { private byte[] modifiedBody; public ModifyRequestBodyWrapper(HttpServletRequest request, FunctionString, String modifier) throws IOException { super(request); String original new String(super.cachedBody, StandardCharsets.UTF_8); String modified modifier.apply(original); this.modifiedBody modified.getBytes(StandardCharsets.UTF_8); } Override public ServletInputStream getInputStream() { return new CachedBodyServletInputStream(this.modifiedBody); } }6.2 异步请求处理对于异步请求需要特别注意线程安全和资源释放Override public void setReadListener(ReadListener listener) { if (this.modifiedBody ! null) { super.setReadListener(listener); } else { throw new IllegalStateException(Cannot set read listener on modified stream); } }6.3 与WebFlux的对比在响应式编程模型中请求体本身就是可重复读取的PostMapping(/flux) public MonoString handleFlux(RequestBody MonoString body) { return body.map(content - Processed: content); }对于传统Servlet应用我们的包装器方案提供了类似的可重复读取能力。7. 最佳实践总结在实际项目中应用请求体包装器时建议遵循以下实践按需使用只在确实需要多次读取请求体的场景使用资源控制监控内存使用防止大请求体导致OOM异常处理确保所有可能的异常都被捕获和处理性能测试在高并发场景下测试包装器的影响清晰文档在团队中明确包装器的用途和限制以下是一个典型的使用流程对比场景传统方式使用包装器签名验证无法在过滤器中读取JSON请求体可以在过滤器中完整读取请求体日志记录只能记录URL参数可以记录完整的请求体内容数据校验需要在Controller中处理可以在过滤器中提前校验性能影响无额外开销增加内存使用和少量CPU开销在最近的一个电商平台项目中我们使用这种技术实现了以下功能全链路请求日志帮助快速定位问题统一的签名验证机制提升API安全性请求数据预处理简化业务逻辑实现过程中最大的挑战是处理大文件上传的情况最终我们通过区分普通请求和multipart请求解决了这个问题。