Stream closed、 在 HttpServletRequest 中,输入流 (InputStream) 只能被读取一次、stream流重复消费。

bug记录:

在日志中记录请求参数时,从request中获取参数报错。 StreamUtils.copyToString(request.getInputStream(), Charset.forName(chartEncoding))

public void setRequestParamValue(SysLogEntity entity, boolean recordParam, HttpServletRequest request) {
    String chartEncoding = StringUtils.isBlank(request.getCharacterEncoding()) ?
            StandardCharsets.UTF_8.toString() : request.getCharacterEncoding();
    try {
        if (recordParam) {
            if (isRestful(request)) {
                String s = StreamUtils.copyToString(request.getInputStream(), Charset.forName(chartEncoding));
                entity.setRequestParam(s);
            } else {
                entity.setRequestParam(getRequestParams(request));
            }
        }
    } catch (Exception e) {
        log.error("记录用户请求体信息出错了", e);
    }
}

报错信息如下:stream流重复消费了。

java.io.IOException: Stream closed
	at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:359)
	at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132)
	at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.Reader.read(Reader.java:140)

检查后流被消耗的地方: @RequestBody

@ApiOperation("保存")
@PostMapping("/save")
@AopLog(
    type = SysLogEnum.OPERATE,
    module = SysLogEnum.Module.PROCESS_FRAMEWORK,
    event = SysLogEnum.Event.ADD,
    describe = "流程体系框架-添加",
    recordParam = true)
public ResultWrapper<Long> save(@Validated @RequestBody ProcessFrameworkParam param) {
  ProcessFramework model = processFrameworkService.insert(param);
  return new ResultWrapper<>(ResultEnum.SUCCESS, model.getId());
}

感觉写的不错的内容记录下来

出现 "Stream closed" 错误通常是因为你尝试读取一个已经被关闭或已经读取过的流。在 HTTP 请求的上下文中,这个问题通常出现在尝试读取 HttpServletRequest 的输入流超过一次时。

HttpServletRequest 中,输入流 (InputStream) 只能被读取一次,因为流的数据在读取后就不再可用。这是一个常见问题,特别是在需要对请求体进行多次读取或分析的情况下。

总结

什么情况下HttpServletRequest 输入流会被消耗:

在处理 HttpServletRequest 时,流数据会在以下情况下被消耗:

  1. 读取请求体
    当你从 HttpServletRequest 读取输入流(InputStream)或读取器(Reader)来获取请求的内容时,流会被消耗。这通常发生在处理 POST 或 PUT 请求的情况下,这些请求类型常用于传输数据。

    例如,使用 request.getInputStream()request.getReader() 会读取并消耗流数据。

  2. 框架级别的自动读取
    在某些情况下,如果你使用的是像 Spring MVC 这样的框架,框架本身可能会在处理请求时自动读取输入流。例如,当你在控制器方法中使用 @RequestBody 注解时,Spring 会自动读取请求体,并将其转换为相应的对象,这同样会消耗流。

  3. 某些类型的过滤器或拦截器
    如果你的应用程序中有过滤器(Filter)或拦截器(Interceptor),它们可能会读取请求体来进行某些操作(如日志记录、请求验证等),这也会消耗输入流。

重要提示

  • 输入流只能被读取一次:一旦流数据被读取,它就不能被再次读取,除非你采取特定措施来缓存这些数据。
  • 多次读取需求:如果你需要在你的应用程序中的不同部分多次读取输入流,你需要在第一次读取时将其内容保存下来,或者使用诸如 Spring 的 ContentCachingRequestWrapper 这样的工具来缓存请求体。
  • 安全性和性能:在处理请求体时,始终考虑到安全性和性能。例如,对于非常大的请求体,缓存数据可能会导致内存问题。

解决方案:

我的解决方案:

我的切面里面可以获取到请求参数

ProceedingJoinPoint point

point.getArgs() 从这里获取请求参数,曲线救国。

fillRequestValues(entity, annotation.recordParam(), request, point.getArgs());

其他解决方案:

  1. 保存请求体的副本:
    在第一次读取流之后,将数据保存在一个变量中,这样就可以在之后需要的时候多次使用它。这个解决方案适用于请求体相对较小的情况。

    String requestBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
    // 使用 requestBody 变量,而不是再次读取输入流
    
    
  2. 使用 Filter 包装 HttpServletRequest:
    如果你需要在一个过滤器(Filter)或拦截器(Interceptor)中读取和修改请求体,可以创建一个包装了原始 HttpServletRequest 的自定义请求对象,并在其中保存请求体的副本。这允许你在应用中的其他部分多次访问请求体。

  3. 特定于框架的解决方案:
    如果你使用的是如 Spring 等框架,可能有更专门的解决方案来处理这种情况。例如,Spring 提供了 ContentCachingRequestWrapper 来缓存请求体,使其可以被多次读取。

注意点

  • 内存管理:对于大型请求体,需要注意内存消耗。保存大型请求体的副本可能会导致内存问题。
  • 性能考虑:复制和保存请求体可能会影响应用程序的性能,特别是在高负载条件下。
  • 安全考虑:确保对任何来自请求的数据进行适当的安全检查,尤其是在涉及到潜在的注入攻击时。

提示:

request 怎么查看是否被消耗了:

检查 HttpServletRequest 的流数据是否已经被消耗通常不是一件简单的事情,因为 Servlet API 本身并不提供直接的方法来检测这一点。不过,你可以通过以下方法来尝试判断:

  1. 尝试读取流
    • 试图从请求中读取数据,例如使用 request.getInputStream()request.getReader()。如果流已被消耗,这些方法可能会抛出异常或返回空内容。
    • 需要注意的是,这种方法本身可能会改变请求对象的状态,因此应谨慎使用。
  2. 利用包装类
    • 在过滤器中,使用类似 ContentCachingRequestWrapper 的包装类对请求进行包装。这类包装器允许多次读取输入流,而不会抛出已消耗流的异常。
    • 通过判断是否能从包装类中成功读取数据,可以间接推断原始请求的流是否被消耗。
  3. 检查框架行为
    • 如果你使用的是诸如 Spring MVC 这样的框架,了解框架的内部行为也是很重要的。例如,了解何时何地框架会自动读取流数据。
    • 检查代码中是否有可能导致请求流被读取的地方,如带有 @RequestBody 注解的控制器方法。
  4. 日志和调试
    • 在开发过程中,使用日志记录在处理请求的各个环节中流的状态。
    • 使用调试工具来跟踪请求对象的状态变化。

注意事项

  • 不要重复读取流:试图重复读取流可能导致程序出现异常或得到错误的数据。
  • 异常处理:在尝试读取流数据时,应妥善处理可能出现的异常,以避免程序因异常而中断。
  • 对性能的影响:频繁地尝试读取请求流可能会影响应用程序的性能,尤其是在高流量的生产环境中。
posted @ 2023-10-27 16:42  dlage  阅读(700)  评论(0编辑  收藏  举报