项目场景:
现有需求要提供出一些接口,对第三方使用,在对接口签名的过程中出现了问题。
问题描述:
通过AOP获得请求中@RequestBody数据时,出现 "stream closed"异常的问题。
java.io.IOException: Stream closed
原因分析:
在进入AOP前,spring框架在使用 对象类型转换器(converter)时,已经使用过一次body了(获取body是以流的形式),所以在AOP切面处理时,想获取body体数据,发生"stream closed"异常。
spring源码中的核心方法 :
readWithMessageConverters()
代码片段
感兴趣的同学可以断点试一下,第一次断点进入时,这里是正常的;可以观察一下这段代码和AOP的执行顺序。
解决方案:
自己封装一个新的request
public class RequestWrapper extends HttpServletRequestWrapper {
/**
* 存储body数据的容器
*/
private final byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 将body数据存储起来
String bodyStr = getBodyString(request);
body = bodyStr.getBytes(Charset.defaultCharset());
}
/**
* 获取请求Body
*
* @param request request
* @return String
*/
public String getBodyString(final ServletRequest request) throws IOException {
return inputStream2String(request.getInputStream());
}
/**
* 获取请求Body
*
* @return String
*/
public String getBodyString() throws IOException {
final InputStream inputStream = new ByteArrayInputStream(body);
return inputStream2String(inputStream);
}
/**
* 将inputStream里的数据读取出来并转换成字符串
*
* @param inputStream inputStream
* @return String
*/
private String inputStream2String(InputStream inputStream) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
/**
* @author 签名校验过滤器
*/
@Slf4j
public class SignStreamFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String contentType = req.getContentType();
String method = "multipart/form-data";
if (contentType != null && contentType.contains(method)) {
// 将转化后的 request 放入过滤链中
request = new StandardServletMultipartResolver().resolveMultipart(request);
}
// 扩展request,使其能够能够重复读取requestBody
ServletRequest requestWrapper = new RequestWrapper(request);
// 这里需要放行,但是要注意放行的 request是requestWrapper
chain.doFilter(requestWrapper, resp);
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
@Configuration
public class SignAutoConfiguration {
@Bean
public SignStreamFilter initSignFilter() {
return new SignStreamFilter();
}
@Bean
public FilterRegistrationBean webAuthFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(initSignFilter());
registration.setName("signFilter");
registration.addUrlPatterns("/*");
registration.setOrder(0);
return registration;
}
}
再放两张debug图
第一张是原生请求的request对象,第二张是针对inputstream流只能用一次封装的request对象,可以看出封装的request中多出了"body"属性。
PS : 上面的方法有一个坑点,之前上面解决了"stream closed"的后,如果有使用到上传文件功能(multipart)一样会出现异常。在方法中已经解决;主要思路是,request进入过滤器时,判断类型如果是multipart类型,则将request手动封装为multipart类型。
HttpServletRequest request = (HttpServletRequest) req;
String contentType = req.getContentType();
String method = "multipart/form-data";
if (contentType != null && contentType.contains(method)) {
// 将转化后的 request 放入过滤链中
request = new StandardServletMultipartResolver().resolveMultipart(request);
}