Spring Boot 3.x 请求体读取后无法重复读取问题详解

一、问题背景

在Spring Boot 3.x中,HTTP请求体(RequestBody)是一个只能读取一次的输入流。这个问题通常出现在以下场景:

  1. 拦截器(Interceptor)和控制器(Controller)都需要读取请求体
  2. 过滤器(Filter)中读取请求体后,后续处理无法再次读取
  3. 全局异常处理器需要访问请求体内容
  4. 日志记录器需要记录请求体

二、根本原因分析

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: 如果使用内存缓存,确实可能。建议:

  1. 设置合理的最大缓存大小
  2. 使用WeakReference或SoftReference
  3. 及时清理缓存

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中请求体无法重复读取的问题。根据具体场景选择合适的方案,并注意性能和安全方面的考虑。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