2026年7月30日

开发小记 解决使用suspend函数的Controller无法使用ContentCachingResponseWrapper获取响应信息&发送的响应为空的问题

作者 TheWhiteDog9487

观前提示:这个问题目前已知会且仅会在Controller函数为Kotlin的suspend可挂起函数内部使用了其他可挂起函数时出现,如果你使用的是Java那可以直接划走了o(* ̄▽ ̄*)ブ

先说下复现环境

	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
	implementation("org.springframework.boot:spring-boot-starter-webmvc")

依赖只需要这三个就够了,版本号也不用填,Spring的BOM会自动填充

为了阅读流畅些不会上下反复跳,我就把所有组件写在一起了哦

package com.example.demo

import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServletResponse
import kotlinx.coroutines.delay
import org.apache.catalina.webresources.CachedResource
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.util.ContentCachingResponseWrapper
import kotlin.time.Duration.Companion.seconds

@SpringBootApplication
class DemoApplication

@Component
class f: Filter{
	override fun doFilter(
		request: ServletRequest,
		response: ServletResponse,
		chain: FilterChain) {
		val CachedResponse = ContentCachingResponseWrapper(response as HttpServletResponse)
		chain.doFilter(request, CachedResponse)
		CachedResponse.copyBodyToResponse() } }

@RestController
class a{
	@GetMapping("/hello")
	suspend fun hello(): String {
		delay(1.seconds)
		return "Hello, World!" } }

fun main(args: Array<String>) {
	runApplication<DemoApplication>(*args) }

很标准的使用对吧,看起来没毛,最后也用copyBodyToResponse把响应内容写回去了。
现在我们来编译、运行、发个请求出去,神奇的一幕出现了

看响应数据那里,Content-Length竟然是0耶,我们发送的 Hello, World! 也失踪不知道上哪去了


经过了漫长且折磨的调试和查资料之后我找到了问题的关键
咱们加几个调试日志看看

注意日志的顺序

过滤开始
开始等待
过滤结束
响应内容:
等待结束

发现问题没有?按照正常流程来说应该先出现 等待结束 再出现 过滤结束,可是现在并不是这样的。

这也就是这个问题的根本原因,当可挂起Controller内部把控制权交出去之后会立刻返回到chain.doFilter,而不是我们所期望的Controller内部return之后再返回

所以…怎么解决呢?

package com.example.demo

import jakarta.servlet.AsyncEvent
import jakarta.servlet.AsyncListener
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServletResponse
import kotlinx.coroutines.delay
import org.apache.catalina.webresources.CachedResource
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.util.ContentCachingResponseWrapper
import kotlin.time.Duration.Companion.seconds

@SpringBootApplication
class DemoApplication

@Component
class f: Filter{
	override fun doFilter(
		request: ServletRequest,
		response: ServletResponse,
		chain: FilterChain) {
		val CachedResponse = ContentCachingResponseWrapper(response as HttpServletResponse)
		println("过滤开始")
		chain.doFilter(request, CachedResponse)
		if (request.isAsyncStarted) {
			request.asyncContext.addListener(object : AsyncListener {
				override fun onComplete(event: AsyncEvent?) {
					println("异步完成")
					println("响应内容:${String(CachedResponse.contentAsByteArray)}")
					CachedResponse.copyBodyToResponse() }

				override fun onTimeout(event: AsyncEvent?) { }
				override fun onError(event: AsyncEvent?) = onTimeout(event)
				override fun onStartAsync(event: AsyncEvent?) { } } ) }
		println("过滤结束") } }

@RestController
class a{
	@GetMapping("/hello")
	suspend fun hello(): String {
		println("开始等待")
		delay(1.seconds)
		println("等待结束")
		return "Hello, World!" } }

fun main(args: Array<String>) {
	runApplication<DemoApplication>(*args) }

你看,这下就对了。


总结一下,我们需要:

  1. chain.doFilter之后判断request.isAsyncStarted属性
  2. 如果isAsyncStartedtrue,使用request.asyncContext.addListener函数注册一个监听器,具体按照你的习惯,我这里直接使用object原地做了一个接口实现出来
  3. 实现内部的onStartAsync用不到,onErroronTimeout看你的具体情况,最重要的是onComplete
  4. onComplete被触发,说明Controller已经使用return退出,这个时候再去读取ContentCachingResponseWrapper就是安全的了
  5. 最后别忘了使用copyBodyToResponse函数把响应数据写回原始对象