问题描述
Java项目开发中可能存在以下几种情况:
1、你需要在拦截器中统一拦截请求,拿到请求中的参数,来做统一的判断处理或者其他操作。
那问题就来了,由于request输入流的数据只能读取一次,所以你在拦截器中你读取了输入流的数据,当请求进入后边的Controller时,输入流中已经没有数据了,导致获取不到数据。
2、你项目里可能需要搞一个统一的异常处理器,然后想在异常处理器中把发生异常的接口地址,方法名,以及请求的参数记录到日志里或者直接发送给你配置的告警系统,比如发送给钉钉群通知。这种情况下,因为你前边controller已经获取过一次request输入流了,在后边的异常处理器里你还想再从request输入流中拿到请求参数等信息,所以也会出现request流只能读取一次的错误。
以上两种情况是开发中比较常见的,当然除此之外,别的场景下你可能也会遇到request流只能读取一次的错误,所以今天就来讲一下如果遇到这种情况该怎么解决。
产生原因
1、一个InputStream对象在被读取完成后,将无法被再次读取,始终返回-1;
2、InputStream并没有实现reset方法(可以重置首次读取的位置),无法实现重置操作;
解决方法
一、引入依赖
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
二、自定义Wrapper类来包装HttpServletRequest
我们需要写一个自定义包装类,并继承HttpServletRequestWrapper
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @描述 包装HttpServletRequest
* MyServletRequestWrapper + RequestReplaceFilter 的作用是:
* 解决异常处理器中拿post请求的json参数时,报request流只能读一次的错
* 原因是 request.getReader() 和 request.getInputStream() 都是只能调用一次
* 所以我这里要使用 HttpServletRequestWrapper 来实现自定义的 MyServletRequestWrapper包装类
* 把request里的 body 保存在 MyServletRequestWrapper中, 并且重写 getInputStream()方法
* 然后所有的request都在RequestReplaceFilter中被转换成了我自定义的HttpServletRequestWrapper
* 然后获取 body时就都是调用 MyServletRequestWrapper中的 getBody()方法了
* @创建人 caoju
*/
public class MyServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public MyServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = IOUtils.toByteArray(super.getInputStream());
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new RequestBodyCachingInputStream(body);
}
private class RequestBodyCachingInputStream extends ServletInputStream {
private byte[] body;
private int lastIndexRetrieved = -1;
private ReadListener listener;
public RequestBodyCachingInputStream(byte[] body) {
this.body = body;
}
@Override
public int read() throws IOException {
if (isFinished()) {
return -1;
}
int i = body[lastIndexRetrieved + 1];
lastIndexRetrieved++;
if (isFinished() && listener != null) {
try {
listener.onAllDataRead();
} catch (IOException e) {
listener.onError(e);
throw e;
}
}
return i;
}
@Override
public boolean isFinished() {
return lastIndexRetrieved == body.length - 1;
}
@Override
public boolean isReady() {
return isFinished();
}
@Override
public void setReadListener(ReadListener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener cann not be null");
}
if (this.listener != null) {
throw new IllegalArgumentException("listener has been set");
}
this.listener = listener;
if (!isFinished()) {
try {
listener.onAllDataRead();
} catch (IOException e) {
listener.onError(e);
}
} else {
try {
listener.onAllDataRead();
} catch (IOException e) {
listener.onError(e);
}
}
}
@Override
public int available() throws IOException {
return body.length - lastIndexRetrieved - 1;
}
@Override
public void close() throws IOException {
lastIndexRetrieved = body.length - 1;
body = null;
}
}
}
三、创建过滤器,通过过滤器包装原有的request对象
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @描述
* @创建人 caoju
*/
@Component
public class RequestReplaceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!(request instanceof MyServletRequestWrapper)) {
request = new MyServletRequestWrapper(request);
}
filterChain.doFilter(request, response);
/*//如果有文件上传的业务场景,需要用下面的代码进行处理,不然文件上传的流会有问题
String contentType = request.getContentType();
//如果contentType是空
//或者contentType是多媒体的上传类型则忽略,不进行包装,直接return
if (contentType == null) {
filterChain.doFilter(request, response);
return;
}else if(request.getContentType().startsWith("multipart/")){
filterChain.doFilter(request, response);
return;
}else if (!(request instanceof MyServletRequestWrapper)) {
request = new MyServletRequestWrapper(request);
}
filterChain.doFilter(request, response);
*/
}
}
通过以上几步,我们就实现了把request里的 body 保存在 MyServletRequestWrapper中的效果
就可以在整个请求链路中任何地方去重复的获取request流了
四、使用案例
配置好之后,就可以在整个请求链路中任何地方去重复的获取request流了。
比如,你可以在请求刚进来时,在过滤器或者拦截器里拿到request对象,再拿到request对象的流数据,去做一些事情,或者你也可以在请求即将结束时,在统一的异常处理器中拿到request对象,拿到request对象流数据里请求的json参数;等等等等,还有其他很多你想使用的场景,都可以这么做。
下面是在代码中利用RequestContextHolder获取request对象,拿到request对象后就可以获取请求方式、请求url、以及请求参数这些数据了。如果你在某些地方也有需要打印记录请求方式、请求url、请求参数的这些需求,那可以直接复制粘贴我下边的代码就ok了
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
String mode = "";
String methodUrl = "";
String param = "";
if (requestAttributes != null) {
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
//请求方式
mode = request.getMethod();
//方法URL
methodUrl = request.getRequestURI();
if(mode.equals(HttpMethod.GET.name())){
param = request.getQueryString();
}
if(mode.equals(HttpMethod.POST.name())){
param = getJsonRequest(request);
}
}
/**
* 获取Request中的JSON字符串
* @param request
* @return
* @throws IOException
*/
public static String getJsonRequest(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = request.getReader();) {
char[] buff = new char[1024];
int len;
while ((len = reader.read(buff)) != -1) {
sb.append(buff, 0, len);
}
} catch (IOException e) {
log.error("POST请求参数获取异常", e);
}
return sb.toString();
}
ok,到这里解决request流只能获取一次的问题就搞定了
希望对你有所帮助