Spring Boot 3.x Web MVC实战:实现流缓存的request

上一节《Spring Boot 3.x Filter实战:记录请求日志》实践最后遇到了request对象的流不可重复读的问题,本小节我们将通过流数据缓存以及流的装饰器模式来解决这个问题。如果觉得对你有帮助,记得点赞收藏,关注小卷,后续更精彩!

文章目录

装饰流操作对象

我们知道,在Java语言的流模块设计中大量采用了装饰器模式,来扩展流的特性。这里我们同样会对接收字节数组流数据的ByteArrayInputStream对象进行包装,将其作为一个从ServletInputStream扩展的CachedServletInputStream类型对象的成员变量,进行缓存。看下代码实现:

java 复制代码
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()方法)时,进行重写,实现每次都返回一个携带原始流数据的ServletInputStreamBufferedReader对象即可。

而要返回的ServletInputStream类型,前面我们已经自定义了一个接收原始字节数据完成构造的CachedServletInputStream类型。

看完成的代码实现:

java 复制代码
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应用

最后,我们在RequestLogFilterdoFilterInternal方法中应用前面对请求包装器的实现:

java 复制代码
@Override
protected void doFilterInternal(...) throws ... {
    ...

    CachedHttpServletRequestWrapper requestWrapper = new CachedHttpServletRequestWrapper(request);

    logParams(requestWrapper);

    logRequestBody(requestWrapper);

    filterChain.doFilter(requestWrapper, response);
}

注意,这里记录日志以及放行请求的后续处理操作的都是包装过的requestWrapper对象。

最后,测试一下之前的添加购物车API,ok!

相关推荐
_UMR_27 分钟前
springboot集成Jasypt实现配置文件启动时自动解密-ENC
java·spring boot·后端
蓝色王者1 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
hashiqimiya3 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
因我你好久不见3 小时前
Windows部署springboot jar支持开机自启动
windows·spring boot·jar
无关86884 小时前
SpringBootApplication注解大解密
spring boot
追梦者1236 小时前
springboot整合minio
java·spring boot·后端
帅气的你6 小时前
Spring Boot 集成 AOP 实现日志记录与接口权限校验
java·spring boot
计算机毕设VX:Fegn08957 小时前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
计算机毕设VX:Fegn08957 小时前
计算机毕业设计|基于springboot + vue博物馆展览与服务一体化系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
帅气的你7 小时前
Spring Boot 1.x 接口性能优化:从 3 秒到 200 毫秒的实战调优之路
java·spring boot