Spring MVC(+Spring Boot)上でのリクエスト共通処理の実装方法を理解する

今回は、Spring MVCベースのWebアプリケーション(画面アプリ or REST API)で、リクエスト処理内の任意のポイントで共通処理を実行する方法をについて説明します。なお、Servlet 3.0でサポートされた非同期処理利用時の共通処理の実装は、今回は対象外として別の回で紹介したいと思います。（乞うご期待！！）

# 動作検証バージョン

* Spring Framework **5.3.6** ~~(4.3.3.RELEASE -> 5.1.7.RELEASE)~~
* Spring Boot **2.4.5** ~~(1.4.1.RELEASE -> 2.1.5.RELEASE)~~
* Tomcat 9.0.45

> **Note:**
>
> **[2021/5/3]**
> 投稿から5年(前回更新から約2年)くらいたっても引き続き一定のViewが継続してあるので、最新のSpring(Spring Boot)バージョンの内容に更新しました。内容自体には大きな変更はなく、最新のSpring及びSpring Bootのバージョンの内容に合わせて動作を再検証しています。また検証コードをGitHubで公開するようにしました。
>
> **[2019/5/18]**
> 投稿から３年くらいたっても一定のViewが継続してあるので、最新のSpring(Spring Boot)バージョンの内容に更新しました。なお、今回の更新では、`web.xml`は使わずにJava Configを使うサンプルを修正しています。

# 検証アプリケーション

GitHub上で公開するようにしました。

* https://github.com/kazuki43zoo/spring-mvc-common-processing-demo


# 共通処理の実装方法

共通処理は以下のいずれかの方法で実装します。

| 実装方法 | 説明 | 
| ------- | ---- |
| `javax.servlet.ServletRequestListener` | リクエスト開始時とリクエスト終了時のタイミングで任意の処理を実行することができます。 | 
| `javax.servlet.Filter` | Servlet、JSP、静的コンテンツなどのWebリソースへのアクセスの前後に、共通処理を実行することができます。 | 
| `HandlerInterceptor` | Spring MVCのHandlerの呼び出し前後に、共通処理を実行することができます。 |
| `@RestControllerAdvice`/`@ControllerAdvice` | Controller専用の特殊なメソッド(`@InitBinder`メソッド、`@ModelAttribute`メソッド、`@ExceptionHandler`メソッド)を複数のController(`@RestController`/`@Controller`を付与したクラス)で共有できます。 |
| Spring AOP(AspectJ) | SpringのDIコンテナで管理されているBeanのpublicメソッドの呼び出し前後に、共通処理を実行することができます。|

