Springでリクエストの内容をログに出力しさらに文字化けも解消する

# はじめに
Springで入力パラメータなどのリクエストの内容をログに出力する方法は、CommonsRequestLoggingFilterを使用することで出来ます。
この方法は割と他でも紹介されているのですが、当方の環境(Spring MVC)だと、他のサイトで紹介されている方法だとうまくいきませんでした。
他のサイトでは主にApplicationConfig等でBean定義する方法が紹介されているのですが、それだとログが出力されませんでした。
原因はSpring BootではなくSpring MVCだからなのかどうかはわかりませんが。
そこで、こちらでは、確実にログに出力ができる方法を紹介します。
さらに、日本語文字列の文字化けへの対処法も紹介していきたいと思います。

# やり方
CommonsRequestLoggingFilterをweb.xmlに設定します。
```xml:web.xml
	<!-- リクエストアクセスログフィルタの設定 -->
	<filter>
	    <filter-name>requestLoggingFilter</filter-name>
	    <filter-class>org.springframework.web.filter.CommonsRequestLoggingFilter</filter-class>
	    <init-param>
	        <param-name>includeClientInfo</param-name>
	        <param-value>true</param-value>
	    </init-param>
	    <init-param>
	        <param-name>includeHeaders</param-name>
	        <param-value>true</param-value>
	    </init-param>
	    <init-param>
	        <param-name>includePayload</param-name>
	        <param-value>true</param-value>
	    </init-param>
	    <init-param>
	        <param-name>maxPayloadLength</param-name>
	        <param-value>1024</param-value>
	    </init-param>
	    <init-param>
	        <param-name>includeQueryString</param-name>
	        <param-value>true</param-value>
	    </init-param>
	</filter>
    <filter-mapping>
        <filter-name>requestLoggingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
```
上記設定で、payloadの部分が、Bodyの内容を出力するかどうかの設定です。
trueを設定して、出力されるようにしています。
maxPayloadLengthは出力の最大サイズです。

さらに、CommonsRequestLoggingFilterで出力したログが出力されるように、ログの設定も行います。
こちらでは、logbackを用いた例です。
```xml:logback.xml
    <logger name="org.springframework.web.filter.CommonsRequestLoggingFilter">
        <level value="debug" />
    </logger>
```

リクエストログはDEBUGレベルなので、上記クラスをDEBUGレベルで出力されるように設定します。

これで、以下のようにリクエスト内容がログに出力されます。

ここで注意が必要なのは、リクエストボディの内容（フォームでPOSTした内容やJSONで送信した内容など）は、payloadに出力されるのですが、開始ログには出力されず、終了ログのほうにのみ出力されることです。

```
2024-08-23 10:11:35 DEBUG	o.s.web.filter.CommonsRequestLoggingFilter	Before request [POST /sample/sample, client=0:0:0:0:0:0:0:1, headers=[user-agent:"PostmanRuntime/7.41.1", accept:"*/*", postman-token:"fb7842b8-3316-477c-a53b-50a35d0f3361", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"136", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]]

.....

2024-08-23 10:11:39	:DEBUG	o.s.web.filter.CommonsRequestLoggingFilter	After request [POST /sample/sample, client=0:0:0:0:0:0:0:1, headers=[user-agent:"PostmanRuntime/7.41.1", accept:"*/*", postman-token:"fb7842b8-3316-477c-a53b-50a35d0f3361", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"136", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"], payload=test=%E3%83%86%E3%82%B9%E3%83%88&test2=test2]
```
こんな感じでログ出力されます。
しかし、test1が文字化けしてしまっています。

ちなみに、JSONだとこんな感じです。
```
2024-08-23 10:11:39	:DEBUG	o.s.web.filter.CommonsRequestLoggingFilter	After request [POST /sample/sample, client=0:0:0:0:0:0:0:1, headers=[user-agent:"PostmanRuntime/7.41.1", accept:"*/*", postman-token:"fb7842b8-3316-477c-a53b-50a35d0f3361", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"136", Content-Type:"application/json;charset=UTF-8"], payload={
      "test":"テスト",
      "test2":"test2"
  }]
```
文字化けしていません。
JSONの場合は標準で用意されているものを使用すれば問題ありませんが、そうでない場合は、ひと工夫が必要です。

