request.reader.readText()
が空になるのは、@RequestBody
アノテーションによって既にリクエストボディが消費されているからです。Springでは、@RequestBody
を利用するとリクエストボディが自動的にデシリアライズされ、HttpServletRequest
のストリームが消費済み状態になります。
リクエストボディを複数回利用したい場合、以下のようなアプローチを取ることができます。
解決方法 1: @RequestBody
とキャッシュ済みリクエストボディを利用
Springの ContentCachingRequestWrapper
を利用して、リクエストボディをキャッシュしつつ @RequestBody
を使います。
コード例
- コントローラ
@PostMapping("/api/login/")
fun login(
@RequestBody loginForm: LoginForm,
request: HttpServletRequest
): ResponseEntity<Any> {
// リクエストボディ
val wrapper = request as ContentCachingRequestWrapper
val requestBody = String(wrapper.contentAsByteArray, Charsets.UTF_8)
return ResponseEntity.ok().build()
}
解決方法 2: @RequestBody
と HttpServletRequest
を同時に利用しない
@RequestBody
を削除し、リクエストボディの読み取りとデシリアライズを手動で行います。
コード例
@PostMapping("/api/login/")
fun login(
request: HttpServletRequest
): ResponseEntity<Any> {
// リクエストボディ
val requestBody = request.reader.readText()
// Formのマッピング
val mapper = ObjectMapper()
val loginForm = mapper.readValue(requestBody, LoginForm::class.java)
return ResponseEntity.ok().build()
}
解決方法 3: カスタム Filter
を利用する
リクエストボディをキャッシュする HttpServletRequestWrapper
を利用します。この方法はリクエストボディを複数回利用できるようにする汎用的な解決策です。
コード例
CachedBodyHttpServletRequest
クラス
import java.io.*
import javax.servlet.*
import javax.servlet.http.*
class CachedBodyHttpServletRequest(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
private val cachedBody: ByteArray
init {
cachedBody = request.inputStream.readBytes()
}
override fun getInputStream(): ServletInputStream {
return CachedServletInputStream(cachedBody)
}
override fun getReader(): BufferedReader {
return cachedBody.inputStream().bufferedReader(Charsets.UTF_8)
}
private class CachedServletInputStream(private val cachedBody: ByteArray) : ServletInputStream() {
private val inputStream = ByteArrayInputStream(cachedBody)
override fun read(): Int = inputStream.read()
override fun isFinished(): Boolean = inputStream.available() == 0
override fun isReady(): Boolean = true
override fun setReadListener(readListener: ReadListener?) {}
}
}
Filter
を登録
import javax.servlet.*
import javax.servlet.http.*
class RequestCachingFilter : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
if (request is HttpServletRequest) {
val cachedRequest = CachedBodyHttpServletRequest(request)
chain.doFilter(cachedRequest, response)
} else {
chain.doFilter(request, response)
}
}
}
- フィルターを Spring に登録
@Configuration
class FilterConfig {
@Bean
fun requestCachingFilter(): FilterRegistrationBean<RequestCachingFilter> {
val registrationBean = FilterRegistrationBean(RequestCachingFilter())
registrationBean.addUrlPatterns("/api/login/*")
return registrationBean
}
}