问题描述

在基于Spring开发Java项目时,可能需要重复读取HTTP请求体中的数据,例如使用拦截器打印入参信息等,但当我们重复调用getInputStream()或者getReader()时,通常会遇到类似以下的错误信息:



大体的意思是当前request的getInputStream()已经被调用过了。那为什么会出现这个问题呢?

原因分析

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

解决方案

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

涉及到变量的修改,我们首先想到的就是有没有提供方法进行修改,不过可惜的是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注解修饰的参数无法正常读取请求体的问题,也就限定了它的使用范围如下图所示:

如果仅需要在业务代码后再次读取请求体内容,那么使用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项目脚手架,我会不定期加入一些通用功能,欢迎关注。

SpringBoot项目中HTTP请求体只能读一次?试试这方案的更多相关文章

  1. 五分钟后,你将学会在SpringBoot项目中如何集成CAT调用链

    买买买结算系统 一年一度的双十一购物狂欢节就要到了,又到剁手党们开始表演的时刻了.当我们把种草很久的商品放入购物车以后,点击"结算"按钮时,就来到了买买买必不可少的结算页面了.让我 ...

  2. 在SpringBoot项目中添加logback的MDC

    在SpringBoot项目中添加logback的MDC     先看下MDC是什么 Mapped Diagnostic Context,用于打LOG时跟踪一个“会话“.一个”事务“.举例,有一个web ...

  3. SpringBoot项目中遇到的BUG

    1.启动项目的时候报错 1.Error starting ApplicationContext. To display the auto-configuration report re-run you ...

  4. SpringBoot项目中,表单的验证操作

    在创建Springboot项目中,我们使用了表单验证操作,这一操作将极大地简化我们编程的开发 1.接收数据,以及验证 @PostMapping("/save") public Mo ...

  5. Spring-Boot项目中配置redis注解缓存

    Spring-Boot项目中配置redis注解缓存 在pom中添加redis缓存支持依赖 <dependency> <groupId>org.springframework.b ...

  6. SpringBoot 项目中配置多个 Jackson 的 ObjectMapper ,以及配置遇到的坑

    目录 问题说明 原因排查分析 结论总结 Jackson 自动装配分析 问题说明 我们都知道,SpringBoot 项目中,如果引入了 Jackson 的包,哪怕不配置,SpringBoot 也会帮我们 ...

  7. SpringBoot12 QueryDSL01之QueryDSL介绍、springBoot项目中集成QueryDSL

    1 QueryDSL介绍 1.1 背景 QueryDSL的诞生解决了HQL查询类型安全方面的缺陷:HQL查询的扩展需要用字符串拼接的方式进行,这往往会导致代码的阅读困难:通过字符串对域类型和属性的不安 ...

  8. 自身使用的springboot项目中比较全的pom.xml

    在学习的时候常建新的项目,mark下商用的jar <dependency> <groupId>org.mybatis</groupId> <artifactI ...

  9. springboot 项目中获取默认注入的序列化对象 ObjectMapper

    在 springboot 项目中使用 @SpringBootApplication 会自动标记 @EnableAutoConfiguration 在接口中经常需要使用时间类型,Date ,如果想要格式 ...

  10. springboot项目中js、css静态文件路径访问

    springboot静态文件访问的问题,相信大家也有遇到这个问题,如下图项目结构. 项目结构如上所示,静态页面引入js.css如下所示. 大家肯定都是这样写的,但是运行的话就是出不来效果,图片也不显示 ...

随机推荐

  1. window10设置保护眼睛的颜色

    1.调出运行菜单.右击开始键选择运行,或者同时按下键盘上的WIN+R打开运行框,输入 regedit 回车转到注册表编辑器.2.选择第二项 HKEY_CURRENT_USER 点击进入.进入后点击 C ...

  2. POJ2247,hdu1058(Humble Numbers)

    Problem Description A number whose only prime factors are 2,3,5 or 7 is called a humble number. The ...

  3. k8s集群搭建及对一些组件的简单理解(二)

    背景 前面写了一篇,k8s集群搭建及对一些组件的简单理解(一),主要讲了下背景和对一些组件的理解. 今天讲一下正式的安装,有网环境的,后续再说下无外网环境纯内网的. k8s集群节点.组件 控制面节点, ...

  4. 配置h5py、netCDF4库的方法:Anaconda环境

      本文介绍基于Anaconda环境,下载并安装Python中h5py与netCDF4这两个模块的方法.   在Python语言中,h5py与netCDF4这两个模块是与遥感图像处理.地学分析等GIS ...

  5. 使用Python爬取公众号的合集内容

    使用Python爬取公众号的合集 前言 ...最近老是更新关于博客的文章,很久没更新其他的了,然后写一下如何爬取微信公众号里面的图片吧! 先看看微信公众号的样子吧: 我爬取的是公众号的合集内容 讲解 ...

  6. Android 7 修改启动动画和开机声音

    背景 在修改开机音量的时候,发现找不到对应的声音功能调用. 因此了解了一下安卓的开机声音是如何实现的. 安卓4~安卓7 都可以这么做. 参考: https://blog.csdn.net/chen82 ...

  7. Elastic-Search 整理(二):高级篇

    ES高级篇 集群部署 集群的意思:就是将多个节点归为一体罢了,这个整体就有一个指定的名字了 window中部署集群 - 了解 把下载好的window版的ES中的data文件夹.logs文件夹下的所有的 ...

  8. 图论最短路径问题与matlab实现

    上一次我们讨论了如何进行图论可视化,这一次我们通过matlab来找出图论中距离最小路径 目录 一.迪杰斯特拉算法(Dijkstra) 二.shortestpath函数用法 1.基本语法 2.参数设计 ...

  9. RAG工程实践拦路虎之一:PDF格式解析杂谈

    背景 PDF(Portable Document Format)是一种广泛用于文档交换的文件格式,由Adobe Systems开发.它具有跨平台性.固定布局和易于打印等特点,因此在商业.学术和个人领域 ...

  10. vue中sass与SCSS的区别

    在Vue中,通常使用SCSS(Sassy CSS)而不是Sass来编写样式.SCSS是Sass的一种语法扩展,提供了更多的功能和灵活性,因此在Vue项目中更常见.下面是Sass和SCSS之间的主要区别 ...