# 日本語文字列が文字化けする問題への対処
URLエンコードされたままの文字列が出力されてしまっているので、CommonsRequestLoggingFilterを拡張して、URLデコードしてあげればOKです。
例は以下の通りです。
```java
/**
 * リクエストの内容をログに出力するフィルタ。<br>
 * payloadの内容がエンコードされてるためデコードする処理を独自に加えている
 */
public class SampleRequestLoggingFilter extends CommonsRequestLoggingFilter {

	@Override
	protected String getMessagePayload(HttpServletRequest request) {
		String message = super.getMessagePayload(request);
		if (message == null) {
			return null;
		}
		try {
			return URLDecoder.decode(message, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}
}

```
そして、上記フィルタをweb.xmlに設定します。
```xml:web.xml
	<!-- リクエストアクセスログフィルタの設定 -->
	<filter>
	    <filter-name>requestLoggingFilter</filter-name>
	    <filter-class>jp.example.SampleRequestLoggingFilter</filter-class>
	    <init-param>
	        <param-name>includeClientInfo</param-name>
	        <param-value>true</param-value>
	    </init-param>
	    <init-param>
	        <param-name>includeHeaders</param-name>
	        <param-value>true</param-value>
	    </init-param>
	    <init-param>
	        <param-name>includePayload</param-name>
	        <param-value>true</param-value>
	    </init-param>
	    <init-param>
	        <param-name>maxPayloadLength</param-name>
	        <param-value>1024</param-value>
	    </init-param>
	    <init-param>
	        <param-name>includeQueryString</param-name>
	        <param-value>true</param-value>
	    </init-param>
	</filter>
    <filter-mapping>
        <filter-name>requestLoggingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
```
ログ設定も行います。
```xml:logback.xml
    <logger name="jp.example.SampleRequestLoggingFilter">
        <level value="debug" />
    </logger>
```

上記によって、ログ出力内容は以下のようになります。
```
2024-08-23 10:11:39	:DEBUG	j.s.SampleRequestLoggingFilter	After request [POST /sample/sample, client=0:0:0:0:0:0:0:1, headers=[user-agent:"PostmanRuntime/7.41.1", accept:"*/*", postman-token:"fb7842b8-3316-477c-a53b-50a35d0f3361", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"136", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"], payload=test=テスト&test2=test2]
```
文字化けが解消されました。

# multipart/form-dataの場合
multipart/form-dataの場合は、以下のようになり、payload自体が出力できません。
```
2024-08-23 10:11:39	:DEBUG	o.s.web.filter.CommonsRequestLoggingFilter	After request [POST /sample/sample, client=0:0:0:0:0:0:0:1, headers=[user-agent:"PostmanRuntime/7.41.1", accept:"*/*", postman-token:"fb7842b8-3316-477c-a53b-50a35d0f3361", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"136", Content-Type:"multipart/form-data;boundary=--------------------------352202375894487868900525;charset=UTF-8"]]
```

multipart/form-dataの場合もリクエスト内容を出力したい、という場合は、拡張したクラスを以下のようにすれば対応できます。
```java
public class SampleRequestLoggingFilter extends CommonsRequestLoggingFilter {

	@Override
	protected String getMessagePayload(HttpServletRequest request) {
		// multipart/form-dataの場合
		if (request.getContentType().toLowerCase().startsWith("multipart/form-data")) {
			// リクエストパラメータの内容をJSONに変換して出力
			ObjectMapper mapper = new ObjectMapper();
			try {
				return mapper.writeValueAsString(request.getParameterMap());
			} catch (Exception e) {
				return null;
			}
		}

		String message = super.getMessagePayload(request);
		if (message == null) {
			return null;
		}
		try {
			return URLDecoder.decode(message, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}
}
```
そうすれば、以下のように出力されます。
```
2024-08-23 10:11:39	:DEBUG	o.s.web.filter.CommonsRequestLoggingFilter	After request [POST /sample/sample, client=0:0:0:0:0:0:0:1, headers=[user-agent:"PostmanRuntime/7.41.1", accept:"*/*", postman-token:"fb7842b8-3316-477c-a53b-50a35d0f3361", host:"localhost:8080", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"136", Content-Type:"multipart/form-data;boundary=--------------------------795648822504512060535807;charset=UTF-8"], payload={"test":["テスト"]}]
```
ファイル以外の通常のパラメータのみがJSON形式で出力されます。
この方法は、multipart/form-dataの場合のみ開始ログにもpayloadが出力されます。

# 最後に
とりあえずどんな形であれログを出力すればいい、というケースや、そもそもJSONでしかやり取りしないというケースであれば、標準で用意されているフィルタを利用すればOKです。
しかし、文字化け解消したい、とか、multipart/form-dataにも対応したい、という場合は、フィルタを拡張して一工夫加えれば簡単にログ出力できます。
こちらで紹介した方法は、multipart/form-dataのファイルのファイル名も出力とかまでは対応していませんが、とりあえずそれ以外の入力内容はログ出力できますので、開発時や試験時には有用だと思います。