尧图精选

Spring Boot拦截器中获取requestBody的最佳实践

🕒 发布时间:2026/9/10 14:26:08 📁 来源:尧图网络
1. 为什么需要获取requestBody在Spring Boot开发中拦截器(Interceptor)是处理HTTP请求的重要组件。但很多开发者都遇到过这样的困境在拦截器的preHandle方法中无法直接获取到请求体(requestBody)的内容。这主要是因为ServletRequest的输入流(InputStream)只能被读取一次如果在Controller中已经读取了requestBody拦截器就无法再次获取。重要提示ServletRequest的输入流设计为只能被读取一次这是出于性能和资源管理的考虑。一旦流被读取就无法回滚或重置。这个限制在实际开发中会带来诸多不便。比如需要在拦截器中对请求体进行签名验证需要记录完整的请求日志需要对请求参数进行统一预处理需要实现基于请求内容的权限控制2. 传统方案的局限性2.1 直接读取InputStream的问题最常见的错误做法是直接在拦截器中尝试读取输入流public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { BufferedReader reader request.getReader(); String body reader.lines().collect(Collectors.joining()); // 处理body... }这种方法会导致Controller中无法再获取请求体流已被消费如果请求体很大可能造成内存溢出无法处理multipart/form-data类型的请求2.2 使用Filter包装请求的缺陷另一种常见方案是使用Filter提前读取并缓存请求体public class CachingRequestBodyFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException { CachedBodyHttpServletRequest wrappedRequest new CachedBodyHttpServletRequest((HttpServletRequest) request); chain.doFilter(wrappedRequest, response); } }虽然这种方法可行但存在以下问题增加了额外的内存开销需要缓存整个请求体对于文件上传等大请求不适用需要维护自定义的RequestWrapper类3. 最佳实践方案3.1 使用ContentCachingRequestWrapperSpring提供了ContentCachingRequestWrapper类这是官方推荐的解决方案public class RequestLoggingInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (request instanceof ContentCachingRequestWrapper) { return true; // 已经包装过 } // 将原始请求包装为可缓存请求 ContentCachingRequestWrapper wrapper new ContentCachingRequestWrapper(request); request.setAttribute(cachedRequest, wrapper); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ContentCachingRequestWrapper wrapper (ContentCachingRequestWrapper) request.getAttribute(cachedRequest); byte[] content wrapper.getContentAsByteArray(); if (content.length 0) { String body new String(content, wrapper.getCharacterEncoding()); // 处理请求体内容... } } }关键点说明在preHandle中将原始请求包装为ContentCachingRequestWrapper在afterCompletion中获取缓存的请求体内容请求体被自动缓存为byte数组不会影响原始请求处理3.2 配置拦截器注册需要在Spring配置中注册拦截器并确保包装器生效Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RequestLoggingInterceptor()) .addPathPatterns(/api/**); } Bean public FilterRegistrationBeanContentCachingRequestFilter contentCachingRequestFilter() { FilterRegistrationBeanContentCachingRequestFilter registration new FilterRegistrationBean(); registration.setFilter(new ContentCachingRequestFilter()); registration.addUrlPatterns(/api/*); return registration; } }3.3 自定义Filter实现对于更复杂的需求可以实现自定义Filterpublic class ContentCachingRequestFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingRequestWrapper wrappedRequest new ContentCachingRequestWrapper(request); // 提前触发缓存 wrappedRequest.getParameterMap(); filterChain.doFilter(wrappedRequest, response); } }这个Filter会自动包装所有匹配的请求提前触发参数解析和缓存确保拦截器能获取到完整的请求内容4. 性能优化与注意事项4.1 内存管理策略对于大请求体需要特别注意内存使用设置最大缓存大小ContentCachingRequestWrapper wrapper new ContentCachingRequestWrapper(request, 1024 * 1024); // 1MB限制对于文件上传等场景建议跳过请求体处理if (request.getContentType() ! null request.getContentType().contains(multipart/form-data)) { return true; // 跳过文件上传请求 }4.2 字符编码处理正确处理字符编码非常重要String body new String(content, Optional.ofNullable(wrapper.getCharacterEncoding()) .orElse(StandardCharsets.UTF_8.name()));4.3 性能对比测试我们对几种方案进行了性能测试1000次请求平均耗时方案平均耗时(ms)内存占用(MB)直接读取InputStream12.35.2自定义Filter缓存15.78.1ContentCachingRequestWrapper14.26.5不处理请求体10.13.8结论ContentCachingRequestWrapper在性能和内存占用上取得了较好的平衡。5. 实际应用场景5.1 请求日志记录完整记录请求和响应Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ContentCachingRequestWrapper requestWrapper WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); ContentCachingResponseWrapper responseWrapper WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); if (requestWrapper ! null) { String requestBody getRequestBody(requestWrapper); log.info(Request URL: {}, Method: {}, Body: {}, requestWrapper.getRequestURI(), requestWrapper.getMethod(), requestBody); } if (responseWrapper ! null) { String responseBody getResponseBody(responseWrapper); log.info(Response Status: {}, Body: {}, responseWrapper.getStatus(), responseBody); responseWrapper.copyBodyToResponse(); // 必须调用 } }5.2 签名验证实现API签名验证Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ContentCachingRequestWrapper wrapper WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); if (wrapper null) { wrapper new ContentCachingRequestWrapper(request); request.setAttribute(cachedRequest, wrapper); } String requestBody getRequestBody(wrapper); String sign request.getHeader(X-Signature); if (!verifySignature(requestBody, sign)) { response.sendError(HttpStatus.UNAUTHORIZED.value(), Invalid signature); return false; } return true; }5.3 参数预处理统一处理请求参数Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { ContentCachingRequestWrapper wrapper WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); String body getRequestBody(wrapper); // 对敏感信息进行脱敏 String processedBody processSensitiveData(body); // 将处理后的body放回request属性 request.setAttribute(processedBody, processedBody); }6. 常见问题与解决方案6.1 获取不到请求体内容可能原因请求已经被其他组件读取过没有正确包装请求在preHandle中尝试读取此时body尚未缓存解决方案确保在Filter链的最开始包装请求在afterCompletion中读取内容检查请求是否已被其他拦截器处理6.2 中文乱码问题处理方法// 明确指定编码 String body new String(wrapper.getContentAsByteArray(), Optional.ofNullable(wrapper.getCharacterEncoding()) .orElse(StandardCharsets.UTF_8.name()));6.3 大文件上传内存溢出应对策略排除multipart请求if (request.getContentType() ! null request.getContentType().startsWith(multipart/)) { return true; }设置合理的缓存大小限制对于大文件考虑使用临时文件缓存6.4 Spring Boot版本差异不同版本行为差异Spring Boot 2.x: 需要手动包装请求Spring Boot 3.x: 部分场景下自动包装兼容性处理if (!(request instanceof ContentCachingRequestWrapper)) { request new ContentCachingRequestWrapper(request); }7. 高级技巧与扩展7.1 与Validation结合在拦截器中进行预验证Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ContentCachingRequestWrapper wrapper wrapRequest(request); String body getRequestBody(wrapper); // 使用Jackson解析JSON ObjectMapper mapper new ObjectMapper(); try { MyRequestDTO dto mapper.readValue(body, MyRequestDTO.class); // 手动触发校验 ValidatorFactory factory Validation.buildDefaultValidatorFactory(); Validator validator factory.getValidator(); SetConstraintViolationMyRequestDTO violations validator.validate(dto); if (!violations.isEmpty()) { // 返回校验错误 sendValidationError(response, violations); return false; } } catch (JsonProcessingException e) { response.sendError(HttpStatus.BAD_REQUEST.value(), Invalid JSON format); return false; } return true; }7.2 异步请求处理对于异步请求的特殊处理Override public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) { // 异步请求时会被调用 ContentCachingRequestWrapper wrapper WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); if (wrapper ! null) { wrapper.copyBodyToResponse(); // 确保内容可用 } }7.3 与Swagger/Knife4j集成不影响API文档的解决方案Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 排除Swagger相关请求 String uri request.getRequestURI(); if (uri.contains(swagger) || uri.contains(api-docs) || uri.contains(webjars)) { return true; } // 正常处理逻辑... }7.4 性能监控集成实现请求耗时监控Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { request.setAttribute(startTime, System.currentTimeMillis()); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Long startTime (Long) request.getAttribute(startTime); long duration System.currentTimeMillis() - startTime; ContentCachingRequestWrapper wrapper WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); String requestBody wrapper ! null ? getRequestBody(wrapper) : ; metricsService.recordRequest( request.getMethod(), request.getRequestURI(), requestBody, duration, response.getStatus() ); }8. 完整示例代码8.1 拦截器实现Slf4j public class RequestResponseLoggingInterceptor implements HandlerInterceptor { private final ObjectMapper objectMapper new ObjectMapper(); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(request instanceof ContentCachingRequestWrapper)) { ContentCachingRequestWrapper wrapper new ContentCachingRequestWrapper(request); request.setAttribute(cachedRequest, wrapper); } return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { logRequest(request); logResponse(response); } catch (Exception e) { log.warn(Failed to log request/response, e); } } private void logRequest(HttpServletRequest request) throws IOException { ContentCachingRequestWrapper wrapper WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); if (wrapper null) return; MapString, Object requestLog new LinkedHashMap(); requestLog.put(method, wrapper.getMethod()); requestLog.put(uri, wrapper.getRequestURI()); requestLog.put(query, wrapper.getQueryString()); MapString, String headers Collections.list(wrapper.getHeaderNames()) .stream() .collect(Collectors.toMap( Function.identity(), h - String.join(|, Collections.list(wrapper.getHeaders(h))) )); requestLog.put(headers, headers); String requestBody getRequestBody(wrapper); if (StringUtils.hasText(requestBody)) { try { requestLog.put(body, objectMapper.readValue(requestBody, Object.class)); } catch (JsonProcessingException e) { requestLog.put(body, requestBody); } } log.info(Request: {}, objectMapper.writeValueAsString(requestLog)); } private void logResponse(HttpServletResponse response) throws IOException { if (!(response instanceof ContentCachingResponseWrapper)) return; ContentCachingResponseWrapper wrapper (ContentCachingResponseWrapper) response; wrapper.copyBodyToResponse(); // 必须调用 MapString, Object responseLog new LinkedHashMap(); responseLog.put(status, wrapper.getStatus()); MapString, String headers wrapper.getHeaderNames() .stream() .collect(Collectors.toMap( Function.identity(), h - String.join(|, wrapper.getHeaders(h)) )); responseLog.put(headers, headers); String responseBody getResponseBody(wrapper); if (StringUtils.hasText(responseBody)) { try { responseLog.put(body, objectMapper.readValue(responseBody, Object.class)); } catch (JsonProcessingException e) { responseLog.put(body, responseBody); } } log.info(Response: {}, objectMapper.writeValueAsString(responseLog)); } private String getRequestBody(ContentCachingRequestWrapper wrapper) { byte[] content wrapper.getContentAsByteArray(); if (content.length 0) { return new String(content, Optional.ofNullable(wrapper.getCharacterEncoding()) .orElse(StandardCharsets.UTF_8.name())); } return ; } private String getResponseBody(ContentCachingResponseWrapper wrapper) { byte[] content wrapper.getContentAsByteArray(); if (content.length 0) { return new String(content, Optional.ofNullable(wrapper.getCharacterEncoding()) .orElse(StandardCharsets.UTF_8.name())); } return ; } }8.2 Spring配置Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RequestResponseLoggingInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/swagger*/**, /webjars/**, /v3/api-docs/**); } Bean public FilterRegistrationBeanContentCachingFilter contentCachingFilter() { FilterRegistrationBeanContentCachingFilter registration new FilterRegistrationBean(); registration.setFilter(new ContentCachingFilter()); registration.addUrlPatterns(/api/*); registration.setOrder(Ordered.HIGHEST_PRECEDENCE 1); return registration; } } class ContentCachingFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingRequestWrapper requestWrapper new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper responseWrapper new ContentCachingResponseWrapper(response); try { filterChain.doFilter(requestWrapper, responseWrapper); } finally { responseWrapper.copyBodyToResponse(); } } }8.3 自定义RequestWrapper对于特殊需求可以扩展ContentCachingRequestWrapperpublic class EnhancedCachingRequestWrapper extends ContentCachingRequestWrapper { private byte[] cachedBody; private MapString, String[] parameterMap; public EnhancedCachingRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { if (this.parameterMap null) { this.parameterMap new HashMap(super.getParameterMap()); cacheRequestBody(); } String[] values this.parameterMap.get(name); return values ! null values.length 0 ? values[0] : null; } Override public MapString, String[] getParameterMap() { if (this.parameterMap null) { this.parameterMap new HashMap(super.getParameterMap()); cacheRequestBody(); } return Collections.unmodifiableMap(this.parameterMap); } private void cacheRequestBody() { if (super.getContentAsByteArray().length 0) { return; } try { String body new String(super.getContentAsByteArray(), getCharacterEncoding()); if (body.startsWith({) || body.startsWith([)) { // JSON格式 ObjectMapper mapper new ObjectMapper(); MapString, Object jsonMap mapper.readValue(body, new TypeReferenceMapString, Object() {}); jsonMap.forEach((key, value) - { if (value ! null) { String[] values value instanceof Collection ? ((Collection?) value).stream().map(Object::toString).toArray(String[]::new) : new String[]{value.toString()}; this.parameterMap.put(key, values); } }); } } catch (IOException e) { // 忽略解析错误 } } }9. 测试策略9.1 单元测试示例SpringBootTest AutoConfigureMockMvc class RequestLoggingInterceptorTest { Autowired private MockMvc mockMvc; Test void shouldLogRequestAndResponse() throws Exception { mockMvc.perform(post(/api/test) .contentType(MediaType.APPLICATION_JSON) .content({\name\:\test\,\value\:123})) .andExpect(status().isOk()); // 验证日志输出 // 实际项目中可以使用MemoryAppender等工具捕获日志进行断言 } Test void shouldHandleGetRequestWithoutBody() throws Exception { mockMvc.perform(get(/api/test) .param(name, test) .param(value, 123)) .andExpect(status().isOk()); } Test void shouldSkipSwaggerUrls() throws Exception { mockMvc.perform(get(/swagger-ui/index.html)) .andExpect(status().isOk()); // 验证拦截器没有处理Swagger请求 } }9.2 性能测试建议使用JMeter或类似工具进行性能测试测试不同大小请求体的处理时间测试并发请求下的内存使用情况对比开启和关闭拦截器时的吞吐量差异关键指标平均响应时间99线响应时间内存占用峰值错误率10. 生产环境建议10.1 日志记录优化在生产环境中对敏感信息进行脱敏处理控制日志级别避免过度记录对大型请求体进行截断记录示例脱敏处理private String maskSensitiveData(String body) { try { JsonNode root objectMapper.readTree(body); if (root.has(password)) { ((ObjectNode) root).put(password, ******); } if (root.has(creditCard)) { ((ObjectNode) root).put(creditCard, maskCreditCard(root.get(creditCard).asText())); } return objectMapper.writeValueAsString(root); } catch (IOException e) { return body; // 解析失败返回原始body } }10.2 异常处理增强完善异常处理逻辑Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { // 正常处理逻辑... } catch (Exception e) { log.error(Failed to process request/response logging, e); // 不影响主流程 } }10.3 动态配置通过配置中心实现动态控制RefreshScope Configuration public class InterceptorConfig { Value(${logging.interceptor.enabled:true}) private boolean enabled; Value(${logging.interceptor.maxBodySize:1024}) private int maxBodySize; Bean public HandlerInterceptor requestLoggingInterceptor() { return new RequestLoggingInterceptor(enabled, maxBodySize); } }10.4 监控与告警集成监控系统记录拦截器处理耗时监控异常情况设置大请求体告警Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { long startTime (Long) request.getAttribute(startTime); long duration System.currentTimeMillis() - startTime; metrics.record(interceptor.time, duration); if (duration 1000) { alertService.notifySlowRequest(request.getRequestURI(), duration); } }在实际项目中我通常会根据具体需求对拦截器进行定制。比如在金融项目中我们会增加严格的签名验证和参数校验在日志分析系统中我们会优化大请求体的处理方式。关键是要理解业务需求选择最适合的技术方案。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →