IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 优雅实现Spring多次读取InputStream -> 正文阅读

[Java知识库]优雅实现Spring多次读取InputStream

问题

在使用Spring的时候,很多人遇到过一个这样的问题,就是当我们想要在自己的业务代码中通过 HttpServletRequest 获取当前请求的流时,会报如下异常信息:

java.io.IOException: Stream closed

原因分析

HttpServletRequest 中的输入流只能读取一次,默认情况下在Spring帮我们处理了反序列化等操作之后,流已经关闭了,如果这个时候再想从 Request 中读取 body 等信息,就会报以上异常。

首先我们来看看为什么 HttpServletRequest 的输入流只能读取一次,当我们调用 getInputStream() 方法获取输入流时,得到的一个是一个 InputStream 对象,而实际类型是 ServletInputStream,它继承于 InputStream

InputStream 中的 read() 方法内部有一个 position,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回 -1,表示已经读取完了。如果想要重新读取,则需要调用 reset() 方法,position就会移动到上次调用 mark() 方法的位置,mark 默认是0,所以就能从头再读了。调用 reset() 方法的前提条件是已经重写了 reset() 方法,当然能否 reset 也是有条件的,它取决于 markSupported() 方法是否返回 true

InputStream 默认是不实现 reset() 方法的,并且 markSupported() 方法返回 false,如下所示:
在这里插入图片描述
在这里插入图片描述
而Servlet提供的 ServletInputStream 中也是没有实现这两个方法的,因此可以判定 ServletInputStream 天生也不支持重复读取。

解决方案

既然明白了为什么在 Java Web 中无法重复读取 InputStream ,那么解决问题的办法的思路就很明显了—— 缓存流数据。即在请求达到时,我们只需要想办法把请求的流数据缓存起来,这样再次读取时直接读取我们缓存的即可。

既然有了思路,那么接下来就是如何来实现的问题了 ,既然 “罪魁祸首” ServletInputStream 无法支持重复读取,基于OOP以及设计模式的思想,要想扩展(增强)一个类,首先想到的就是 装饰器模式,而万幸的是Java 也想到了这个点,因此为我们提供了一个用于扩展 ServletInputStream 的包装器类,也是我们今天的主角 —— HttpServletRequestWrapper

使用 HttpServletRequestWrapper 我们可以轻而易举的自定义我们自己的 Request ,那么接下来就是思考如何缓存输入流数据了。

我们知道基于流的交互,数据主体格式都是二进制,而二进制可以使用 byte[] 来进行表示。因此我们的解决方法呼之欲出了——使用字节数组缓存输入流。

自定义HttpServletRequest

基于上述解决方案,我们来实现一个名为 CachedBodyHttpServletRequestWrapperHttpServletRequest 装饰器类,代码如下:

版本一

@Slf4j
public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 保存流便于重复读取
     */
    private final byte[] cachedBody;

    public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

好了,自定义的Request我们封装好了,接下来需要考虑如何使我们自定义的包装类生效呢?要解决这个问题,我们需要先了解对于Java Web应用来说,一个请求从客户端到达我们的控制器层中间需要经历哪些组件,如下图所示:
在这里插入图片描述
从上图中我们可以清楚的知道请求从 Web 容器转发到我们的程序中时,最先经过的就是过滤器层(Filter),因此我们如果想要使得我们自定义的 CachedBodyHttpServletRequestWrapper 生效,并且后续其他层读取流产生影响(可重复读),最好的方式就是在过滤器层对 Java 原生的 HttpServletRequest 进行 “偷梁换柱”

自定义Filter

在基于Spring开发的项目中,自定义Filter的方式有很多,这里我推荐使用继承 Spring 提供的 OncePerRequestFilter 抽象类来实现,该抽象类从字面意思上来看就知道通过这种方式定义的过滤器,每次请求只会调用一次(这是Spring为了兼容不同容器或不同版本的Servlet对过滤器的处理方式不同造成的差异)。

@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CachedBodyFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("Invoke the CachedBodyFilter to wrapper the HttpServletRequest object.");
        filterChain.doFilter(new CachedBodyHttpServletRequestWrapper(request),response);
    }
}

为了保证我们自定义的过滤器优先执行,这里使用注解 @Order 设置了最高优先级。

接下来我们来测试一把,测试代码如下:

@Data
@ToString
public class TestModel {

    private Long id;

    private String name;
}

@PostMapping("/test4")
    public String test4(@RequestBody TestModel model, HttpServletRequest request) throws IOException {
        System.out.println(model);
        System.out.println("再次读取InputStream");
        TestModel model1 = JSON.parseObject(request.getInputStream(), StandardCharsets.UTF_8,new TypeReference<TestModel>(){}.getType());
        System.out.println(model1);
        return "Ok";
    }

控制台输出结果如下:

TestModel(id=1, name=admin)
再次读取InputStream
TestModel(id=1, name=admin)

这样就结束了吗?没那么简单,在 Spring Boot 2.1.x版本的时候这样是可以的,但是在 Spring Boot 2.2.0以后如果请求的Content-Typemultipart/form-data 或者 application/x-www-form-urlencode 那么这种失效方式就无法获取数据了。因此我们要对这种情况做特殊处理,如下代码所示:

@Slf4j
public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 保存流便于重复读取
     */
    private final byte[] cachedBody;

    public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String contentType = request.getContentType();
        //判断当前请求数据类型是否为表单提交
        if (!StringUtils.isEmpty(contentType) && (contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE) || contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE))) {
            String bodyString = "";
            Map<String, String[]> parameterMap = request.getParameterMap();
            if (!CollectionUtils.isEmpty(parameterMap)) {
                bodyString = parameterMap.entrySet().stream().map(x -> {
                    String[] values = x.getValue();
                    return x.getKey() + "=" + (values != null ? (values.length == 1 ? values[0] : Arrays.toString(values)) : null);
                }).collect(Collectors.joining("&"));
            }
            this.cachedBody = bodyString.getBytes();
        } else {
            this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

再来测试一下:

@PostMapping("/test2")
    @PostMapping("/test2")
    public String test2(TestModel model,HttpServletRequest request) throws IOException {
        System.out.println(model);
        System.out.println("再次读取InputStream");
        String s = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        System.out.println(s);
        return "Ok";
    }

使用Postman来测试如下图:
在这里插入图片描述
控制台输出结果如下:

TestModel(id=1, name=admin)
再次读取InputStream
id=1&name=admin

好了,打完收工!!!!

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-09-24 20:42:45  更:2022-09-24 20:47:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 9:06:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码