Spring Boot(Kotlin)中HttpServletRequest输入流已关闭问题求助
问题解答
1. 关于控制器绑定@RequestBody时读取输入流的说法是否正确?
正确。Spring MVC处理@RequestBody注解时,会通过对应的HttpMessageConverter(比如Jackson的MappingJackson2HttpMessageConverter)读取HttpServletRequest的输入流,将其反序列化为目标对象。而Servlet规范中,InputStream只能被读取一次,读取完成后流会被关闭,后续再读取就会抛出输入流已关闭的异常。
2. 解决方案:缓存请求体输入流
核心思路是通过包装HttpServletRequest,将请求体内容缓存到字节数组中,让输入流可以被多次读取。具体实现步骤如下:
步骤1:实现缓存请求体的Request包装类
class CachedBodyHttpServletRequest(request: HttpServletRequest) : HttpServletRequestWrapper(request) { private val cachedBody: ByteArray init { // 初始化时读取原始输入流并缓存到字节数组 request.inputStream.use { inputStream -> cachedBody = inputStream.readAllBytes() } } override fun getInputStream(): ServletInputStream { // 每次返回从缓存字节数组生成的新输入流 val byteArrayInputStream = ByteArrayInputStream(cachedBody) return object : ServletInputStream() { override fun read(): Int = byteArrayInputStream.read() override fun isFinished(): Boolean = byteArrayInputStream.available() == 0 override fun isReady(): Boolean = true override fun setReadListener(listener: ReadListener?) = Unit } } override fun getReader(): BufferedReader { // 对应getReader方法,返回基于缓存的Reader return BufferedReader(InputStreamReader(inputStream, characterEncoding)) } }
步骤2:修改JWT过滤器使用包装后的Request
修改JWTAuthenticationFilter的doFilter方法,先将原始Request包装为缓存版本,再用包装后的对象进行认证和传递:
class JWTAuthenticationFilter(private val tokenAuthenticationService: TokenAuthenticationService) : GenericFilterBean() { @Throws(IOException::class, ServletException::class) override fun doFilter(request: ServletRequest, response: ServletResponse, filterChain: FilterChain) { // 包装原始Request为缓存版本 val cachedRequest = CachedBodyHttpServletRequest(request as HttpServletRequest) val authentication = tokenAuthenticationService.getAuthentication(cachedRequest) SecurityContextHolder.getContext().authentication = authentication // 传递包装后的Request给后续过滤器/控制器 filterChain.doFilter(cachedRequest, response) } }
说明
- 缓存时机:在过滤器创建
CachedBodyHttpServletRequest实例时,就会读取原始输入流并将内容缓存到字节数组中,这就是首次读取流进行缓存的时机。 - 后续所有读取操作(包括过滤器中的
getAuthentication方法和控制器的@RequestBody绑定)都会使用缓存的字节数组生成新的输入流,彻底解决了流只能读一次的问题。
内容的提问来源于stack exchange,提问作者Cristi
相关产品推荐
相关产品推荐

