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
时,流数据会在以下情况下被消耗:
-
读取请求体:
当你从HttpServletRequest
读取输入流(InputStream
)或读取器(Reader
)来获取请求的内容时,流会被消耗。这通常发生在处理 POST 或 PUT 请求的情况下,这些请求类型常用于传输数据。例如,使用
request.getInputStream()
或request.getReader()
会读取并消耗流数据。 -
框架级别的自动读取:
在某些情况下,如果你使用的是像 Spring MVC 这样的框架,框架本身可能会在处理请求时自动读取输入流。例如,当你在控制器方法中使用@RequestBody
注解时,Spring 会自动读取请求体,并将其转换为相应的对象,这同样会消耗流。 -
某些类型的过滤器或拦截器:
如果你的应用程序中有过滤器(Filter)或拦截器(Interceptor),它们可能会读取请求体来进行某些操作(如日志记录、请求验证等),这也会消耗输入流。
重要提示
- 输入流只能被读取一次:一旦流数据被读取,它就不能被再次读取,除非你采取特定措施来缓存这些数据。
- 多次读取需求:如果你需要在你的应用程序中的不同部分多次读取输入流,你需要在第一次读取时将其内容保存下来,或者使用诸如 Spring 的
ContentCachingRequestWrapper
这样的工具来缓存请求体。 - 安全性和性能:在处理请求体时,始终考虑到安全性和性能。例如,对于非常大的请求体,缓存数据可能会导致内存问题。
解决方案:
我的解决方案:
我的切面里面可以获取到请求参数
ProceedingJoinPoint point
point.getArgs()
从这里获取请求参数,曲线救国。
fillRequestValues(entity, annotation.recordParam(), request, point.getArgs());
其他解决方案:
-
保存请求体的副本:
在第一次读取流之后,将数据保存在一个变量中,这样就可以在之后需要的时候多次使用它。这个解决方案适用于请求体相对较小的情况。String requestBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // 使用 requestBody 变量,而不是再次读取输入流
-
使用
Filter
包装HttpServletRequest
:
如果你需要在一个过滤器(Filter)或拦截器(Interceptor)中读取和修改请求体,可以创建一个包装了原始HttpServletRequest
的自定义请求对象,并在其中保存请求体的副本。这允许你在应用中的其他部分多次访问请求体。 -
特定于框架的解决方案:
如果你使用的是如Spring
等框架,可能有更专门的解决方案来处理这种情况。例如,Spring
提供了ContentCachingRequestWrapper
来缓存请求体,使其可以被多次读取。
注意点
- 内存管理:对于大型请求体,需要注意内存消耗。保存大型请求体的副本可能会导致内存问题。
- 性能考虑:复制和保存请求体可能会影响应用程序的性能,特别是在高负载条件下。
- 安全考虑:确保对任何来自请求的数据进行适当的安全检查,尤其是在涉及到潜在的注入攻击时。
提示:
request 怎么查看是否被消耗了:
检查 HttpServletRequest
的流数据是否已经被消耗通常不是一件简单的事情,因为 Servlet API 本身并不提供直接的方法来检测这一点。不过,你可以通过以下方法来尝试判断:
- 尝试读取流:
- 试图从请求中读取数据,例如使用
request.getInputStream()
或request.getReader()
。如果流已被消耗,这些方法可能会抛出异常或返回空内容。 - 需要注意的是,这种方法本身可能会改变请求对象的状态,因此应谨慎使用。
- 试图从请求中读取数据,例如使用
- 利用包装类:
- 在过滤器中,使用类似
ContentCachingRequestWrapper
的包装类对请求进行包装。这类包装器允许多次读取输入流,而不会抛出已消耗流的异常。 - 通过判断是否能从包装类中成功读取数据,可以间接推断原始请求的流是否被消耗。
- 在过滤器中,使用类似
- 检查框架行为:
- 如果你使用的是诸如 Spring MVC 这样的框架,了解框架的内部行为也是很重要的。例如,了解何时何地框架会自动读取流数据。
- 检查代码中是否有可能导致请求流被读取的地方,如带有
@RequestBody
注解的控制器方法。
- 日志和调试:
- 在开发过程中,使用日志记录在处理请求的各个环节中流的状态。
- 使用调试工具来跟踪请求对象的状态变化。
注意事项
- 不要重复读取流:试图重复读取流可能导致程序出现异常或得到错误的数据。
- 异常处理:在尝试读取流数据时,应妥善处理可能出现的异常,以避免程序因异常而中断。
- 对性能的影响:频繁地尝试读取请求流可能会影响应用程序的性能,尤其是在高流量的生产环境中。