今天在给一个项目加日志切面,在从request里面获取body参数时,报错:java.io.IOException: Stream closed。
先看一下相关代码。
private String printToLogStringByRequestBody(HttpServletRequest request) {
String body = "";
ServletInputStream inputStream = null;
try {
inputStream = request.getInputStream();
body = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
} catch (IOException e) {
log.error("body to string is error !", e);
} finally {
IOUtils.closeQuietly(inputStream);
}
return body;
}
报错信息很简单,就是说从inputStream获取body时,该字节流已关闭,无法再读取。打断点可以看到closed这个属性是true。
首先我肯定没有对它进行手动关闭。但是有一个很奇怪的地方,在调用一部分接口的时候不会报错,报错的接口有一个共同点,就是参数加了@RequestBody
注解。随后我就在网上查了一下,基本上确定了问题就是出在这个注解上。
stackoverflow上面有个回答解释了这个问题。
You use
@RequestBody User user
for first case, so Spring framework already
processed input stream of HttpServletRequest instance (can happen only
once by default), parsed data and injected user instance to your
handler.
So input stream is closed when handler is called. You can overload
attemptAuthentication method and pass user instance as a param
instead.
I suppose you don’t have any Spring mappings for second case, so input
stream stays “untouched” and you are able to process it within the
authentication method.
翻译一下就是:
对于第一种情况,Spring框架已经处理了HttpServletRequest实例的输入流(默认情况下只能发生一次),解析数据并将user实例注入到处理程序中。
因此,在调用处理程序时,输入流将关闭。 你可以重载ttemptAuthentication方法,然后将user实例作为参数传递。
我想你在第二种情况下没有任何Spring映射,因此输入流保持“不变”,并且您可以在身份验证方法中对其进行处理。
所以我们要做的就是写一个RequestWrapper
把request
中的输入流保存一下。
下面是代码:
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
/**
* @description 将request中输入流中的内容保存起来
* @param request HttpServletRequest
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
byte[] bytes = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
bytes = IOUtils.toByteArray(inputStream);
} catch (IOException e) {
log.error("requestWrapper error", e);
} finally {
IOUtils.closeQuietly(inputStream);
}
body = bytes;
}
/**
* @description 重写getInputStream,返回保存在属性中的body
* @return javax.servlet.ServletInputStream
*/
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
}
@Component
@ServletComponentScan
public class HttpServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
/**
* @description 将自定义的ServletRequest替换进filterChain中,使request可以重复读取
* @param servletRequest servletRequest
* @param servletResponse servletResponse
* @param filterChain filterChain
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest request = null;
if (servletRequest instanceof HttpServletRequest) {
request = new RequestWrapper((HttpServletRequest) servletRequest);
}
if (request != null) {
filterChain.doFilter(request, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
}
}
注意加上@Component
和@ServletComponentScan
注解,原来的代码不需要做任何修改。