しおしお

IntelliJ IDEAのことなんかを書いてます

ServletInputStreamをラップして、リクエストのボディをログに出力してみる

お仕事で、リクエストボディのJSONの内容をログに出力したいんだと言われたので作ってみた。

実現方法

1. FilterでServletRequestをラップする
2. ServletRequestのラッパーでは、InputStreamのgetterでログ出力機能をもったServletInputStream実装を返す
3. ログ出力機能を持ったServletInputStream実装では、read要求の中で読み込んだデータをログに出力する。

Filterでボディの内容を全部ログに出力してから、ServletRequestラップして後続に処理を移譲するでも良いんだけど、ボディが全部到達するまでFilterで処理止まってしまうんで、read要求の中でログ出力するようにしている感じです。

WildFlyでの使う場合

WildFly9.0.2で試したところ、デフォルトの構成だとServletRequestをラップすると怒られるので、以下コマンドでラップしたRequestやResponseを許容するようにしてあげる必要があるみたいです。

$ ./jboss-cli.sh
[disconnected /] connect
[standalone@localhost:9990 /] cd subsystem=undertow/servlet-container=default
[standalone@localhost:9990 servlet-container=default] :write-attribute(name=allow-non-standard-wrappers, value=true)

ラッパーを許容しない設定の場合に発生する例外はこれ。

Exception handling request to /loggingfilter-1.0-SNAPSHOT/app: java.lang.IllegalArgumentException: UT010023: Request siosio.LoggingFilter$RequestWrapper@77ab9fb3 was not original or a wrapper
実装例
@WebFilter(urlPatterns = arrayOf("/app/*"))
class LoggingFilter : Filter {
  override fun destroy() {
  }

  override fun init(filterConfig: FilterConfig?) {
  }

  override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
    // ServletRequestをラップして後続のフィルタに処理を移譲
    chain.doFilter(RequestWrapper(request as HttpServletRequest), response)
  }

  class RequestWrapper(val request: HttpServletRequest) : HttpServletRequest by request {
    override fun getInputStream(): ServletInputStream? {
      // ログ出力機能付きのServletInputStreamを返す
      return LoggingInputStream(request.inputStream)
    }
  }

  class LoggingInputStream(val inputStream: ServletInputStream) : ServletInputStream() {

    companion object {
      val logger: Logger = Logger.getLogger(LoggingInputStream::class.java)
    }

    override fun isReady(): Boolean {
      return inputStream.isFinished
    }

    override fun isFinished(): Boolean {
      return inputStream.isFinished
    }

    override fun setReadListener(readListener: ReadListener?) {
      inputStream.setReadListener(readListener)
    }

    override fun read(b: ByteArray?): Int {
      return inputStream.read(b)
    }

    override fun read(b: ByteArray, off: Int, len: Int): Int {
      // 読み込んだ内容やバイト数をログに出力する
      val result = inputStream.read(b, off, len)
      logger.info("read http body. offset:$off, length:$result, data:${b.copyOf(result).toString("UTF-8")} ")
      return result
    }

    override fun readLine(b: ByteArray?, off: Int, len: Int): Int {
      return super.readLine(b, off, len)
    }

    override fun read(): Int {
      return inputStream.read()
    }
  }
}
動かした結果

こんな感じにログに出力されますよと。

16:59:52,263 INFO  [siosio.LoggingFilter$LoggingInputStream] (default task-4) read http body. offset:0, length:31, data:{   "name": "あいうえお" } 

おわり。