https://www.cnblogs.com/Sinte-Beuve/p/13260249.html

前言

在某些业务中可能会需要多次读取 HTTP 请求中的参数,比如说前置的 API 签名校验。这个时候我们可能会在拦截器或者过滤器中实现这个逻辑,但是尝试之后就会发现,如果在拦截器中通过 getInputStream() 读取过参数后,在 Controller 中就无法重复读取了,会抛出以下几种异常:

HttpMessageNotReadableException: Required request body is missing
IllegalStateException: getInputStream() can't be called after getReader()

这个时候需要我们将请求的数据缓存起来。本文会从 ServletRequest 数据封装原理开始详细讲讲如何解决这个问题。如果不想看原理的,可直接阅读 最佳解决方案。

ServletRequest 数据封装原理

平时我们接受 HTTP 请求的参数时,基本是通过 SpringMVC 的包装。

  • POST form-data 参数时,直接用实体类,或者直接在 Controller 的方法上把参数填上就可以了,手动则可以通过 request.getParameter() 来获取。
  • POST json 时,会在实体类上添加 @RequestBody 参数或者直接调用 request.getInputStream() 获取流数据。

我们可以发现在获取不同数据格式的数据时调用的方法是不同的,但是阅读源码可以发现,其实底层他们的数据来源都是一样的,只是 SpringMVC 帮我们做了一下处理。下面我们就来讲讲 ServletRequest 数据封装的原理。

实际上我们通过 HTTP 传输的参数都会存在 Request 对象的 InputStream 中,这个 Request 对象也就是 ServletRequest 最终的实现,是由 tomcat 提供的。然后针对于不同的数据格式,会在不同的时刻对 InputStream 中的数据进行封装。

Spring MVC 对不同类型数据的封装

  • GET 请求的数据一般是 Query String,直接在 url 的后面,不需要特殊处理

  • 通过例如 POST、PUT 发送 multipart/form-data 格式的数据

// 源码中适当去除无关代码
// 对于这类数据,SpringMVC 在 DispatchServlet 的 doDispatch() 方法中就会进行处理。具体处理流程如下:
// org.springframework.web.servlet.DispatcherServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// Determine handler for the current request.// other code...
}
// 1. 调用 checkMultipart(request),当前请求的数据类型是否为 multipart/form-data
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {return this.multipartResolver.resolveMultipart(request);}return request;
}
//2. 如果是,调用 multipartResolver 的 resolveMultipart(request),返回一个 StandardMultipartHttpServletRequest 对象。
// org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.java
public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {this(request, false);
}
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {super(request);if (!lazyParsing) {parseRequest(request);}
}
// 3. 在构造 StandardMultipartHttpServletRequest 对象时,会调用 parseRequest(request),将 InputStream 中是数据流进行进一步的封装。
// 不贴源码了,主要是对 form-data 数据的封装,包含字段和文件。
  • 通过例如 POST、PUT 发送 application/x-www-form-urlencoded 格式的数据
// 非 form-data 的数据,会存储在 HttpServletRequest 的 InputStream 中。
// 在第一次调用 getParameterNames() 或 getParameter() 时,
// 会调用 parseParameters() 方法对参数进行封装,从 InputStream 中读取数据,并封装到 Map 中。//org.apache.catalina.connector.Request.java
public String getParameter(String name) {if (!this.parametersParsed) {this.parseParameters();}return this.coyoteRequest.getParameters().getParameter(name);
}
  • 通过例如 POST、PUT 发送 application/json 格式的数据
// 数据会直接会存储在 HttpServletRequest 的 InputStream 中,通过 request.getInputStream() 或 getReader() 获取。

读取参数时出现的问题

现在我们基本已经对 SpringMVC 是如何封装 HTTP 请求参数有了一定的认识。根据之前描述的,我们如果要在拦截器中和 Controller 中重复读取参数时,会出现以下异常:

HttpMessageNotReadableException: Required request body is missing
IllegalStateException: getInputStream() can't be called after getReader()

这是由于 InputStream 这个流数据的特殊性,在 Java 中读取 InputStream 数据时,内部是通过一个指针的移动来读取一个一个的字节数据的,当读完一遍后,这个指针并不会 reset,因此第二遍读的时候就会出现问题了。而之前讲了,HTTP 请求的参数也是封装在 Request 对象中的 InputStream 里,所以当第二次调用 getInputStream() 时会抛出上述异常。具体的问题可以细分成多种情况:

  1. 请求方式为 multipart/form-data,在拦截器中手动调用 request.getInputStream()
