Spring Boot 请求体支持多次获取:重写请求体、缓存请求体

package cn.com.xuxiaowei.filter;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.util.ContentCachingRequestWrapper;

// Spring Boot 2 环境
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// Spring Boot 3 环境
//import jakarta.servlet.FilterChain;
//import jakarta.servlet.ServletException;
//import jakarta.servlet.http.HttpFilter;
//import jakarta.servlet.http.HttpServletRequest;
//import jakarta.servlet.http.HttpServletResponse;

/**
 * 缓存请求体,将 {@link HttpServletRequest} 强制类型转换为 {@link ContentCachingRequestWrapper},
 * 使用 {@link ContentCachingRequestWrapper#getContentAsByteArray()}、{@link ContentCachingRequestWrapper#getContentAsString()} 获取请求体。
 * <p>
 * 1. {@link RequestBody} 与 {@link ContentCachingRequestWrapper#getContentAsByteArray()}、{@link ContentCachingRequestWrapper#getContentAsString()} 相互不影响。
 * <p>
 * 2. 在 Spring Boot 2 中,只有 {@link ContentCachingRequestWrapper#getContentAsByteArray()},无 {@link ContentCachingRequestWrapper#getContentAsString()}
 *
 * @author xuxiaowei
 * @since 0.0.1
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ContentCachingRequestBodyFilter extends HttpFilter {

	@Override
	protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		ContentCachingRequestWrapper contentCachingRequestWrapper = new ContentCachingRequestWrapper(request);
		super.doFilter(contentCachingRequestWrapper, response, chain);
	}

}
package cloud.xuxiaowei.filter;

import cloud.xuxiaowei.utils.servlet.InputStreamHttpServletRequestWrapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

// Spring Boot 2 环境
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// Spring Boot 3 环境
//import jakarta.servlet.FilterChain;
//import jakarta.servlet.ServletException;
//import jakarta.servlet.http.HttpFilter;
//import jakarta.servlet.http.HttpServletRequest;
//import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/**
 * 重写请求流
 *
 * @author xuxiaowei
 * @since 0.0.1
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class HttpServletRequestInputStreamFilter extends HttpFilter {

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        InputStreamHttpServletRequestWrapper httpServletRequestWrapper = new InputStreamHttpServletRequestWrapper(request);
        super.doFilter(httpServletRequestWrapper, response, chain);
    }

}
package cloud.xuxiaowei.utils.servlet;

// 使用 org.apache.tomcat.embed:tomcat-embed-core 依赖
import org.apache.tomcat.util.http.fileupload.IOUtils;

// 使用 commons-io:commons-io 依赖
//import org.apache.commons.io.IOUtils;

// Spring Boot 2 环境
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

// Spring Boot 3 环境
//import jakarta.servlet.ReadListener;
//import jakarta.servlet.ServletInputStream;
//import jakarta.servlet.http.HttpServletRequest;
//import jakarta.servlet.http.HttpServletRequestWrapper;

import java.io.*;

/**
 * 重写请求流,请求流支持多次获取 {@link HttpServletRequest}
 *
 * @author xuxiaowei
 * @see <a href=
 * "https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once/17129256#17129256">http-servlet-request-lose-params-from-post-body-after-read-it-once</a>
 * @since 0.0.1
 */
public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper {

	/**
	 * 用于缓存输入流
	 */
	private ByteArrayOutputStream cachedBytes;

	public InputStreamHttpServletRequestWrapper(HttpServletRequest request) {
		super(request);
	}

	@Override
	public ServletInputStream getInputStream() throws IOException {
		if (cachedBytes == null) {
			// 首次获取流时,将流放入 缓存输入流 中
			cacheInputStream();
		}

		// 从 缓存输入流 中获取流并返回
		return new CachedServletInputStream(cachedBytes.toByteArray());
	}

	@Override
	public BufferedReader getReader() throws IOException {
		return new BufferedReader(new InputStreamReader(getInputStream()));
	}

	/**
	 * 首次获取流时,将流放入 缓存输入流 中
	 */
	private void cacheInputStream() throws IOException {
		// 缓存输入流以便多次读取。为了方便, 我使用 org.apache.commons IOUtils
		cachedBytes = new ByteArrayOutputStream();
		IOUtils.copy(super.getInputStream(), cachedBytes);
	}

	/**
	 * 读取缓存的请求正文的输入流
	 * <p>
	 * 用于根据 缓存输入流 创建一个可返回的 {@link ServletInputStream}
	 */
	public static class CachedServletInputStream extends ServletInputStream {

		private final ByteArrayInputStream input;

		public CachedServletInputStream(byte[] buf) {
			// 从缓存的请求正文创建一个新的输入流
			input = new ByteArrayInputStream(buf);
		}

		@Override
		public boolean isFinished() {
			return false;
		}

		@Override
		public boolean isReady() {
			return false;
		}

		@Override
		public void setReadListener(ReadListener listener) {

		}

		@Override
		public int read() throws IOException {
			return input.read();
		}

	}

}