一、背景介绍
相信很多同学不知道HttpServletRequest输入流只能读取一次,因为一般的功能开发中很少会碰到以下的情况。
1)我们希望写一个过滤器,统一对请求的参数进行校验处理,就会涉及到要读取一次输入流,获取对应的参数值。
2)我们想用aop的方式,对所有controller切面中打印请求日志,所以需要在aop中获取一次请求参数。
3)我们想在项目做统一异常处理,并实现邮件通知功能,邮件内容附带上此时接口请求的参数,方便跟踪问题。
以上的这三种情况,做到后面测试的时候,同学们会发现,控制器里注入的参数居然都是空的,异常处理里面拿到的请求参数也是空的,最后发现自己前面写的东西都白做了!
在 jwt在前后端分离的最佳实践方式(二)这篇文章中,笔者也介绍过一个请求的执行顺序,大概得路径是 请求-》filter-》sevlet-》Interceptor-》aop-》controller,只要在controller前读取过一次请求流,那么后面就再拿不到请求参数了。
二、原因分析
当我们调用getInputStream()
方法获取输入流时,得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承InputStream。
查看InputStream的源码可以看到(这里就不贴代码,大家有兴趣可以去找具体的源码部分),读取流的时候会根据position来获取当前位置,每读取一次,该标志就会移动一次,如果读到最后,read()
返回-1,表示已经读取完了。如果想要重新读取,可以调用inputstream.reset方法,但是能否reset取决于markSupported方法,返回true可以reset,反之则不行。查看ServletInputStream可知,这个类并没有重写markSupported和reset方法。
综上,InputStream默认不实现reset方法,而ServletInputStream也没有重写reset相关方法,这样就无法重复读取流,这就是我们从request对象中输入流只能读取一次的原因。
三、解决方法
一般使用 HttpServletRequestWrapper + Filter 的方式处理,既然ServletInputStream不支持重新读写,那么我们就把流读出来后用容器存储起来,后面就可以多次利用了。
HttpServletRequestWrapper
它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面。
我们定义一个容器,将输入流里面的数据存储到这个容器里,然后我们重写getInputStream方法,每次都从这个容器里读数据,这样我们的输入流就可以读取任意次了。
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
//存储body数据的容器
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
//获取请求流
InputStream requestInputStream = request.getInputStream();
//以btye的方式,将其复制缓存起来
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
}
CachedBodyServletInputStream
缓存输入流,继承 ServletInputStream。
public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
log.error("CachedBodyServletInputStream isFinished err: {}", e.getMessage(), e);
}
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
Filter处理
除了要写一个包装器外,我们还需要在过滤器里将原生的HttpServletRequest对象替换成我们的RequestWrapper对象。
除此以外,为了兼容文件上传的情况,我们还需要将其分开处理,对于multipart/form-data请求,我们使用spring原来的处理方式,不作特殊处理。
@WebFilter(filterName = "httpServletRequestWrapperFilter", urlPatterns = {"/*"})
public class BackendHttpServletRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
try {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String contentType = httpRequest.getContentType();
//这里判断content-type,对于multipart/form-data类型将不作处理
if (StrUtil.isNotBlank(contentType) && contentType.contains(ContentType.MULTIPART.getValue())) {//multipart/form-data类型
//spring中使用MultipartResolver处理文件上传,所以这里需要将其封装往后传递
MultipartResolver resolver = new StandardServletMultipartResolver();
MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(httpRequest);
chain.doFilter(multipartRequest, responseWrapper);
} else {
//对于其他的情况,我们统一使用包装类,将请求流进行缓存到容器
CachedBodyHttpServletRequest cachedBodyHttpServletRequest =
new CachedBodyHttpServletRequest((HttpServletRequest) request);
chain.doFilter(cachedBodyHttpServletRequest, responseWrapper);
}
} finally {
//读取完 Response body 之后,通过这个设置回去,就可以使得接口调用者可以正常接收响应了,否则会产生空响应的情况
//注意要在过滤器方法的最后调用
responseWrapper.copyBodyToResponse();
}
}
}
至此,我们就可以实现拦截器的数据校验、aop日志、统一异常处理的邮件请求内容发送等功能。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/93613.html