参照:request.getInputStream()输入流只能读取一次问题
问题
- 一个InputStream对象在被读取完成后,将无法被再次读取,始终返回-1;
- InputStream并没有实现reset方法(可以重置首次读取的位置),无法实现重置操作;
因此,当调用了一次getInputStream(),并且将此输入流读取完后,后面再去调用getInputStream(),读取的数据都为空,导致异常:java.io.IOException: Stream closed。
分析
request的inputStream返回的是CoyoteInputStream,该输入流将读取操作实质上是委托给了InputBuffer来实现,而在InputBuffer中,会判断流是否已关闭,如果已关闭,那么将会抛出java.io.IOException: Stream closed的异常。那么流为什么会关闭呢?
这个流不会主动关闭。但是如果在项目里面使用@RequestBody注解,那么jackson在解析json时,读取完流之后,就会关闭这个流,注意是jackson去关闭的这个输入流,一旦关闭这个输入流,inputBuffer里的close属性就置为了true,就会抛出这个异常。已经解释了为什么会抛出这个异常。
但是还有个问题没解决,就是这个CoyoteInputStream输入流,它没有提供reset()方法,也就是读完之后,你再去读它,始终都会返回-1,表示没有数据了,全都读完了。
所以要实现这个输入流的复用,我们首先要拿到这个流,然后把流全部读完,将读取的数据都先存起来,然后再去使用这个读取的数据,并且我们可以使用ByteArrayInputStream将读取的字节转化成输入流,这个字节输入流支持reset方法,下次我们再拿流的的时候,可以调用reset方法,重置下,就可以继续读取了。
解决
请注意:在以下示例中,如果你是在Filter中就把流全都读了一遍的话,因为CoyoteInputStream不支持reset,这个输入流就已经废了,后面再使用@RequestBody注解(内部使用了jackson读取流),就读取不到任何内容了
@PostMapping("test01")
@ResponseBody // 使用@RequestBody会读取一次流, 并且在读取完毕后, 会关闭此流
public Object test01(@RequestBody Person person,HttpServletRequest request) throws IOException {
System.out.println(person);
request.getInputStream().reset(); // 重置输入流, 可再次读取该流
InputStreamReader isReader = new InputStreamReader(request.getInputStream());
BufferedReader bReader = new BufferedReader(isReader);
String line = null;
while ((line = bReader.readLine()) != null) {
System.out.println(line);
}
request.getInputStream().close();
bReader.close();
return "ok";
}
InputStreamFilter
在springboot中,标记为组件的Filter将会自动被识别到,添加到tomcat容器中作为过滤器
@Component
public class InputStreamFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
MyServletRequest req = new MyServletRequest(servletRequest);
filterChain.doFilter(req,servletResponse);
}
@Override
public void destroy() {
}
}
MyServletRequest
自定义ServletRequest包装原始的ServletRequest,重写它的getInputStream方法
@Slf4j
public class MyServletRequest extends HttpServletRequestWrapper {
private final ByteArrayInputStream bais;
public MyServletRequest(HttpServletRequest request) {
super(request);
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
StreamUtils.copy(request.getInputStream(), baos);
byte[] bodyBytes = baos.toByteArray();
bais = new ByteArrayInputStream(bodyBytes);
} catch (IOException e) {
log.error("获取流数据,发生错误");
throw new RuntimeException();
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
new ByteArrayInputStream()
ServletRequest request = super.getRequest();
return new ServletInputStream() {
@Override
public boolean isFinished() {
try {
return request.getInputStream().isFinished();
} catch (IOException e) {
return false;
}
}
@Override
public boolean isReady() {
try {
return request.getInputStream().isReady();
} catch (IOException e) {
return false;
}
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public synchronized void reset() throws IOException {
bais.reset();
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
}
方式二(推荐)
可以再加一个判断,如果前端传过来的content-type是json的话,那就替换,不是的话,就不替换
@Slf4j
public class MyServletRequest extends HttpServletRequestWrapper {
private byte[] bodyBytes;
public MyServletRequest(HttpServletRequest request) {
super(request);
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
StreamUtils.copy(request.getInputStream(), baos);
bodyBytes = baos.toByteArray();
} catch (IOException e) {
log.error("获取流数据,发生错误");
throw new RuntimeException();
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 每次获取输入流的时候, 都返回一个新的输入流
ByteArrayInputStream bais = new ByteArrayInputStream(bodyBytes);
return new ServletInputStream() {
@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 bais.read();
}
};
}
}
@PostMapping("test01")
@ResponseBody // 这样,我们同时使用2个@RequestBody都没关系了
public Object test01(@RequestBody Person person,
@RequestBody Person person2) throws IOException {
System.out.println(person);
System.out.println(person2);
return "ok";
}