読者です 読者をやめる 読者になる 読者になる

a fledgling

駆け出しが駆け出してみる

Spring BootでRequest Bodyをログに出力する

API serverをSpring Bootで作っていて、リクエストボディ(JSON文字列)をログに吐こうとしてちょっと悩んだのでメモ。

問題

Interceptorで実装しているロギング処理でリクエストボディを取得しようとすると以下の問題にぶつかった。

  1. HttpServletRequest#getReaderIllegalStateExceptionを投げる
  2. HttpServletRequest#getInputStream#readLineとかで読むと、Controllerでリクエストボディがないと言われる
  3. ServletInputStreamresetに対応していない

辛い。

解決策

ググってると、HttpServletRequestをラップして、リクエストボディを保持しておいて、そこから都度InputStreamを作ればええねん! ってのを見つけたので、実装。例によってKotlinで書いている。

private class MyServletInputStream(bytes: ByteArray) : ServletInputStream() {
    private val inputStream: ByteArrayInputStream = ByteArrayInputStream(bytes)
    
    override fun read(): Int = inputStream.read()
    
    override fun read(b: ByteArray?, off: Int, len: Int): Int {
        return inputStream.read(b, off, len)
    }

    override fun isReady(): Boolean = inputStream.available() != 0

    override fun setReadListener(listener: ReadListener?) {}

    override fun isFinished(): Boolean = inputStream.available() == 0
}

private class MyServletRequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
    private val bytes: ByteArray = ByteArrayOutputStream().use {
        request.inputStream.copyTo(it)
        it.toByteArray()
    }

    override fun getInputStream(): ServletInputStream {
        return MyServletInputStream(this.bytes)
    }
}

ServletInputStreamの実装クラスのread以外のメソッドは適当に書いた。

どう使うか

  1. Interceptorより前に処理が走るFilterで、HttpServletRequstMyServletRequestWrapperでラップして、FilterChain#doFilterの引数として渡す。
  2. Interceptor内で、HttpServletRequest#getInputStream#readLineとかで読んでログに吐く
    • このHttpServletRequestの実態はMyServletRequestWrapper

終わり