上一节《Spring Boot 3.x Filter实战:记录请求日志》实践最后遇到了request
对象的流不可重复读的问题,本小节我们将通过流数据缓存以及流的装饰器模式来解决这个问题。如果觉得对你有帮助,记得点赞收藏,关注小卷,后续更精彩!
我们知道,在Java
语言的流模块设计中大量采用了装饰器模式,来扩展流的特性。这里我们同样会对接收字节数组流数据的ByteArrayInputStream
对象进行包装,将其作为一个从ServletInputStream
扩展的CachedServletInputStream
类型对象的成员变量,进行缓存。看下代码实现:
package com.juan.demo.common.web.support.servlet;
import ...
@Slf4j
public class CachedServletInputStream extends ServletInputStream {
private final InputStream cachedInputStream;
public CachedServletInputStream(byte[] cachedByteArray) {
this.cachedInputStream = new ByteArrayInputStream(cachedByteArray);
}
@Override
public boolean isFinished() {
try {
return cachedInputStream.available() == 0;
} catch (IOException e) {
log.error(e.getMessage());
}
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return cachedInputStream.read();
}
}
该流操作对象扩展自ServletInputStream
,可以提供给外部作为请求的流对象使用,在调用ServletInputStream
相关流操作方法时,实际调用的是该包装类重写的相关方法。这里主要关注isFinished()
和read()
方法,它们的实现其实就是调用被缓存和包装的流对象的相关方法。
对于http servlet
请求,Java EE
提供了HttpServletRequestWrapper
包装器类对原始的请求对象进行包装,这对应用层的开发人员来说是透明的,咱们仅关注面向HttpServletRequest
接口的编程,实际运行时的调用会对包装器对象调用内部被包装对象来完成相应的功能。
这里我们仅仅要做的就是扩展HttpServletRequestWrapper
,并缓存最原始的字节输出流数据。而在调用目标请求对象相关方法(这里是getInputStream()
和getReader()
方法)时,进行重写,实现每次都返回一个携带原始流数据的ServletInputStream
和BufferedReader
对象即可。
而要返回的ServletInputStream
类型,前面我们已经自定义了一个接收原始字节数据完成构造的CachedServletInputStream
类型。
看完成的代码实现:
package com.juan.demo.common.web.support.servlet;
import ...
public class CachedHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] cachedByteArray;
public CachedHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
copyInputStreamIfNecessary();
return new CachedServletInputStream(this.cachedByteArray);
}
@Override
public BufferedReader getReader() throws IOException {
copyInputStreamIfNecessary();
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(this.cachedByteArray)));
}
private void copyInputStreamIfNecessary() throws IOException {
if (this.cachedByteArray == null) {
this.cachedByteArray = StreamUtils.copyToByteArray(getRequest().getInputStream());
}
}
}
延迟拷贝流数据
注意我们这里的设计,并不是在构造
CachedHttpServletRequestWrapper
时就进行流数据的拷贝,而是推迟到需要使用时才调用copyInputStreamIfNecessary()
去做拷贝。这种方式更加的安全,也考虑到一般一个请求的处理在一个请求线程上,不会有线程安全问题。
最后,我们在RequestLogFilter
的doFilterInternal
方法中应用前面对请求包装器的实现:
@Override
protected void doFilterInternal(...) throws ... {
...
CachedHttpServletRequestWrapper requestWrapper = new CachedHttpServletRequestWrapper(request);
logParams(requestWrapper);
logRequestBody(requestWrapper);
filterChain.doFilter(requestWrapper, response);
}
注意,这里记录日志以及放行请求的后续处理操作的都是包装过的requestWrapper
对象。
最后,测试一下之前的添加购物车API,ok!