Spring Boot 3.x 请求体读取后无法重复读取问题详解
·
Spring Boot 3.x 请求体读取后无法重复读取问题详解
一、问题背景
在Spring Boot 3.x中,HTTP请求体(RequestBody)是一个只能读取一次的输入流。这个问题通常出现在以下场景:
- 拦截器(Interceptor)和控制器(Controller)都需要读取请求体
- 过滤器(Filter)中读取请求体后,后续处理无法再次读取
- 全局异常处理器需要访问请求体内容
- 日志记录器需要记录请求体
二、根本原因分析
1. HTTP协议限制
- HTTP请求体是基于流的(InputStream)
- 流只能被读取一次,读取后指针到达末尾
- Servlet规范中的
ServletRequest.getInputStream()只能调用一次
2. Spring Boot 3.x的变化
- 基于Jakarta EE 9+ API
- 更强的流管理机制
- 默认更严格的安全限制
3. 请求体生命周期
客户端请求 → Servlet容器 → 过滤器链 → 拦截器链 → 控制器 → 响应
↓
InputStream (只能读取一次)
三、解决方案详解
方案1:自定义HttpServletRequestWrapper(推荐)
1.1 基础版本:缓存请求体到字节数组
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* 可重复读取请求体的HttpServletRequest包装器
*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedBody;
private final String requestBody;
/**
* 构造函数
*/
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// 读取请求体到字节数组
InputStream requestInputStream = request.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = requestInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
this.cachedBody = byteArrayOutputStream.toByteArray();
// 同时保存为字符串(可选)
Charset charset = getCharset(request);
this.requestBody = new String(cachedBody, charset);
}
/**
* 获取字符编码
*/
private Charset getCharset(HttpServletRequest request) {
String charsetName = request.getCharacterEncoding();
if (charsetName == null) {
charsetName = StandardCharsets.UTF_8.name();
}
return Charset.forName(charsetName);
}
/**
* 重写getInputStream方法,返回基于缓存的流
*/
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 不需要实现
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
/**
* 重写getReader方法
*/
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
/**
* 获取缓存的请求体字节数组
*/
public byte[] getCachedBody() {
return cachedBody;
}
/**
* 获取缓存的请求体字符串
*/
public String getRequestBody() {
return requestBody;
}
/**
* 获取请求体大小
*/
public int getContentLength() {
return cachedBody.length;
}
/**
* 获取请求体长度(兼容方法)
*/
@Override
public long getContentLengthLong() {
return cachedBody.length;
}
}
1.2 高级版本:支持大文件和流式处理
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
/**
* 支持大文件的可重复读取请求体包装器
* 使用临时文件存储大请求体,避免内存溢出
*/
public class CachedBodyLargeRequestWrapper extends HttpServletRequestWrapper {
private static final int MEMORY_THRESHOLD = 10 * 1024 * 1024; // 10MB
private static final String TEMP_FILE_PREFIX = "request_body_";
private static final String TEMP_FILE_SUFFIX = ".tmp";
private final byte[] memoryCachedBody; // 内存缓存
private final Path tempFilePath; // 临时文件路径
private final boolean isLargeContent; // 是否大内容
private final long contentLength; // 内容长度
/**
* 构造函数
*/
public CachedBodyLargeRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.contentLength = request.getContentLengthLong();
// 根据内容大小决定缓存策略
if (contentLength > 0 && contentLength <= MEMORY_THRESHOLD) {
// 小内容:内存缓存
this.memoryCachedBody = readToMemory(request);
this.tempFilePath = null;
this.isLargeContent = false;
} else {
// 大内容:文件缓存
this.memoryCachedBody = null;
this.tempFilePath = writeToTempFile(request);
this.isLargeContent = true;
}
}
/**
* 读取到内存
*/
private byte[] readToMemory(HttpServletRequest request) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = request.getInputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
return baos.toByteArray();
}
}
/**
* 写入临时文件
*/
private Path writeToTempFile(HttpServletRequest request) throws IOException {
Path tempFile = Files.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
try (OutputStream os = Files.newOutputStream(tempFile, StandardOpenOption.WRITE);
InputStream is = request.getInputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
// 注册JVM退出时删除临时文件
tempFile.toFile().deleteOnExit();
return tempFile;
}
/**
* 重写getInputStream方法
*/
@Override
public ServletInputStream getInputStream() {
if (!isLargeContent) {
// 内存缓存
ByteArrayInputStream bais = new ByteArrayInputStream(memoryCachedBody);
return createServletInputStream(bais);
} else {
// 文件缓存
try {
InputStream fis = Files.newInputStream(tempFilePath);
return createServletInputStream(fis);
} catch (IOException e) {
throw new RuntimeException("无法读取临时文件", e);
}
}
}
/**
* 创建ServletInputStream
*/
private ServletInputStream createServletInputStream(InputStream inputStream) {
return new ServletInputStream() {
@Override
public boolean isFinished() {
try {
return inputStream.available() == 0;
} catch (IOException e) {
return true;
}
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 异步读取不支持
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public void close() throws IOException {
inputStream.close();
}
};
}
/**
* 重写getReader方法
*/
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
/**
* 获取缓存的请求体字节数组(仅适用于小内容)
*/
public byte[] getCachedBody() throws IOException {
if (!isLargeContent) {
return memoryCachedBody.clone();
} else {
return Files.readAllBytes(tempFilePath);
}
}
/**
* 获取缓存的请求体字符串
*/
public String getRequestBody() throws IOException {
if (!isLargeContent) {
return new String(memoryCachedBody, getCharacterEncoding());
} else {
return new String(Files.readAllBytes(tempFilePath), getCharacterEncoding());
}
}
/**
* 获取请求体大小
*/
@Override
public long getContentLengthLong() {
return contentLength;
}
/**
* 清理资源
*/
public void clean() throws IOException {
if (isLargeContent && tempFilePath != null) {
Files.deleteIfExists(tempFilePath);
}
}
/**
* 获取缓存方式
*/
public String getCacheStrategy() {
return isLargeContent ? "FILE" : "MEMORY";
}
/**
* 获取临时文件路径(如果使用文件缓存)
*/
public Path getTempFilePath() {
return tempFilePath;
}
}
方案2:过滤器链中包装请求
2.1 通用请求体缓存过滤器
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 请求体缓存过滤器
* 在过滤器链中包装请求,使其可以重复读取
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CachingRequestBodyFilter implements Filter {
private static final ThreadLocal<Long> requestSizeHolder = new ThreadLocal<>();
private static final int MAX_REQUEST_SIZE = 10 * 1024 * 1024; // 10MB
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 检查是否是需要缓存的请求类型
if (shouldCacheRequest(request)) {
// 包装请求
CachedBodyHttpServletRequest wrappedRequest =
new CachedBodyHttpServletRequest(request);
// 记录请求大小
requestSizeHolder.set((long) wrappedRequest.getCachedBody().length);
try {
// 继续过滤器链
filterChain.doFilter(wrappedRequest, servletResponse);
} finally {
// 清理ThreadLocal
requestSizeHolder.remove();
}
} else {
// 不需要缓存,直接传递
filterChain.doFilter(servletRequest, servletResponse);
}
}
/**
* 判断是否需要缓存请求体
*/
private boolean shouldCacheRequest(HttpServletRequest request) {
String contentType = request.getContentType();
String method = request.getMethod();
// 只缓存POST、PUT、PATCH方法的JSON、XML等请求
if (!"POST".equalsIgnoreCase(method) &&
!"PUT".equalsIgnoreCase(method) &&
!"PATCH".equalsIgnoreCase(method)) {
return false;
}
if (contentType == null) {
return false;
}
// 检查内容类型
boolean isJson = contentType.contains("application/json");
boolean isXml = contentType.contains("application/xml") ||
contentType.contains("text/xml");
boolean isFormUrlEncoded = contentType.contains("application/x-www-form-urlencoded");
boolean isMultipart = contentType.contains("multipart/");
// 不缓存multipart请求(文件上传)
if (isMultipart) {
return false;
}
// 检查请求大小
long contentLength = request.getContentLengthLong();
if (contentLength > MAX_REQUEST_SIZE) {
// 大请求不缓存
return false;
}
return isJson || isXml || isFormUrlEncoded;
}
/**
* 获取当前请求的大小
*/
public static Long getCurrentRequestSize() {
return requestSizeHolder.get();
}
@Override
public void init(FilterConfig filterConfig) {
// 初始化逻辑
}
@Override
public void destroy() {
// 清理逻辑
}
}
2.2 针对特定路径的过滤器
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 针对特定API路径的请求体缓存过滤器
*/
@Component
@Order(1)
public class ApiCachingRequestBodyFilter extends OncePerRequestFilter {
// 需要缓存的API路径
private static final List<String> CACHEABLE_PATHS = Arrays.asList(
"/api/v1/",
"/api/v2/",
"/webhook/"
);
// 排除的API路径
private static final List<String> EXCLUDED_PATHS = Arrays.asList(
"/api/v1/upload",
"/api/v1/export"
);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
// 检查是否匹配需要缓存的路径
boolean shouldCache = CACHEABLE_PATHS.stream()
.anyMatch(requestURI::startsWith);
// 检查是否在排除列表中
boolean isExcluded = EXCLUDED_PATHS.stream()
.anyMatch(requestURI::startsWith);
// 检查请求方法
String method = request.getMethod();
boolean isCacheableMethod = "POST".equals(method) ||
"PUT".equals(method) ||
"PATCH".equals(method);
if (shouldCache && !isExcluded && isCacheableMethod) {
// 包装请求
CachedBodyHttpServletRequest wrappedRequest =
new CachedBodyHttpServletRequest(request);
// 添加自定义头信息
response.addHeader("X-Request-Cached", "true");
try {
filterChain.doFilter(wrappedRequest, response);
} catch (Exception e) {
handleFilterException(e, request, response);
}
} else {
filterChain.doFilter(request, response);
}
}
/**
* 处理过滤器异常
*/
private void handleFilterException(Exception e,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
if (e instanceof IOException) {
// 请求体读取异常
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json");
response.getWriter().write(String.format(
"{\"error\":\"请求体读取失败\",\"message\":\"%s\"}",
e.getMessage()
));
} else {
throw new ServletException(e);
}
}
/**
* 判断是否应该跳过过滤器
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
// 静态资源跳过
if (path.startsWith("/static/") ||
path.startsWith("/css/") ||
path.startsWith("/js/") ||
path.startsWith("/images/")) {
return true;
}
return false;
}
}
方案3:拦截器中的请求体处理
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 请求体日志拦截器
* 使用Spring的ContentCachingRequestWrapper
*/
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
private static final int MAX_PAYLOAD_LENGTH = 10000; // 最大记录长度
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 包装请求以便缓存请求体
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
// 将包装后的请求设置回属性中,供后续使用
request.setAttribute("cachedRequest", request);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
// 获取缓存的请求
if (request instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
// 获取缓存的请求体内容
byte[] content = wrapper.getContentAsByteArray();
if (content.length > 0) {
String body = new String(content, request.getCharacterEncoding());
// 记录请求体日志(限制长度)
String logBody = body.length() > MAX_PAYLOAD_LENGTH ?
body.substring(0, MAX_PAYLOAD_LENGTH) + "..." : body;
System.out.println("Request Body: " + logBody);
// 可以存储到请求属性中供后续使用
request.setAttribute("requestBody", body);
}
}
}
}
/**
* 自定义ContentCachingRequestWrapper
* 扩展Spring的包装器,增加更多功能
*/
class ExtendedContentCachingRequestWrapper extends ContentCachingRequestWrapper {
private byte[] cachedBody;
private boolean bodyRead = false;
public ExtendedContentCachingRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 提前读取并缓存请求体
cacheRequestBody();
}
/**
* 缓存请求体
*/
private void cacheRequestBody() throws IOException {
if (!bodyRead) {
// 读取请求体到字节数组
InputStream inputStream = super.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
this.cachedBody = baos.toByteArray();
this.bodyRead = true;
}
}
/**
* 获取缓存的请求体
*/
public byte[] getCachedBody() {
return cachedBody;
}
/**
* 获取请求体字符串
*/
public String getRequestBodyAsString() {
if (cachedBody == null) {
return null;
}
return new String(cachedBody, getCharacterEncoding());
}
/**
* 重写getInputStream方法
*/
@Override
public ServletInputStream getInputStream() throws IOException {
cacheRequestBody();
ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return bais.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 不需要实现
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
}
方案4:全局异常处理器中的请求体访问
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
* 处理请求体无法重复读取的问题
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理所有异常
*/
@ExceptionHandler(Exception.class)
public ErrorResponse handleAllExceptions(Exception ex, HttpServletRequest request) {
// 尝试从请求属性中获取缓存的请求体
String requestBody = (String) request.getAttribute("requestBody");
// 如果请求体没有缓存,尝试从包装器中获取
if (requestBody == null && request instanceof CachedBodyHttpServletRequest) {
requestBody = ((CachedBodyHttpServletRequest) request).getRequestBody();
}
// 构建错误响应
ErrorResponse error = new ErrorResponse();
error.setTimestamp(System.currentTimeMillis());
error.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
error.setError("Internal Server Error");
error.setMessage(ex.getMessage());
error.setPath(request.getRequestURI());
// 在开发环境中包含请求详细信息
if (isDevelopmentEnvironment()) {
error.setRequestDetails(getRequestDetails(request, requestBody));
error.setStackTrace(getStackTrace(ex));
}
return error;
}
/**
* 处理请求体读取异常
*/
@ExceptionHandler(IOException.class)
public ErrorResponse handleIOException(IOException ex, WebRequest webRequest,
HttpServletRequest request) {
ErrorResponse error = new ErrorResponse();
error.setTimestamp(System.currentTimeMillis());
error.setStatus(HttpStatus.BAD_REQUEST.value());
error.setError("Request Body Error");
error.setMessage("无法读取请求体: " + ex.getMessage());
error.setPath(request.getRequestURI());
// 建议使用可重复读取的请求包装器
error.setSuggestion("请在过滤器中使用CachedBodyHttpServletRequest包装原始请求");
return error;
}
/**
* 获取请求详细信息
*/
private Map<String, Object> getRequestDetails(HttpServletRequest request,
String requestBody) {
Map<String, Object> details = new HashMap<>();
// 请求头
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, request.getHeader(name));
}
details.put("headers", headers);
// 请求参数
Map<String, String[]> parameters = request.getParameterMap();
details.put("parameters", parameters);
// 请求体
if (requestBody != null) {
details.put("body", requestBody);
}
// 请求方法
details.put("method", request.getMethod());
// 请求URL
details.put("url", request.getRequestURL().toString());
// 远程地址
details.put("remoteAddr", request.getRemoteAddr());
return details;
}
/**
* 获取异常堆栈跟踪
*/
private String[] getStackTrace(Exception ex) {
StackTraceElement[] stackTrace = ex.getStackTrace();
String[] stackTraceArray = new String[Math.min(stackTrace.length, 10)];
for (int i = 0; i < stackTraceArray.length; i++) {
stackTraceArray[i] = stackTrace[i].toString();
}
return stackTraceArray;
}
/**
* 判断是否为开发环境
*/
private boolean isDevelopmentEnvironment() {
String env = System.getProperty("spring.profiles.active");
return "dev".equals(env) || "development".equals(env);
}
/**
* 错误响应类
*/
static class ErrorResponse {
private long timestamp;
private int status;
private String error;
private String message;
private String path;
private String suggestion;
private Map<String, Object> requestDetails;
private String[] stackTrace;
// getters and setters
}
}
方案5:WebFlux响应式处理
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* WebFlux响应式请求体缓存过滤器
*/
@Component
@Order(1)
public class CachingRequestBodyWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 获取原始请求
ServerHttpRequest request = exchange.getRequest();
// 缓存请求体
return DataBufferUtils.join(request.getBody())
.flatMap(dataBuffer -> {
// 读取请求体内容
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
// 将请求体存储到交换属性中
String body = new String(bytes, StandardCharsets.UTF_8);
exchange.getAttributes().put("cachedRequestBody", body);
// 创建新的可重复读取的请求
ServerHttpRequestDecorator decoratedRequest =
new ServerHttpRequestDecorator(request) {
private final AtomicReference<byte[]> cachedBodyRef =
new AtomicReference<>(bytes);
@Override
public Flux<DataBuffer> getBody() {
return Flux.create(sink -> {
byte[] cachedBody = cachedBodyRef.get();
if (cachedBody != null) {
DataBuffer buffer = exchange.getResponse()
.bufferFactory()
.wrap(cachedBody);
sink.next(buffer);
sink.complete();
} else {
sink.error(new IllegalStateException("请求体已被释放"));
}
});
}
};
// 继续过滤器链
return chain.filter(exchange.mutate().request(decoratedRequest).build());
})
.switchIfEmpty(chain.filter(exchange)); // 如果没有请求体,直接传递
}
}
/**
* WebFlux控制器中获取缓存的请求体
*/
@RestController
@RequestMapping("/api/reactive")
public class ReactiveController {
/**
* 获取请求体
*/
@PostMapping("/process")
public Mono<String> processRequest(ServerWebExchange exchange,
@RequestBody Mono<String> requestBody) {
// 方式1:从交换属性中获取缓存的请求体
String cachedBody = exchange.getAttribute("cachedRequestBody");
// 方式2:使用请求体参数
return requestBody.flatMap(body -> {
// 处理请求体
return Mono.just("Processed: " + body);
});
}
/**
* 读取多次请求体
*/
@PostMapping("/multiple-read")
public Mono<Map<String, Object>> multipleRead(ServerWebExchange exchange) {
return exchange.getRequest().getBody()
.collectList()
.flatMap(dataBuffers -> {
// 第一次读取
StringBuilder firstRead = new StringBuilder();
for (DataBuffer buffer : dataBuffers) {
firstRead.append(buffer.toString(StandardCharsets.UTF_8));
}
// 重置缓冲区位置,以便再次读取
for (DataBuffer buffer : dataBuffers) {
buffer.readPosition(0);
}
// 第二次读取
StringBuilder secondRead = new StringBuilder();
for (DataBuffer buffer : dataBuffers) {
secondRead.append(buffer.toString(StandardCharsets.UTF_8));
}
// 验证两次读取是否相同
Map<String, Object> result = new HashMap<>();
result.put("firstRead", firstRead.toString());
result.put("secondRead", secondRead.toString());
result.put("same", firstRead.toString().equals(secondRead.toString()));
return Mono.just(result);
});
}
}
方案6:集成配置和测试
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import com.fasterxml.jackson.databind.ObjectMapper;
@SpringBootTest
@AutoConfigureMockMvc
public class RequestBodyCachingTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
/**
* 测试请求体可重复读取
*/
@Test
public void testRepeatableRead() throws Exception {
TestRequest request = new TestRequest("测试数据", 123);
mockMvc.perform(MockMvcRequestBuilders.post("/api/test")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.originalBody").exists())
.andExpect(jsonPath("$.repeatedBody").exists())
.andExpect(jsonPath("$.same").value(true));
}
/**
* 测试大请求体处理
*/
@Test
public void testLargeRequestBody() throws Exception {
// 生成1MB的请求体
StringBuilder largeBody = new StringBuilder();
for (int i = 0; i < 10000; i++) {
largeBody.append("这是一段测试文本,用于测试大请求体的处理能力。");
}
Map<String, String> request = Map.of("data", largeBody.toString());
mockMvc.perform(MockMvcRequestBuilders.post("/api/large")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.size").value(largeBody.length()));
}
/**
* 测试过滤器链中的多次读取
*/
@Test
public void testFilterChainReads() throws Exception {
TestRequest request = new TestRequest("过滤器测试", 456);
mockMvc.perform(MockMvcRequestBuilders.post("/api/filter-test")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.filterReads").value(2)) // 过滤器读取了2次
.andExpect(jsonPath("$.controllerReads").value(1)); // 控制器读取了1次
}
/**
* 测试异常处理器中的请求体访问
*/
@Test
public void testExceptionHandlerBodyAccess() throws Exception {
TestRequest request = new TestRequest("异常测试", 789);
mockMvc.perform(MockMvcRequestBuilders.post("/api/error-test")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.requestDetails.body").exists());
}
/**
* 测试多部分请求(文件上传)
*/
@Test
public void testMultipartRequest() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"test.txt",
"text/plain",
"测试文件内容".getBytes(StandardCharsets.UTF_8)
);
mockMvc.perform(MockMvcRequestBuilders.multipart("/api/upload")
.file(file)
.param("name", "测试文件"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.filename").value("test.txt"));
}
static class TestRequest {
private String data;
private int number;
public TestRequest(String data, int number) {
this.data = data;
this.number = number;
}
// getters and setters
}
}
/**
* 配置类:注册所有需要的组件
*/
@Configuration
public class RequestBodyConfig {
/**
* 注册过滤器
*/
@Bean
public FilterRegistrationBean<CachingRequestBodyFilter> cachingRequestBodyFilter() {
FilterRegistrationBean<CachingRequestBodyFilter> registrationBean =
new FilterRegistrationBean<>();
registrationBean.setFilter(new CachingRequestBodyFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1);
registrationBean.setName("cachingRequestBodyFilter");
return registrationBean;
}
/**
* 注册拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestLoggingInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/health");
}
/**
* 配置请求包装器工厂
*/
@Bean
public RequestWrapperFactory requestWrapperFactory() {
return new DefaultRequestWrapperFactory();
}
static class DefaultRequestWrapperFactory implements RequestWrapperFactory {
@Override
public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {
// 根据请求特征选择合适的包装器
String contentType = request.getContentType();
long contentLength = request.getContentLengthLong();
if (contentType != null && contentType.contains("multipart/")) {
// 文件上传请求,不包装
return request;
} else if (contentLength > 10 * 1024 * 1024) {
// 大请求,使用文件缓存包装器
return new CachedBodyLargeRequestWrapper(request);
} else {
// 普通请求,使用内存缓存包装器
return new CachedBodyHttpServletRequest(request);
}
}
}
interface RequestWrapperFactory {
HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException;
}
}
四、性能优化建议
1. 选择合适的缓存策略
- 小请求(<10MB):使用内存缓存
- 大请求(≥10MB):使用临时文件缓存
- 超大请求(>100MB):考虑流式处理,避免完全缓存
2. 内存管理
// 配置内存阈值
@Configuration
public class MemoryConfig {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofMegabytes(100));
factory.setMaxRequestSize(DataSize.ofMegabytes(100));
factory.setFileSizeThreshold(DataSize.ofMegabytes(10)); // 内存阈值
return factory.createMultipartConfig();
}
}
3. 临时文件管理
// 定期清理临时文件
@Component
public class TempFileCleaner {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanOldTempFiles() {
Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"));
try {
Files.walk(tempDir)
.filter(path -> path.getFileName().toString().startsWith("request_body_"))
.filter(path -> Files.isRegularFile(path))
.filter(path -> {
try {
return Files.getLastModifiedTime(path).toMillis() <
System.currentTimeMillis() - 24 * 60 * 60 * 1000;
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
// 记录日志,但不中断
}
});
} catch (IOException e) {
// 记录错误日志
}
}
}
4. 监控和指标
// 请求体缓存监控
@Component
public class RequestBodyMetrics {
private final MeterRegistry meterRegistry;
private final AtomicLong memoryCacheCount = new AtomicLong();
private final AtomicLong fileCacheCount = new AtomicLong();
public RequestBodyMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
initMetrics();
}
private void initMetrics() {
// 注册自定义指标
meterRegistry.gauge("request.body.cache.memory.count", memoryCacheCount);
meterRegistry.gauge("request.body.cache.file.count", fileCacheCount);
// 记录缓存策略分布
meterRegistry.counter("request.body.cache.strategy",
"strategy", "memory");
meterRegistry.counter("request.body.cache.strategy",
"strategy", "file");
}
public void recordCache(String strategy) {
if ("memory".equals(strategy)) {
memoryCacheCount.incrementAndGet();
} else if ("file".equals(strategy)) {
fileCacheCount.incrementAndGet();
}
}
}
五、常见问题FAQ
Q1: 缓存请求体会不会导致内存泄漏?
A: 如果使用内存缓存,确实可能。建议:
- 设置合理的最大缓存大小
- 使用WeakReference或SoftReference
- 及时清理缓存
Q2: 文件上传请求是否需要缓存?
A: 通常不需要。文件上传请求体通常很大,而且Spring已经提供了MultipartFile接口来处理。
Q3: 如何判断是否应该缓存请求体?
A: 可以根据以下因素判断:
- 请求方法(POST/PUT/PATCH)
- 内容类型(JSON/XML)
- 请求大小
- 请求路径(特定API)
Q4: WebFlux和Servlet在处理上有何不同?
A: WebFlux是响应式的,数据流是Flux<DataBuffer>,需要不同的缓存策略。
Q5: 缓存请求体对性能有多大影响?
A: 影响取决于:
- 请求体大小
- 缓存策略(内存/文件)
- 服务器资源
- 通常小请求的影响可以忽略,大请求需要考虑使用流式处理。
六、最佳实践总结
1. 分层缓存策略
请求大小 < 1MB → 内存缓存
1MB ≤ 请求大小 < 10MB → 可选内存或文件缓存
请求大小 ≥ 10MB → 文件缓存或流式处理
2. 配置管理
# application.yml
request:
body:
cache:
enabled: true
memory-threshold: 10MB
max-size: 100MB
temp-dir: /tmp/request-cache
3. 安全考虑
- 敏感数据不要记录到日志
- 及时清理临时文件
- 设置合理的文件权限
4. 监控告警
- 监控缓存命中率
- 监控内存使用情况
- 设置文件系统空间告警
5. 测试策略
- 单元测试:测试包装器功能
- 集成测试:测试过滤器链
- 性能测试:测试大请求处理
通过以上解决方案,可以有效处理Spring Boot 3.x中请求体无法重复读取的问题。根据具体场景选择合适的方案,并注意性能和安全方面的考虑。
更多推荐




所有评论(0)