// 上文讲了在 doDispatch() 时就会进行处理,因此这里会取不到值
log.info("input stream content: {}", new String(StreamUtils.copyToByteArray(request.getInputStream())));
  1. 请求方式为 application/x-www-form-urlencoded,在拦截器中手动调用 request.getInputStream()
// 第 1 次可以取到值
log.info("input stream content: {}", new String(StreamUtils.copyToByteArray(request.getInputStream())));
// 第一次执行 getParameter() 会调用 parseParameters(),parseParameters 进一步调用 getInputStream()
// 这里就取不到值了
log.info("form-data param: {}", request.getParameter("a"));
log.info("form-data param: {}", request.getParameter("b"));
  1. 请求方式为 application/json,在拦截器中手动调用 request.getInputStream()
// 第 1 次可以取到值
log.info("input stream content: {}", new String(StreamUtils.copyToByteArray(request.getInputStream())));
// 之后再任何地方再调用 getInputStream() 都取不到值,会抛出异常

为了能够多次获取到 HTTP 请求的参数,我们需要将 InputStream 流中的数据缓存起来。

最佳解决方案

通过查阅资料,实际上 springframework 自己就有相应的 wrapper 来解决这个问题,在 org.springframework.web.util 包下有一个 ContentCachingRequestWrapper 的类。这个类的作用就是将 InputStream 缓存到 ByteArrayOutputStream 中,通过调用 ``getContentAsByteArray()` 实现流数据的可重复读取。

/*** {@link javax.servlet.http.HttpServletRequest} wrapper that caches all content read from* the {@linkplain #getInputStream() input stream} and {@linkplain #getReader() reader},* and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.* @see ContentCachingResponseWrapper*/

在使用上,只需要添加一个 Filter,将 HttpServletRequest 包装成 ContentCachingResponseWrapper 返回给拦截器和 Controller 就可以了。

@Slf4j
@WebFilter(urlPatterns = "/*")
public class CachingContentFilter implements Filter {private static final String FORM_CONTENT_TYPE = "multipart/form-data";@Overridepublic void init(FilterConfig filterConfig) {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {String contentType = request.getContentType();if (request instanceof HttpServletRequest) {HttpServletRequest requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);// #1if (contentType != null && contentType.contains(FORM_CONTENT_TYPE)) {chain.doFilter(request, response);} else {chain.doFilter(requestWrapper, response);}return;}chain.doFilter(request, response);}@Overridepublic void destroy() {}
}// 添加扫描 filter 注解
@ServletComponentScan
@SpringBootApplication
public class SeedApplication {public static void main(String[] args) {SpringApplication.run(SeedApplication.class, args);}
}

在拦截器中,获取请求参数:

// 流数据获取,比如 json
// #2
String jsonBody = IOUtils.toString(wrapper.getContentAsByteArray(), "utf-8");
// form-data 和 urlencoded 数据
String paramA = request.getParameter("paramA");
Map<String,String[]> params = request.getParameterMap();

tips:

  1. 这里需要根据 contentType 做一下区分,遇到 multipart/form-data 数据时,不需要 wrapper,会直接通过 MultipartResolver 将参数封装成 Map,当然这也可以灵活的在拦截器中判断。
  2. wrapper 在具体使用中,我们可以使用 getContentAsByteArray() 来获取数据,并通过 IOUtils 转换成 String。尽量不使用 request.getInputStream()。因为虽然经过了包装,但是 InputStream 仍然只能读一次,而参数进入 Controller 的方法前 HttpMessageConverter 的参数转换需要调用这个方法,所以把它保留就可以了。

总结

遇到这个问题的时候也参考了很多博客,有的使用了 ContentCachingRequestWrapper,也有的自己实现了一个 Wrapper。但是自己实现 Wrapper 的方案,多半是直接在 Wrapper 的构造函数中读取流数据到 byte[] 数据中去,这样在遇到 multipart/form-data 这种数据类型的时候就会出现问题了,因为包装在调用 MultipartResolver 之前执行,再次调用的时候就读不到数据了。

所以博主又自己研究了一下 Spring 的源码,实现了这种方案,基本上可以处理多种通用的数据类型了。

解决HttpServletRequest 流数据不可重复读相关推荐

  1. 解决 HttpServletRequest 流数据不可重复读

    背景介绍 甲方客户的生产系统,有安全风险预警和安全事件快速溯源要求,需要做一套日志管理规范. 要求我们接入的系统,要对用户登录.注册.密码修改等重要场景,严格按照提供的格式,输出相应的日志. 后续通过 ...

  2. 数据库的四种隔离级别及对应解决的脏读、不可重复读、幻读问题

    数据库的四种隔离级别 数据库事务的隔离级别有4种,由低到高分别为Read uncommitted .Read committed .Repeatable read .Serializable .而且, ...

  3. mysql 快照读 幻读,InnoDB的MVCC如何解决不可重复读和快照读的幻读,当前读用next-key解决幻读...

    InnoDB默认的隔离级别是RR(可重复读),可以解决脏读和不可重复读,只解决了快照读情况下的幻读问题,当前读情况下解决幻读问题得靠next-key锁. mysql如何实现避免幻读: 在快照读读情况下 ...

  4. mysql中mvcc解决不可重复读

    最近在了解了mysql中事务的隔离级别,记录一下 事务的隔离级别: 隔离级别 脏读 不可重复读 幻读 读未提交 read-uncommitted 是 是 是 读已提交 read-committed 否 ...

  5. MySQL MVCC多版本并发控制(脏读和不可重复读解决原理)

    文章目录 一.MVCC概念 二.MVCC应用于已提交读隔离级别 1. 解决脏读 2. 无法解决不可重复读 3. 无法解决幻读 三.MVCC应用于可重复读隔离级别 1. 解决脏读 2. 解决不可重复读 ...

  6. MySQL 可重复读隔离级别,完全解决幻读了吗?

    我在上一篇文章中提到,MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种: 针对快照读(普通 select 语句),是通 ...

  7. day08 操作索引脏读、幻读、不可重复读 python复习

    day08 操作索引脏读.幻读.不可重复读 python复习 昨日内容复习 视图 把sql语句查询结果保存下来,结果就叫视图视图的数据来自于原始表,在硬盘中只有表结构 create view 视图名称 ...

  8. 事务隔离级别——未提交读、已提交读、可重复读、串行

    事务隔离级别--未提交读.已提交读.可重复读.串行 事务隔离级别是指多个事务之间,不同事务中涉及的读写操作互相影响的隔离.其中多个事务中同时对同一条数据或者表进行写操作(insert.update.d ...

  9. MySQL理论:脏读、不可重复读、幻读

    文章目录 1. 脏读(dirty read) 脏读是指事务读取到其他事务未提交的数据 2. 不可重复读(non-repeatable read) 不可重复读是指在同一次事务中前后查询不一致的问题 3. ...

最新文章

  1. 2w字长文,让你瞬间拥有「调用链」开发经验
  2. 车牌识别--Towards End-to-End License Plate Detection and Recognition: A Large Dataset and Baseline
  3. 从体验上拉开差距,Serverless 将成就云计算的下一个 10 年!
  4. 第五章 Response(JavaTM Servlet 规范3.1 )
  5. Linux内核0.12完全注释
  6. 聚焦数字化智慧安防的新型社区
  7. 数字时钟html5 js,html5 canvas js(数字时钟)实例代码
  8. wpp助手怎么连接服务器,aewpp.com
  9. linux下设置物联网卡apn,负控终端物联网卡APN参数修改步骤
  10. 黑客在数十个 WordPress 插件和主题中插入秘密后门,可发动供应链攻击
  11. 八大排序算法之希尔排序
  12. Java学生管理系统设计与实现 (超详细,含课程设计)
  13. linux打开caj文件,在Deepin、UOS、Linux下打开caj格式文件的软件
  14. 【shenyu网关学习】1.什么是 Apache ShenYu
  15. 图像处理 - ImageMagick 简单介绍与案例
  16. 盟军敢死队I:深入敌后--秘籍
  17. 2D卷积和3D卷积的区别及pytorch实现
  18. Android开发过程中的一些基本常识
  19. Univariate Distribution Relationships(单变量分布关系)
  20. 【院校信息】2021中国海洋大学计算机考研数据汇总

热门文章

  1. 从智能音箱发展,看智能家居的未来
  2. 对于VS2012的位图无法加载到资源视图“Bitmap”中的解决方案
  3. 滴滴的拼车功能怎么让大家用的更多
  4. python二级考试14问(关于真正考试的时候的相关问题)
  5. PAT甲级真题 1064 完全二叉搜索树
  6. PIL:处理图像的好模块
  7. 5G+物联网商业模式促使物联网卡迎来增长新风口
  8. 1014 科学计数法 (C++)
  9. 【系统架构设计师】第一章:操作系统(1.1.1---1.1.2)操作系统的分类和结构
  10. 第七周任务-各种星号图