request.reader.readText()が空？ContentCachingRequestWrapperで解決！

`request.reader.readText()` が空になるのは、`@RequestBody` アノテーションによって既にリクエストボディが消費されているからです。Springでは、`@RequestBody` を利用するとリクエストボディが自動的にデシリアライズされ、`HttpServletRequest` のストリームが消費済み状態になります。

リクエストボディを複数回利用したい場合、以下のようなアプローチを取ることができます。

---

### **解決方法 1: `@RequestBody` とキャッシュ済みリクエストボディを利用**

Springの `ContentCachingRequestWrapper` を利用して、リクエストボディをキャッシュしつつ `@RequestBody` を使います。

#### **コード例**

- **コントローラ**

```kotlin
@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` を削除し、リクエストボディの読み取りとデシリアライズを手動で行います。

#### **コード例**

```kotlin
@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` クラス**

```kotlin
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` を登録**

```kotlin
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 に登録**

```kotlin
@Configuration
class FilterConfig {
    @Bean
    fun requestCachingFilter(): FilterRegistrationBean<RequestCachingFilter> {
        val registrationBean = FilterRegistrationBean(RequestCachingFilter())
        registrationBean.addUrlPatterns("/api/login/*")
        return registrationBean
    }
}
```