You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:30:18