【问题记录】SpringBoot 解决 getReader() has already been called for this request 错误
1,错误日志
getReader() has already been called for this request
2,错误原因
根据Servlet规范,Servlet规范不允许直接多次读取请求体。由于我在拦截器中读取了请求体的信息,又在controller层中使用@RequestBody来获取请求体的信息,导致多次读取请求体的信息,抛出IllegalStateException异常。
3,解决办法
解决方案:
将请求体缓存起来
具体实现:
自定义一个 CacheRequestBodyWrapper 类继承 HttpServletRequestWrapper 类,把请求体信息保存在 CacheRequestBodyWrapper 类中,并重写getReader()和getInputStream()方法,返回新的流对象。这样就可以多次读取请求体信息。
编码实现:可直接复制使用
1,自定义CacheRequestBodyWrapper类保存请求体信息
import com.cms.utils.servlet.ServletUtils;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author Hva
* Request Body 缓存 Wrapper
*/
public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
/**
* 缓存请求体
*/
private final byte[] body;
/**
* 构造函数,初始化时缓存请求体
*/
public CacheRequestBodyWrapper(HttpServletRequest request) {
super(request);
body = ServletUtils.getBodyBytes(request);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
// 返回 ServletInputStream
return new ServletInputStream() {
@Override
public int read() {
return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {}
@Override
public int available() {
return body.length;
}
};
}
}
ServletUtils工具类如下:
我使用的springboot3.x版本,所以使用的是 javax.servlet-api 依赖,使用的工具类为:JakartaServletUtil
如果你的版本是springboot2.x,你应该使用 javax.servlet-api 依赖,工具类使用:ServletUtil
public class ServletUtils {
/**
* 判断是否是 json 请求
*/
public static boolean isJsonRequest(ServletRequest request) {
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
}
/**
* 获取json请求的内容
*/
public static byte[] getBodyBytes(HttpServletRequest request) {
// 只有 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
if (isJsonRequest(request)) {
return JakartaServletUtil.getBodyBytes(request);
}
return null;
}
}
2,自定义过滤器
import com.cms.utils.servlet.ServletUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* @author Hva
* @Description: Request Body 缓存 Filter,实现它的可重复读取
*/
public class CacheRequestBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
// 使用自定义的可重复读的请求体包装类
filterChain.doFilter(new CacheRequestBodyWrapper(request), response);
}
@Override
protected boolean shouldNotFilter(@NotNull HttpServletRequest request) {
// 只处理 json 请求内容
return !ServletUtils.isJsonRequest(request);
}
}
3,Web配置类
在配置类中注册过滤器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
*/
@Bean
public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() {
return createFilterBean(new CacheRequestBodyFilter(), 1);
}
public static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
bean.setOrder(order);
// 拦截所有资源
bean.addUrlPatterns("/*");
return bean;
}
}
完成这些配置后再访问接口就不会报错了!!!