首页 > 其他分享 >SpringBoot项目中HTTP请求体只能读一次?试试这方案

SpringBoot项目中HTTP请求体只能读一次?试试这方案

时间:2024-08-07 20:39:03浏览次数:14  
标签:HttpServletRequest HTTP SpringBoot 自定义 试试 getReader public getInputStream 读取

问题描述

在基于Spring开发Java项目时,可能需要重复读取HTTP请求体中的数据,例如使用拦截器打印入参信息等,但当我们重复调用getInputStream()或者getReader()时,通常会遇到类似以下的错误信息:
image
大体的意思是当前request的getInputStream()已经被调用过了。那为什么会出现这个问题呢?

原因分析

主要原因有两个,一是Java自身的设计中,InputStream作为数据管道本身只支持读取一次,如果要支持重复读取的话就需要重新初始化;二是Servlet容器中Request的实现问题,我们以默认的Tomcat为例,可以发现在Request有两个boolean类型的属性,分别是usingReader和usingInputStream,当调用getInputStream()或getReader()时会分别检查两个属性的值,并在执行后将对应的属性设置为true,如果在检查时变量的值已经为true了,那么就会报出以上错误信息。
image

解决方案

不太可行的方案:简单粗暴的反射机制

涉及到变量的修改,我们首先想到的就是有没有提供方法进行修改,不过可惜的是usingReader和usingInputStream并未提供,所以想要在使用过程中修改这两个属性估计只能靠反射了,在使用过程中每次调用后通过反射将usingReader和usingInputStream设置为false,每次根据读取出的内容把数据流初始化回去,理论上就可以再次读取了。

首先说反射机制本身就是通过破坏类的封装来实现动态修改的,有点过于粗暴了,其次也是主要原因,我们只能针对我们自己实现的代码进行处理,框架本身如果调用getInputStream()和getReader()的话,我们就没法通过这个办法干预了,所以这个方案在给予Spring的Web项目中并不可行。

理论上可行的方案:HttpServletRequest接口

HttpServletRequest是一个接口,理论上我们只需要创建一个实现类就可以自定义getInputStream()和getReader()的行为,自然也就能解决RequestBody不能重复读取的问题,但这个方案的问题在于HttpServletRequest有70个方法,而我们只需要修改其中两个而已,通过这种方式去解决有点得不偿失。

部分场景可行的方案:ContentCachingRequestWrapper

Spring本身提供了一个Request包装类来处理重复读取的问题,即ContentCachingRequestWrapper,其实现思路就是在读取RequestBody时将内存缓存到它内部的一个字节流中,后续读取可以通过调用getContentAsString()或getContentAsByteArray()获取到缓存下来的内容。

之所以说这个方案是部分场景可行主要是两个方面,一是ContentCachingRequestWrapper没有重写getInputStream()和getReader()方法,所以框架中使用这两个方法的地方依然获取不到缓存下来的内容,仅支持自定义的业务逻辑;第二点和第一点有所关联,因为其没有修改getInputStream()和getReader()方法,所以我们在使用时只能在使用RequestBody注解后使用ContentCachingRequestWrapper,否则就会出现RequestBody注解修饰的参数无法正常读取请求体的问题,也就限定了它的使用范围如下图所示:
image

如果仅需要在业务代码后再次读取请求体内容,那么使用ContentCachingRequestWrapper也足以满足需求,具体使用方法请参考下一节的说明。

目前的最佳实践:继承HttpServletRequestWrapper

之前我们提到实现HttpServletRequest需要实现70个方法,所以不太可能自行实现,这个方案算是进阶版本,继承HttpServletRequest的实现类,之后再自定义我们需要修改的两个方法。

HttpServletRequest作为一个接口,肯定会有其实现去支撑它的业务功能,因为Servlet容器的选择较多,我们也不能使用某一方提供的实现,所以选择的范围也就被限制到了Java EE(现在叫Jakarta EE)标准范围内,通过查看HttpServletRequest的实现,可以发现在标准内提供了一个包装类:HttpServletRequestWrapper,我们的方案也是围绕它展开。

思路简述

  1. 自定义子类,继承HttpServletRequestWrapper,在子类的构造方法中将RequestBody缓存到自定义的属性中。
  2. 自定义getInputStream()和getReader()的业务逻辑,不再校验usingReader和usingInputStream,且在调用时读取缓存下来的内容。
  3. 自定义Filter,将默认的HttpServletRequest替换为自定义的包装类。

代码展示

  1. 继承HttpServletRequestWrapper,实现子类CustomRequestWrapper,并自定义getInputStream()和getReader()的业务逻辑
// 1.继承HttpServletRequestWrapper
public class CustomRequestWrapper extends HttpServletRequestWrapper {

    // 2.定义final属性,用于缓存请求体内容
    private final byte[] content;

    public CustomRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 3.构造方法中将请求体内容缓存到内部属性中
        this.content = StreamUtils.copyToByteArray(request.getInputStream());
    }

    // 4.重新getInputStream()
    @Override
    public ServletInputStream getInputStream() {
        // 5.将缓存下来的内容转换为字节流
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() {
                // 6.读取时读取第5步初始化的字节流
                return byteArrayInputStream.read();
            }
        };
    }

    // 7.重写getReader()方法,这里复用getInputStream()的逻辑
    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}
  1. 自定义Filter将默认的HttpServletRequest替换为自定义的CustomRequestWrapper