![interceptorProcessing.png](https://qiita-image-store.s3.amazonaws.com/0/117313/cb9cfdc6-dc9f-496a-6002-31d139e21383.png)

# `javax.servlet.ServletRequestListener`

Servlet標準のAPIである`javax.servlet.ServletRequestListener`インタフェースを実装したクラスを作成します。
作成したクラスをサーブレットコンテナに登録すると、リクエスト開始時とリクエスト終了時のタイミングで任意の処理を実行することができます。

```java
package com.example.component.servlet;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomServletRequestListener implements ServletRequestListener {

  private static final Logger logger = LoggerFactory.getLogger(CustomServletRequestListener.class);

  @Override
  public void requestInitialized(ServletRequestEvent sre) {
    logger.debug("requestInitialized : {}", sre);
    // リクエスト開始時の処理を行う。
    // (実装は省略)
  }

  @Override
  public void requestDestroyed(ServletRequestEvent sre) {
    logger.debug("requestDestroyed : {}", sre);
    // リクエスト終了時の処理を行う。
    // (実装は省略)
  }

}
```

実装した`ServletRequestListener`クラスをサーブレットコンテナに登録します。

```java
package com.example.config;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

import org.example.component.servlet.CustomServletRequestListener;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class[]{AppConfig.class};
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[]{WebMvcConfig.class};
  }

  @Override
  protected String[] getServletMappings() {
    return new String[]{"/"};
  }

  @Override
  protected String getServletName() {
    return "app";
  }

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);
    servletContext.addListener(CustomServletRequestListener.class); // サーブレットコンテナに登録
  }

}
```

# Beanのインジェクション

`ServletRequestListener`内の処理でDIコンテナで管理しているBeanを利用したい場合は、`ServletRequestListener`クラスをDIコンテナに登録し、DIコンテナから取得したBeanをサーブレットコンテナへ登録します。

```java
package com.example.component.servlet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.util.Locale;

@Component
public class CustomServletRequestListenerWithDI implements ServletRequestListener {

  private static final Logger logger = LoggerFactory.getLogger(CustomServletRequestListenerWithDI.class);

  private final String systemName;

  public CustomServletRequestListenerWithDI(MessageSource messageSource) { // MessageSourceをインジェクション
    this.systemName = messageSource.getMessage("system.name", null, "demo", Locale.getDefault());
  }

  @Override
  public void requestInitialized(ServletRequestEvent sre) {
    logger.debug("{} : requestInitialized : {}", systemName, sre);
    // リクエスト開始時の処理を行う。
    // (実装は省略)
  }

  @Override
  public void requestDestroyed(ServletRequestEvent sre) {
    logger.debug("{} : requestDestroyed : {}", systemName, sre);
    // リクエスト終了時の処理を行う。
    // (実装は省略)
  }

}
```

```java:
package com.example.config;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;

import com.example.component.servlet.CustomServletRequestListener;
import com.example.component.servlet.CustomServletRequestListenerWithDI;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class[]{AppConfig.class};
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[]{WebMvcConfig.class};
  }

  @Override
  protected String[] getServletMappings() {
    return new String[]{"/"};
  }

  @Override
  protected String getServletName() {
    return "app";
  }

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    // ルートのアプリケーションコンテキストを生成・初期化はSpring提供の機能(ContextLoader)を利用して自前で行う
    WebApplicationContext wac = createRootApplicationContext();
    ContextLoader loader = new ContextLoader(wac);
    loader.setContextInitializers(getRootApplicationContextInitializers());
    loader.initWebApplicationContext(servletContext);
    // アプリケーションコンテキストの破棄処理はリスナー(ContextLoaderListener)の実装を利用
    servletContext.addListener(new ContextLoaderListener(wac) {
      @Override
      public void contextInitialized(ServletContextEvent event) {
        // 初期化済みのため、リスナー経由で再度初期化が行われないようにする
      }
    });
    servletContext.addListener(CustomServletRequestListener.class);
    // DIコンテナからDIコンテナから取得したリスナーをサーブレットコンテナに登録
    servletContext.addListener(wac.getBean(CustomServletRequestListenerWithDI.class));

    // 親クラスのメソッドを呼び出してDispatcherServletを登録
    super.registerDispatcherServlet(servletContext);
  }

}
```

> **Warning: ベストプラクティスについて**
> この方法がベストプラクティスなのかは**かなり微妙です（あまり自身なし・・）** :sweat_smile: 
> 後ほど紹介しますが、Spring Bootを使う場合は、単純にDIコンテナに登録するだけで自動でサーブレットコンテナに登録されます！！
> 

# Spring提供のServletRequestListenerクラス

Spring Web及びSpring MVC提供の`ServletRequestListener`クラスは以下のとおりです。アプリケーションの要件に応じて使用してください。

| クラス | 説明 | 
| ----- | ---- |
| `RequestContextListener` | リクエスト(`HttpServletRequest`と`HttpServletResponse`)+リクエストのロケール情報をスレッドローカルに設定するための`ServletRequestListener`クラス。 |


# `javax.servlet.Filter`

Servlet標準のAPIである`javax.servlet.Filter`インタフェースを実装したクラスを作成します。
作成したクラスをサーブレットコンテナに登録すると、Servletの呼び出し前後に任意の処理を実行することができます。

```java
package com.example.component.servlet;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomFilter implements Filter {

  private static final Logger logger = LoggerFactory.getLogger(CustomFilter.class);

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    logger.debug("init : {}", filterConfig);
    // 初期化処理を行う。このメソッドはアプリケーション起動時に呼び出される。
    // サーブレットフィルタの初期化パラメータは引数のFilterConfigから取得できる。
    // (実装は省略)
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    logger.debug("doFilter : {} {}", request, response);
    // ここに前処理を実装する
    // (実装は省略)
    // 後続処理(次のFilter又はServlet)を呼び出したくない場合は、このタイミングでメソッドを終了(return)すればよい。

    // 後続処理(次のFilter又はServlet)を呼び出す
    filterChain.doFilter(request, response);

    // ここに後処理を実装する
    // (実装は省略)
  }

  @Override
  public void destroy() {
    logger.debug("destroy");
    // アプリケーション終了時に行う処理を実装する
    // (実装は省略)
  }

}
```

`AbstractDispatcherServletInitializer#getServletFilters`メソッドを利用して、実装したFilterクラスをサーブレットコンテナに登録します。

```java:src/main/java/com/example/config/MyDispatcherServletInitializer.java
public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  // ...
  @Override
  protected Filter[] getServletFilters() {
    return new Filter[]{new CustomFilter()}; // 登録したいサーブレットフィルタを返却する
  }
}
```

`AbstractDispatcherServletInitializer`のデフォルト実装では、サーブレットフィルタが適用されるタイミング(`DispatcherType`)は、

* `REQUEST` : 一致するパスやサーブレットへのリクエストを受けた時
* `ASYNC` : 一致するパスやサーブレットへの非同期リクエストを受けた時
* `FORWARD` : 一致するパスやサーブレットへフォワードした時
* `INCLUDE` : 一致するパスやサーブレットをインクルードした時
* `ERROR` : 一致するパスやサーブレットへエラー遷移した時

になります。


## Spring提供のサポートクラスを利用したFilterクラスの作成

Springは、Filterを作成するためのサポートクラスを提供しています。

| クラス | 説明 |
| ----- | ---- |
| `GenericFilterBean` | Filterの初期化パラメータをFilterクラスのプロパティに設定する機能を持つ基底クラス。Springが提供するFilterクラスの多くは、このクラスの子クラスとして実装されています。 |
| `OncePerRequestFilter` | 同一リクエスト内で１回だけ処理を実行することを担保する機能を持つ基底クラス。 `GenericFilterBean`を継承しており、Springが提供するFilterクラスの多くは、このクラスの子クラスとして実装されています。 |


## Beanのインジェクション

Filter内の処理でDIコンテナで管理しているBeanを利用したい場合は、`GenericFilterBean`または`GenericFilterBean`の子クラスを継承したクラスをDIコンテナに登録し、`org.springframework.web.filter.DelegatingFilterProxy`経由でFilterの処理を実行します。

```java
package com.example.component.servlet;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

@Component // コンポーネントスキャン対象にしてDIコンテナに登録
public class CustomFilterWithDI extends GenericFilterBean { // GenericFilterBeanまたはGenericFilterBeanの子クラスを指定

  private static final Logger logger = LoggerFactory.getLogger(CustomFilterWithDI.class);

  private final String systemName;

  public CustomFilterWithDI(MessageSource messageSource) { // MessageSourceをインジェクション
    this.systemName = messageSource.getMessage("system.name", null, "demo", Locale.getDefault());
  }

  @Override
  protected void initFilterBean() throws ServletException {
    logger.debug("{} : initFilterBean", systemName);
    // 初期化処理を行う。このメソッドはアプリケーション起動時に呼び出される。
    // (実装は省略)
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    logger.debug("{} : doFilter : {} {}", systemName, request, response);
    // ここに前処理を実装する
    // (実装は省略)
    // 後続処理(次のFilter又はServlet)を呼び出したくない場合は、このタイミングでメソッドを終了(return)すればよい。

    // 後続処理(次のFilter又はServlet)を呼び出す
    filterChain.doFilter(request, response);

    // ここに後処理を実装する
    // (実装は省略)
  }

  @Override
  public void destroy() {
    logger.debug("{} : destroy", systemName);
    // アプリケーション終了時に行う処理を実装する
    // (実装は省略)
  }
}
```

```java
public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  // ...
  @Override
  protected Filter[] getServletFilters() {
    return new Filter[]{new CustomFilter()
        , new DelegatingFilterProxy("customFilterWithDI")}; // 登録したいサーブレットフィルタを返却する
  }
}
```

なお、FilterのBeanは、`DispatcherServlet`毎のアプリケーションコンテキストではなく、ルートのアプリケーションコンテキストに登録する必要があります。(`DispatcherServlet`毎のDIコンテナにFilterのBeanを登録しても認識されません)


## Spring提供のFilterクラス

Spring Web及びSpring MVC提供のFilterクラスは以下のとおりです。

| クラス | 説明 | 
| ----- | ---- |
| `CorsFilter` | CORS連携用のFilterクラス。 |
| ~~`HttpPutFormContentFilter`~~ | ~~HTMLフォームからのリクエスト(application/x-www-form-urlencoded)でPUTとPATCHメソッドを利用できるようにするためのFilterクラス。~~(**Spring 5.1から非推奨のAPIになり`FormContentFilter`に置き換えることがアナウンスされています**) |
| `HiddenHttpMethodFilter` | リクエストパラメータ(hiddenパラメータ)で指定されたHTTPメソッドに変換する(なりすます)ためのFilterクラス。 |
| `CharacterEncodingFilter` | リクエストとレスポンスの文字エンコーディングを指定するためのFilterクラス。 ※ Spring 4.3からエンコーディングの強制フラグがリクエストとレスポンス毎に指定可能になっている。 |
| `RequestContextFilter` | リクエスト(`HttpServletRequest`と`HttpServletResponse`)+リクエストのロケール情報をスレッドローカルに設定するためのFilterクラス。(`RequestContextListener`のFilterクラス版です) |
| `ResourceUrlEncodingFilter` | 静的リソースにアクセスするためのURLを`ResourceResolver`と連携して生成するFilterクラス。 詳しくは、「[Spring MVC(+Spring Boot)上での静的リソースへのアクセスを理解する](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37)」をご覧ください。|
| `MultipartFilter` | マルチパートリクエストを解析するためのFilterクラス。 |
| `ShallowEtagHeaderFilter` | ETagの制御を行うFilterクラス。 |
| `ServletContextRequestLoggingFilter` | リクエストデータをサーブレットコンテナのログに出力するFilterクラス。 |
| `CommonsRequestLoggingFilter` | リクエストデータをApache Commons Logging(JCL)のAPI経由でログに出力するFilterクラス。 |
| `ForwardedHeaderFilter` | `HttpServletRequest`の`getServerName()`、`getServerPort()`、`getScheme()`、`isSecure()`、`getContextPath()`、`getRequestURI()`、`getRequestURL()`の値をリクエストヘッダ（`Forwarded`や`X-Forwarded-*`）から取得するようにするためのFilterクラス。 ※ Spring 4.3から追加 |
| `RelativeRedirectFilter ` | リダイレクトのロケーションに相対パスを指定できるようにするためのFilterクラス。※ Spring 4.3.10から追加 |
| `FormContentFilter` | HTMLフォームからのリクエスト(application/x-www-form-urlencoded)でPUT/PATCH/DELETEメソッドを利用できるようにするためのFilterクラス。(**Spring 5.1から`HttpPutFormContentFilter`の代わりに追加されたクラスです**) |


# `HandlerInterceptor`

Spring MVCのAPIである`org.springframework.web.servlet.HandlerInterceptor`インタフェースを実装したクラスを作成します。作成したクラスをSpring MVCのフレームワーク機能に適用すると、Handlerメソッドの呼び出し前後に任意の処理を実行することができます。

![handlerInterceptor.png](https://qiita-image-store.s3.amazonaws.com/0/117313/c72169a7-3039-05f2-40a5-c83fe995c25a.png)


```java
package com.example.component.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CustomHandlerInterceptor implements HandlerInterceptor {

  private static final Logger logger = LoggerFactory.getLogger(CustomHandlerInterceptor.class);

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    logger.debug("preHandle : {} {} {}" , request, request, handler);
    // Handlerメソッドが呼び出される前に行う処理を実装する
    // (実装は省略)

    // Handlerメソッドを呼び出す場合はtrueを返却する
    return true;
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    logger.debug("postHandle : {} {} {} {}" , request, response, handler, modelAndView);
    // Handlerメソッドが正常終了した後に行う処理を実装する(例外が発生した場合は、このメソッドは呼び出されない)
    // (実装は省略)
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    logger.debug("afterCompletion : {} {} {} {}" , request, response, handler, ex);
    // Handlerメソッドの呼び出しが完了した後に行う処理を実装する(例外が発生しても、このメソッドは呼び出される)
    // (実装は省略)
  }

}
```

作成した`HandlerInterceptor`をSpring MVCのフレームワーク機能に適用します。

```java
@Configuration
@EnableWebMvc // 注意：Spring Bootの場合は、@EnableWebMvcはつけちゃダメ！！
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CustomHandlerInterceptor())
                .addPathPatterns("/**") // 適用対象のパス(パターン)を指定する
                .excludePathPatterns("/static/**"); // 除外するパス(パターン)を指定する
    }
}
```

適用対象のパスや除外パスには、Ant形式のパスパターンが利用できるため、サーブレットフィルターに比べて柔軟に処理の適用対象を指定することができます。

> **Warning: Spring BootでWebMvcConfigurerAdapterを利用する際の注意点**
>
> Spring Bootで`WebMvcConfigurerAdapter`の子クラスを作成する場合は、`@EnableWebMvc`は絶対につけないでください。`@EnableWebMvc`をつけてしまうと、Spring BootのAutoConfigureのコンフィギュレーションが一部無効になってしまいます。これは[Spring Bootのリファレンス](https://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration)にも記述されています。


## Spring提供の`HandlerInterceptor`の実装クラス

Spring MVC提供の`HandlerInterceptor`の実装クラスは以下のとおりです。アプリケーションの要件に応じて使用してください。

| クラス | 説明 | 
| ----- | ---- |
| `LocaleChangeInterceptor` | LocaleをUI操作(リクエストパラメータ)で変更する機能を実装する際に利用するクラス。 `LocaleResolver`と連携して使用します。|
| `ThemeChangeInterceptor` | ThemeをUI操作(リクエストパラメータ)で変更する機能を実装する際に使用するクラス。 `ThemeResolver`と連携して使用します。|
| `WebContentInterceptor` | リソースのパスパターン毎にHTTPのキャッシュ制御用のレスポンスヘッダーを付与したい場合に使用するクラス。静的リソースのキャッシュ制御については、「[Spring MVC(+Spring Boot)上での静的リソースへのアクセスを理解する](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37)」をご覧ください。 |
| `UserRoleAuthorizationInterceptor ` | `HttpServletRequest#isUserInRole`メソッドを呼び出して、ユーザからのリクエストへの認可を行うためのクラス。※ 最初の投稿時点ですでに存在指定たクラスですが、見落としていたので追加しました |


上記にあがたクラス群は、Webアプリケーションの外部仕様に関わる動作を変えるため仕組みを提供してくれているクラスですが、内部の作りを手助けしてくれるためのクラス群もいくつか提供されています。（最初に投稿した時点ですでに提供されていたクラス群になりますが、最新化に伴い説明を追加してみました）

| クラス | 説明 | 
| ----- | ---- |
| `MappedInterceptor` | `HandlerInterceptor`の適用パスパターンと除外パスパターンを指定することができるクラス。 |
| `ConversionServiceExposingInterceptor ` | `ConversionService`をリクエストスコープに追加するためのクラス。このクラスは、JSPなどのテンプレートエンジンがリクエストスコープを介して`ConversionService`にアクセスできるようにするために作成されたみたいです。 |
| `ResourceUrlProviderExposingInterceptor ` | `ResourceUrlProvider`をリクエストスコープに追加するためのクラス。 |


# `@RestControllerAdvice` / `@ControllerAdvice`

`@RestController`(`@Controller`)を付与したクラスには、Handlerメソッド(`@RequestMapping`を付与したメソッド)とは別に、Controller専用の特殊なメソッド(`@InitBinder`メソッド、`@ModelAttribute`メソッド、`@ExceptionHandler`メソッド)を実装することができます。これらのメソッドを複数のControllerクラスで共有したい場合は、`@RestControllerAdvice`(`@ControllerAdvice`)の仕組みを使用します。


| メソッド | 説明 |
| ------ | ---- |
| `@InitBinder`メソッド | `WebDataBinder`オブジェクト(リクエストデータをJavaオブジェクトにバインドするためのオブジェクト)をカスタマイズするためのメソッド。型変換、フォーマッティング、バリデーションなどをカスタマイズすることができます。 |
| `@ModelAttribute`メソッド | `Model`にオブジェクトを格納するためのメソッド。Handlerメソッド実行前に呼び出され、返却したオブジェクトは`Model`に格納されます。 |
| `@ExceptionHandler`メソッド | 例外をハンドリングするためのメソッド。 |


![controllerAdvice.png](https://qiita-image-store.s3.amazonaws.com/0/117313/f3e8387e-119c-740c-b08f-757dacac484c.png)


```java
package com.example.component.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Optional;
import java.util.UUID;

@RestControllerAdvice // クラスレベルに@RestControllerAdvice(or @ControllerAdvice)を指定する
public class CustomControllerAdvice {

  private static final Logger logger = LoggerFactory.getLogger(CustomControllerAdvice.class);

  @InitBinder
  public void initBinder(WebDataBinder dataBinder) {
    logger.debug("initBinder : {}", dataBinder);
    // WebDataBinderのメソッドを呼び出してカスタマイズする
    dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
  }

  @ModelAttribute("trackingId")
  public String addOneObject(@RequestHeader("X-Tracking-Id") Optional<String> trackingId) {
    logger.debug("addOneObject : {}", trackingId);
    // Modelに追加するオブジェクトを返却する
    return trackingId.orElse(UUID.randomUUID().toString());
  }

  @ExceptionHandler
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public String handleSystemException(Exception e) {
    // 例外ハンドリングを行う
    logger.error("System Error occurred.", e);
    return "System Error!!";
  }

}
```

> **Note:**
>
> `@RestControllerAdvice`は、`@ControllerAdvice`と`@ResponseBody`の合成アノテーションになっているため、`@ExceptionHandler`のメソッドの返り値は、レスポンスBODYに設定する値として扱われます。



`@RestControllerAdvice`/`@ControllerAdvice`の属性をすべて省略した場合は、すべてのControllerクラスに適用されます。適用対象を絞り込みたい場合は、「ベースパッケージ(`basePackages`、`basePackageClasses`属性)」「マーカーインターフェース(`assignableTypes`属性)」「マーカーアノテーション(`annotations`属性)」を使用してください。

> **Note: `@ModelAttribute`メソッドのシグネチャ**
> `@ModelAttribute`メソッドの返り値を`void`にし、引数で受け取った`Model`に直接オブジェクトを追加することもできます。これは、ひとつのメソッドで複数のオブジェクトを`Model`に追加したい場合に使えます。
>
> ```java
> @ModelAttribute
> public void addSomeObjects(@RequestHeader("X-Tracking-Id") Optional<String> trackingId, Model model) {
>     // Modelにオブジェクトを追加する
>     model.addAttribute("trackingId", trackingId.orElse(UUID.randomUUID().toString()));
>     // ...
> }
> ```


# Spring AOP with AspectJ

SpringのDIコンテナで管理されているBeanのpublicメソッドの呼び出し前後に共通処理を実行したい場合は、Spring AOPのAdviceを実装します。Spring AOPで実装できるAdviceは以下の5種類です。

> **Note: Adviceとは**
> Join Pointに対して特定のタイミングで実行する横断的な関心事(共通処理)のことです。 
> **Note: Join Pointとは**
> 横断的な関心事(共通処理)を適用するポイントのことです。 Spring AOPのJoin Pointは「**メソッドの実行時**」です。

| Advice | 説明 |
| ------ | ---- |
| Before | Join Pointの前に実行するAdvice。Join Pointへ処理が流れないようにしたい場合は、例外のスローします。|
| AfterReturning | Join Pointが正常終了した後に実行するAdvice。Join Pointで例外が発生した場合は、このAdviceは実行されません。|
| AfterThrowing | Join Pointから例外がスローされた後に実行するAdvice。Join Pointが正常終了した場合は、このAdviceは実行されません。|
| After | Join Pointの後に実行するAdvice。Join Pointの正常終了や例外のスローに関係なく常に実行されます。|
| Around | Join Pointの前後で実行するAdvice。共通処理を実行するタイミングなどを完全にコントロールすることができます。|

各Adviceが実行されるタイミングを図で表すと以下のようになります。

![aopAdvice.png](https://qiita-image-store.s3.amazonaws.com/0/117313/a3ba9fb2-7644-7846-9444-54b5c7777f17.png)

Around Adviceを使うとBefore, AfterReturning, AfterThrowing, Afterと同じことを実現できますが、用途にあったAdviceを使うようにしましょう。

## Spring AOPとは

Spring Frameworkは、AOPを実現するサブプロジェクトとしてSpring AOPを提供しています。 Spring AOPでは、DIコンテナに管理されているBeanをTargetとしてAdviceを埋め込むことができます。Spring AOPは、最も有名なAOPフレームワークであるAspectJを利用しており、AspectJが提供するアノテーションやPointCutの式言語を利用しています。

> **Note: PointCutとは**
> Adviceを実行するJoin Pointを選択する表現(式)のことです。
> **Note: Weavingとは**
> アプリケーションコードの適切なポイントにAspectを入れ込む処理のことです。AspectJではWeavingのメカニズムとして、コンパイル時、クラスロード時、実行時がサポートされていますが、 Spring AOPではProxyオブジェクトを作ることで実行時のWeavingのみサポートしています。

![aopProxy.png](https://qiita-image-store.s3.amazonaws.com/0/117313/67d2cd41-64a5-2507-0337-a1551ae91c25.png)


## Spring AOPのセットアップ

Spring AOPを使用する場合は、spring-aopモジュール(spring-webmvcが依存している)とaspectjweaverが必要になります。

```xml:pom.xml
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version> <!-- 投稿時(更新時)の最新バージョン -->
</dependency>
```

Spring AOPを使用するために必要となるBeanを定義します。Java Configの場合は、コンフィギュレーションクラスに`@org.springframework.context.annotation.EnableAspectJAutoProxy`を付与するだけです。

```java
@Configuration
@ComponentScan("com.example.component")
@EnableAspectJAutoProxy // 追加
public class AppConfig {
    // ...
}
```

Spring AOPはProxyを生成してAspectをWeavingしますが、対象となるクラスの状態によってProxy生成に使用される仕組みが異なります。

* 対象がインターフェースを実装していればJDKのProxyの仕組み
* 対象がインターフェースを実装していなければCGLIBのProxyの仕組み

CGLIBの仕組みを使ってProxyを作ることを強制したい場合は、`proxyTargetClass`属性を`true`にしてください。

```java
// ...
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
    // ...
}
```



## 対象クラスの作成

まず、Adviceを埋め込む対象となるクラスを作成します。
ここでは、正常終了するメソッドと例外が発生する２つのメソッドを実装しています。

```java
package com.example.component.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class GreetingService {

  private static final Logger logger = LoggerFactory.getLogger(GreetingService.class);

  public String hello(String name, String trackingId) {
    logger.debug("Hi {}.", name);
    if ("system-error".equals(name)) {
      throw new IllegalArgumentException("Name is invalid.");
    }
    return "Hello " + name + "! tracing with " + trackingId;
  }

}
```

つぎに、動作確認用にJUnitのテストケースクラスとテストメソッドを作成します。
ここでは、SpringのDIコンテナと連携する必要があるため、Spring Testが提供しているDIコンテナと連携する仕組みを利用します。

```java
package com.example;

import com.example.component.service.GreetingService;
import com.example.config.AppConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
class GreetingServiceTest {

  @Autowired
  GreetingService greetingService;

  @Test
  void success() {
    String message = greetingService.hello("Kazuki", "abc");
    Assertions.assertEquals("Hello Kazuki! tracing with abc", message);
  }

  @Test
  void failing() {
    Assertions.assertThrows(IllegalArgumentException.class,
        () -> greetingService.hello("system-error", "def"));
  }

}
```

> **Note:**
>
> 初回投稿時はJUnit 4を使う前提で記載していましたが、最新化にともないJUnit 5を利用するサンプルに変更しています。

テストケースを実行すると、コンソールに以下の内容が表示されます。


```text:コンソール
date:2021-05-04 00:29:39.762	thread:main	level:DEBUG	message:Hi Kazuki.
date:2021-05-04 00:29:39.783	thread:main	level:DEBUG	message:Hi system-error.
```

```xml:logback.xmlの設定例
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern><![CDATA[date:%d{yyyy-MM-dd HH:mm:ss.SSS}\tthread:%thread\tlevel:%-5level\tmessage:%msg%n]]></pattern>
    </encoder>
  </appender>

  <logger name="com.example">
    <level value="debug" />
  </logger>

  <root>
    <level value="info" />
    <appender-ref ref="STDOUT" />
  </root>

</configuration>
```


なお、上記のコードをコンパイルするためには、以下のアーティファクトを依存ライブラリに追加する必要があります。

```xml:pom.xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <version>5.7.1</version> <!-- 投稿時(更新時)の最新バージョン -->
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>5.3.6</version> <!-- 投稿時(更新時)の最新バージョン -->
  <scope>test</scope>
</dependency>
```

## Aspectクラスの作成

Aspectは、AOPの単位となる横断的な関心事を示すモジュールのことです。Spring AOPでは、AspectJの`@org.aspectj.lang.annotation.Aspect`が付与されたクラスがAspectとして認識されます。

```java
package com.example.component.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LoggingAspect {
  private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
}
```

## Before Adviceの実装

Before Adviceは、`@org.aspectj.lang.annotation.Before`を付与したメソッドに実装します。

```java
  @Before("execution(* *..*Service.*(..))")
  public void startLog(JoinPoint jp) {
    logger.debug("@Before : {}", jp.getSignature());
  }
```

```text:コンソール
date:2021-05-04 00:35:27.966	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:35:27.975	thread:main	level:DEBUG	message:Hi Kazuki.

date:2021-05-04 00:35:27.995	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:35:27.995	thread:main	level:DEBUG	message:Hi system-error.
```

## AfterReturning Adviceの実装

AfterReturning Adviceは、`@org.aspectj.lang.annotation.AfterReturning`を付与したメソッドに実装します。

```java
  @AfterReturning(pointcut = "execution(* *..*Service.*(..))", returning = "ret")
  public void normalEndLog(JoinPoint jp, Object ret) {
    logger.debug("@AfterReturning : {}", jp.getSignature() + " ret: " + ret);
  }
```

```text:コンソール
date:2021-05-04 00:37:58.903	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:37:58.911	thread:main	level:DEBUG	message:Hi Kazuki.
date:2021-05-04 00:37:58.911	thread:main	level:DEBUG	message:@AfterReturning : String com.example.component.service.GreetingService.hello(String,String) ret: Hello Kazuki! tracing with abc

date:2021-05-04 00:37:58.925	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:37:58.925	thread:main	level:DEBUG	message:Hi system-error.
```

## AfterThrowing Adviceの実装

AfterThrowing Adviceは、`@org.aspectj.lang.annotation.AfterThrowing`を付与したメソッドに実装します。

```java
  @AfterThrowing(pointcut = "execution(* *..*Service.*(..))", throwing = "t")
  public void failureEndLog(JoinPoint jp, Throwable t) {
    logger.debug("@AfterThrowing : {}", jp.getSignature() + " t: " + t);
  }
```

```text:コンソール
date:2021-05-04 00:39:33.238	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:39:33.251	thread:main	level:DEBUG	message:Hi Kazuki.
date:2021-05-04 00:39:33.252	thread:main	level:DEBUG	message:@AfterReturning : String com.example.component.service.GreetingService.hello(String,String) ret: Hello Kazuki! tracing with abc

date:2021-05-04 00:39:33.273	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:39:33.273	thread:main	level:DEBUG	message:Hi system-error.
date:2021-05-04 00:39:33.274	thread:main	level:DEBUG	message:@AfterThrowing : String com.example.component.service.GreetingService.hello(String,String) t: java.lang.IllegalArgumentException: Name is invalid.
```

## After Adviceの実装

After Adviceは、`@org.aspectj.lang.annotation.After`を付与したメソッドに実装します。

```java
  @After("execution(* *..*Service.*(..))")
  public void completeLog(JoinPoint jp) {
    logger.debug("@After : {}", jp.getSignature());
  }
```

```text:コンソール
date:2021-05-04 00:41:18.531	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:41:18.539	thread:main	level:DEBUG	message:Hi Kazuki.
date:2021-05-04 00:41:18.539	thread:main	level:DEBUG	message:@AfterReturning : String com.example.component.service.GreetingService.hello(String,String) ret: Hello Kazuki! tracing with abc
date:2021-05-04 00:41:18.539	thread:main	level:DEBUG	message:@After : String com.example.component.service.GreetingService.hello(String,String)

date:2021-05-04 00:41:18.554	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:41:18.554	thread:main	level:DEBUG	message:Hi system-error.
date:2021-05-04 00:41:18.554	thread:main	level:DEBUG	message:@AfterThrowing : String com.example.component.service.GreetingService.hello(String,String) t: java.lang.IllegalArgumentException: Name is invalid.
date:2021-05-04 00:41:18.554	thread:main	level:DEBUG	message:@After : String com.example.component.service.GreetingService.hello(String,String)
```

## Around Advice

Around Adviceは、`@org.aspectj.lang.annotation.Around`を付与したメソッドに実装します。

```java
  @Around("execution(* *..*Service.*(..))")
  public Object aroundLog(ProceedingJoinPoint jp) throws Throwable {
    Object ret;
    try {
      logger.debug("Before by @Around : {}", jp.getSignature());
      ret = jp.proceed();
      logger.debug("AfterReturning by @Around : {} ret:{}", jp.getSignature(), ret);
    } catch (Throwable t) {
      logger.debug("AfterThrowing by @Around : {}", jp.getSignature(), t);
      throw t;
    } finally {
      logger.debug("After by @Around : {}", jp.getSignature());
    }
    return ret;
  }
```

```text
date:2021-05-04 00:42:31.585	thread:main	level:DEBUG	message:Before by @Around : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:42:31.585	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:42:31.598	thread:main	level:DEBUG	message:Hi Kazuki.
date:2021-05-04 00:42:31.599	thread:main	level:DEBUG	message:@AfterReturning : String com.example.component.service.GreetingService.hello(String,String) ret: Hello Kazuki! tracing with abc
date:2021-05-04 00:42:31.599	thread:main	level:DEBUG	message:@After : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:42:31.599	thread:main	level:DEBUG	message:AfterReturning by @Around : String com.example.component.service.GreetingService.hello(String,String) ret:Hello Kazuki! tracing with abc
date:2021-05-04 00:42:31.599	thread:main	level:DEBUG	message:After by @Around : String com.example.component.service.GreetingService.hello(String,String)

date:2021-05-04 00:42:31.616	thread:main	level:DEBUG	message:Before by @Around : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:42:31.616	thread:main	level:DEBUG	message:@Before : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:42:31.617	thread:main	level:DEBUG	message:Hi system-error.
date:2021-05-04 00:42:31.618	thread:main	level:DEBUG	message:@AfterThrowing : String com.example.component.service.GreetingService.hello(String,String) t: java.lang.IllegalArgumentException: Name is invalid.
date:2021-05-04 00:42:31.618	thread:main	level:DEBUG	message:@After : String com.example.component.service.GreetingService.hello(String,String)
date:2021-05-04 00:42:31.622	thread:main	level:DEBUG	message:AfterThrowing by @Around : String com.example.component.service.GreetingService.hello(String,String)
java.lang.IllegalArgumentException: Name is invalid.
	at com.example.component.service.GreetingService.hello(GreetingService.java:15)
        ...
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
date:2021-05-04 00:42:31.623	thread:main	level:DEBUG	message:After by @Around : String com.example.component.service.GreetingService.hello(String,String)
```

## PointCut式

PointCut式は、Join Pointを選択する表現のことで、上記の例だとアノテーションの`value`属性(or `pointcut`属性)に指定している「`"execution(* *..*Service.*(..))"`」がPointCut式になります。AspectJでは様々な式を用いてJoinPointを選択することができ、Spring AOPでもAspectJのPointCut式の多くをサポートしています。
PointCut式には、マッチング方法毎に指示子(designator)が用意されており、それぞれ書式が異なります。 

Spring AOPでサポートされている指示子は以下のとおりです。

| 指示子 | 説明 |
| ------ | --- |
| `execution` | 指定したメソッド(パターン)に一致するメソッドをJoinPointとして選択する。 |
| `within` | 指定したクラス(パターン)に一致するBeanが保持するメソッドをJoinPointとして選択する。 |
| `this` | 指定したタイプ(クラス、インターフェース)に一致するBeanへのProxyが保持するメソッドをJoinPointとして選択する。 |
| `target` | 指定したインターフェース(クラス、インターフェース)に一致するBeanが保持するメソッドをJoinPointとして選択する。 |
| `args` | 指定したタイプが引数に宣言されているメソッドをJoinPointとして選択する。 |
| `@within` | 指定したアノテーションが付与されたクラスが保持するメソッドをJoinPointとして選択する。 |
| `@annotation` | 指定したアノテーションがメソッドに付与されているメソッドをJoinPointとして選択する。 |
| `@args` | 指定したアノテーションを保持するタイプが引数に宣言されているメソッドをJoinPointとして選択する。 |
| `bean` | 指定したbean名に一致するBeanが保持するメソッドをJoinPointとして選択する。 Spring AOPのみで使用できる指示子。 |

各指示子の書式や指定例は、 [Spring Frameworkの公式リファレンス](https://docs.spring.io/spring-framework/docs/5.3.6/reference/html/core.html#aop-pointcuts)を参照してください。


# Spring Boot上での共通処理の実装

ここでは、Spring Boot Starter Webをベースに、Spring Boot固有の仕組みや実装方法について説明します。
Spring Bootは、Spring-Boot向けの開発プロジェクトを生成するためのWeb Service「[SPRING INITIALIZR](https://start.spring.io/)」を提供しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/e1e405b9-5d11-7cf1-900a-525fabadcaa5.png)


## `javax.servlet.ServletRequestListener`

### Springや3rdパーティ製の`ServletRequestListener`の登録

Bean定義ファイルを使用してDIコンテナに登録します。

```java
@Bean
public RequestContextListener requestContextListener(){
    return new RequestContextListener();
}
```

3rdパーティ製の`ServletRequestListener`に`@javax.servlet.annotation.WebListener`が付与されている場合は、`@org.springframework.boot.web.servlet.ServletComponentScan`を使用することもできます。

```java
@SpringBootApplication
@ServletComponentScan
public class WebDemoApplication {
   // ....
}
```

### 自作の`ServletRequestListener`の登録

`@Component`を付与した`ServletRequestListener`クラスを作成するのが一番簡単でしょう。適用順序も`@org.springframework.core.annotation.Order`で指定できます(もしくは、`org.springframework.core.Ordered`インタフェースを実装して指定する)。

```java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 必要に応じて優先度を指定。優先度が高い方が先に適用される。
public class CustomServletRequestListener implements ServletRequestListener {
    // ...
}
```

もちろん`@Bean`や`@ServletComponentScan`を使用してDIコンテナに登録することもできます。


## `javax.servlet.Filter`

## Spring Boot提供のFilterクラス

Spring Boot提供のFilterクラスは以下のとおりです。なお、適用順序を強制するためだけに拡張している`OrderedXxxFilter`の紹介は割愛します。

| クラス | 説明 | 
| ----- | ---- |
| `ApplicationContextHeaderFilter` | アプリケーションコンテキストのID(`ApplicationContext#getId()`)をレスポンスヘッダ(X-Application-Context)に出力するためのFilterクラス。 |

### 自動登録される`javax.servlet.Filter`

Spring Bootは、自動コンフィギュレーションによって以下の`Filter`が登録されます。

| クラス | マッピング | デフォルトで登録 | 備考 |
| ----- | ---- | ---- | ---- |
| `CharacterEncodingFilter`<br>(`OrderedCharacterEncodingFilter`) | `/*` | yes | - |
| `FormContentFilter`<br>(`OrderedFormContentFilter`) | `/*` | yes | - |
| `RequestContextFilter`<br>(`OrderedRequestContextFilter`) | `/*` | yes | - |
| `HiddenHttpMethodFilter`<br>(`OrderedHiddenHttpMethodFilter`) | `/*` | no | 2.2.0よりデフォルトで登録されなくなった |

> NOTE:
>
> `HiddenHttpMethodFilter`は、Spring Boot提供のプロパティを使って有効化することができます。
>
> ```properties
> spring.mvc.hiddenmethod.filter.enabled=true
> ```

登録されたフィルターを確認するには、以下のようにログレベルを変更を変えてみてください。

```properties:src/resources/application.properties
logging.level.web=trace
```

```text:コンソール
...
2021-05-04 02:24:14.144 TRACE 72467 --- [           main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'characterEncodingFilter'; order=-2147483648, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class]
2021-05-04 02:24:14.144 TRACE 72467 --- [           main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'formContentFilter'; order=-9900, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.class]
2021-05-04 02:24:14.144 TRACE 72467 --- [           main] o.s.b.w.s.ServletContextInitializerBeans : Created Filter initializer for bean 'requestContextFilter'; order=-105, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]
2021-05-04 02:24:14.177 DEBUG 72467 --- [           main] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105
...
```

なお、`org.springframework.boot:spring-boot-starter-web`以外のstarterやライブラリをプロジェクトに追加した場合は、上記以外の`Filter`が自動登録される可能性もあります。


### `javax.servlet.Filter`の登録

Spring Bootの自動コンフィギュレーションに仕組みで登録されない`Filter`や自作の`Filter`をサーブレットコンテナに登録する場合は、`Filter`クラスをDIコンテナに登録します。

### Springや3rdパーティ製の`Filter`の登録

Bean定義ファイルを使用してDIコンテナに登録します。

```java
@Bean
public CommonsRequestLoggingFilter commonsRequestLoggingFilter(){
    return new CommonsRequestLoggingFilter();
}
```

`Filter`の適用順序の指定や細かい設定をカスタマイズしたい場合は、`org.springframework.boot.context.embedded.FilterRegistrationBean`を使用してDIコンテナに登録します。

```java
@Bean
public FilterRegistrationBean commonsRequestLoggingFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean(new CommonsRequestLoggingFilter());
    registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 優先度を指定。優先度が高い方が先に適用される。
    return registrationBean;
}
```

3rdパーティ製の`Filter`に`@javax.servlet.annotation.WebFilter`が付与されている場合は、`@org.springframework.boot.web.servlet.ServletComponentScan`を使用することもできます。

```java
@SpringBootApplication
@ServletComponentScan
public class WebDemoApplication {
   // ....
}
```

### 自作の`Filter`の登録

`@Component`を付与した`Filter`クラスを作成するのが一番簡単でしょう。適用順序も`@org.springframework.core.annotation.Order`で指定できます(もしくは、`org.springframework.core.Ordered`インタフェースを実装して指定する)。

```java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 必要に応じて優先度を指定。優先度が高い方が先に適用される。
public class CustomFilter implements Filter {
    // ...
}
```

もちろん`@Bean`(+`FilterRegistrationBean`)や`@ServletComponentScan`を使用してDIコンテナに登録することもできます。

### `CharacterEncodingFilter`のカスタマイズ

Spring Bootの自動コンフィギュレーションでは、`CharacterEncodingFilter`の文字エンコーディングはUTF-8になります。この動作を変更したい場合は、application.properties(or yml)の指定で変更することができます。

```properties:src/main/resources/application.properties
# CharacterEncodingFilterの適用有無を指定(デフォルトは適用する)
server.servlet.encoding.enabled=false
# エンコーディングを指定(デフォルトはUTF-8)
server.servlet.encoding.charset=Windows-31J
# エンコーディングの適用を強制するかのデフォ値を指定（リクエスト／レスポンス毎の指定がない時に利用される）
server.servlet.encoding.force=false
# リクエストへエンコーディングの適用を強制するかを指定(省略時は全体のデフォ値、全体のデフォ値の指定がない場合は(true=強制)
server.servlet.encoding.force-request=false
# レスポンスへエンコーディングの適用を強制するかを指定(省略時は全体のデフォ値、全体のデフォ値の指定がない場合は(false=強制しない)
server.servlet.encoding.force-response=true
```

> NOTE:
>
> Spring Boot 2.3よりプレフィックスキーが`spring.http`から`server.servlet`に変更になっています。

## Spring AOP

Spring AOPとAspectJのアーティファクトは、Spring Bootが提供している`org.springframework.boot:spring-boot-starter-aop`を指定すれば解決できます。これは、SPRING INITIALIZRでプロジェクトを作成する際に、Dependenciesに「Aspects」を指定すれば追加してくれます。

```xml:pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
```

`@EnableAspectJAutoProxy`はSpring Bootがデフォルトで適用してくれる仕組みになっています。
Spring Bootの自動コンフィギュレーションによって`@EnableAspectJAutoProxy`を適用したくない場合は、以下のような設定を追加してください。

```properties:src/main/resources/application.properties
spring.aop.auto=false
```

### Proxy生成にCGLIBの利用を強制

CGLIBの仕組みを使ってProxyを作ることを強制したい場合は、以下のような設定を追加してください。

```properties:src/main/resources/application.properties
spring.aop.proxy-target-class=true
```

# まとめ

今回は、リクエスト処理内の任意のポイントで共通処理を実行する方法を紹介しました。
`Filter`、`HandlerInterceptor`、AOP(特にAOP)は共通処理を実装するのに強力な仕組みですが、**乱用するとメンテナンス性を損なう可能性がある**ことを意識しておいた方がよいでしょう。例えば、入力値(メソッド引数)や出力値(メソッドの返り値)を更新してしまうような共通処理（その共通処理がないと後続処理の動作がかわってしまうような処理）は原則さけるべきです。この手の処理を共通化したい場合は、共通処理を行うコンポーネントを作成し、明示的にアプリケーションコードの中からメソッドを呼び出すスタイルの方が私はよいと思います。

ちなみに、Spring AOPの説明は、全貌のほんの一部です。この投稿では、Spring AOPの雰囲気を掴んでもらえれば幸いです。Spring AOPについて詳しく知りたい方は、[Spring Framework公式リファレンス](https://docs.spring.io/spring-framework/docs/5.3.6/reference/html/core.html#aop)をご覧ください。
また、「Servlet 3.0でサポートされた非同期処理利用時の共通処理の実装」については、後日「Spring MVC(+Spring Boot)上でのServlet標準の非同期処理を理解する(仮名)」の中で紹介できればと思っています。


# 参考サイト

* https://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle/
* https://docs.spring.io/spring-framework/docs/5.3.6/reference/html/
* http://terasolunaorg.github.io/guideline/5.7.0.RELEASE/ja/