// 1.实现Filter接口,此处也可以选择继承HttpFilter
public class RequestWrapperFilter implements Filter {
    // 2. 重写或实现doFilter方法
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 3.此处判断是为了缩小影响范围,本身CustomRequestWrapper只是针对HttpServletRequest,不进行判断可能会影响其他类型的请求
        if (request instanceof HttpServletRequest) {
            // 4.将默认的HttpServletRequest转换为自定义的CustomRequestWrapper
            CustomRequestWrapper requestWrapper = new CustomRequestWrapper((HttpServletRequest) request);
            // 5.将转换后的request传递至调用链中
            chain.doFilter(requestWrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }
}
  1. 将Filter注册到Spring容器,这一步可以通过多种方式执行,这里采用比较传统但比较灵活的Bean方式注册,如果图方便可以通过ServletComponentScan注解+ WebFilter注解的方式。
/**
 * 过滤器配置,支持第三方过滤器
 */
@Configuration
public class FilterConfigure {
    /**
     * 请求体封装
     * @return
     */
    @Bean
    public FilterRegistrationBean<RequestWrapperFilter> filterRegistrationBean(){
        FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new RequestWrapperFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
}

至此我们就可以在项目中重复读取请求体了,如果选择使用Spring提供的ContentCachingRequestWrapper,那么在Filter中将CustomRequestWrapper替换为ContentCachingRequestWrapper即可,不过需要注意在上一节提到的可用范围较小的问题。

文章内的代码可以参考 https://gitee.com/itartisans/itartisans-framework,这是我开源的一个SpringBoot项目脚手架,我会不定期加入一些通用功能,欢迎关注。

标签:HttpServletRequest,HTTP,SpringBoot,自定义,试试,getReader,public,getInputStream,读取
From: https://www.cnblogs.com/itartisans/p/18347861

相关文章

  • springboot的jar在linux上sh启动脚本
     java在linux上start、stop、restart、status等启动命令,sh脚本,appMgr.sh放在reources/ops下#!/usr/bin/shAPP_NAME="@project.name@[email protected]@.jar"DEPLOY_PATH=`pwd`#JVM启动参数1JVM_PARAMS="-Dfastjson.parser.safeMode=true"command=$1#nohup......
  • 使用Streamlit构建一个web模拟HTTP请求工具
    目录前言HTTP工具功能点:1.导入库: 2.设置页面配置:3.Markdown格式的说明文本:4.用户输入界面:5.发送请求按钮和逻辑:6.发送HTTP请求并计算请求细节:7.总结 前言    最初就是因为在微信看到一篇文章中,看到此http工具的制作因为觉得Streamlit有无限......
  • java httpclient发送中文乱码
    在使用Java的HttpClient发送请求时,如果遇到中文乱码问题,通常需要确保请求和响应的字符集都正确设置为UTF-8。以下是一些解决方法:指定请求数据的字符集为UTF-8格式:在使用UrlEncodedFormEntity或StringEntity时,确保传递正确的字符集参数。例如:StringEntityentity=newUrlEnco......
  • Springboot计算机毕业设计大学生请假系统(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表学生,教师,学院,专业,班级,请假信息,请假条,销假信息,公告信息,出勤率开题报告内容一、选题背景与意义随着高等教育的普及和学生数量的不断增加,传统的学生请假......
  • Springboot计算机毕业设计大学生档案管理系统-程序+源码
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表学生,教师,学生档案,班级成绩单,登记表,个人荣誉开题报告内容一、研究背景与意义研究背景:随着高等教育的发展,大学生人数的不断增加,学生档案信息的数量急剧增长......
  • Springboot计算机毕业设计打印助手平台21swx
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表打印参数,用户,店铺,打印服务,打印订单开题报告内容一、项目背景与意义随着信息技术的快速发展,企业和校园中的打印服务已成为日常办公和学习中不可或缺的一部分......
  • leetcode 1486. 数组异或操作 https://leetcode.cn/problems/xor-operation-in-an-arr
    1486.数组异或操作题目描述给你两个整数,n和start。数组nums定义为:nums[i]=start+2*i(下标从0开始)且n==nums.length。请返回nums中所有元素按位异或(XOR)后得到的结果。示例示例1:输入:n=5,start=0输出:8解释:数组nums为[0,2,4,6,8],其中(0^......
  • DockerCompose中使用自定义网络的方式实现部署SpringBoot+Mysql+Redis
    场景Docker中Docker网络-理解Docker0与自定义网络的使用示例:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/140788458Docker中使用自定义网络方式实现Redis集群部署与测试流程:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/140797109上面介绍了D......
  • springboot实战
    pom<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org......
  • 基于springboot+vue开发的垃圾分类识别系统
    背景随着社会的快速发展,计算机的影响是全面且深入的。日常生活中,“垃圾”无处不在,家庭公寓里的垃圾桶、街头巷尾的垃圾箱、城市郊区的垃圾场、校园的垃圾站点等等,你也常常会发现,垃圾处理时通常有着多种分类,随着垃圾分类的普及,用户的数量和管理员的工作量在不断增加,工作也更......