Help us understand the problem. What is going on with this article?

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

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

動作検証バージョン

  • Spring Framework 4.3.3.RELEASE -> 5.1.7.RELEASE
  • Spring Boot 1.4.1.RELEASE -> 2.1.5.RELEASE

Note:

[2019/5/18]
投稿から3年くらいたっても一定のViewが継続してあるので、最新のSpring(Spring Boot)バージョンの内容に更新しました。なお、今回の更新では、web.xmlは使わずにJava Configを使うサンプルを修正しています。

共通処理の実装方法

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

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

interceptorProcessing.png

javax.servlet.ServletRequestListener

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

package com.example.component;

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クラスをサーブレットコンテナに登録します。

src/main/java/com/example/config/MyDispatcherServletInitializer.java
package com.example.config;

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

import com.example.spring.web.listener.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をサーブレットコンテナへ登録します。

package com.example.component;

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

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

@Component // コンポーネントスキャン対象にしてDIコンテナへ登録
public class CustomServletRequestListener implements ServletRequestListener {

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

  private final String systemName;

  public CustomDIServletRequestListener(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);
    // リクエスト終了時の処理を行う。
    // (実装は省略)
  }

}
src/main/java/com/example/config/MyDispatcherServletInitializer.java
package com.example.config;

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

import com.example.spring.web.listener.CustomDIServletRequestListener;
import com.example.spring.web.listener.CustomServletRequestListener;
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(CustomDIServletRequestListener.class));

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

}

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

Spring提供のServletRequestListenerクラス

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

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

javax.servlet.Filter

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

package com.example.component;

import javax.servlet.*;
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クラスをサーブレットコンテナに登録します。

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 同一リクエスト内で1回だけ処理を実行することを担保する機能を持つ基底クラス。 GenericFilterBeanを継承しており、Springが提供するFilterクラスの多くは、このクラスの子クラスとして実装されています。

Beanのインジェクション

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

public class CustomFilter implements Filter {
import javax.servlet.*;
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 CustomFilter extends GenericFilterBean { // GenericFilterBeanまたはGenericFilterBeanの子クラスを指定

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

  private final String systemName;

  public CustomDIFilter(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);
    // アプリケーション終了時に行う処理を実装する
    // (実装は省略)
  }

}
src/main/java/com/example/config/MyDispatcherServletInitializer.java
public class MyDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  // ...
  @Override
  protected Filter[] getServletFilters() {
    return new Filter[]{new DelegatingFilterProxy("customDIFilter")};
  }
}

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

Spring提供のFilterクラス

Spring Web及びSpring MVC提供のFilterクラスは以下のとおりです。CharacterEncodingFilterは必ず設定する必要がありますが、その他の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 リクエスト(HttpServletRequestHttpServletResponse)+リクエストのロケール情報をスレッドローカルに設定するためのFilterクラス。(RequestContextListenerのFilterクラス版です)
ResourceUrlEncodingFilter 静的リソースにアクセスするためのURLをResourceResolverと連携して生成するFilterクラス。 詳しくは、「Spring MVC(+Spring Boot)上での静的リソースへのアクセスを理解する」をご覧ください。
MultipartFilter マルチパートリクエストを解析するためのFilterクラス。
ShallowEtagHeaderFilter ETagの制御を行うFilterクラス。
ServletContextRequestLoggingFilter リクエストデータをサーブレットコンテナのログに出力するFilterクラス。
CommonsRequestLoggingFilter リクエストデータをApache Commons Logging(JCL)のAPI経由でログに出力するFilterクラス。
ForwardedHeaderFilter HttpServletRequestgetServerName()getServerPort()getScheme()isSecure()getContextPath()getRequestURI()getRequestURL()の値をリクエストヘッダ(ForwardedX-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

package com.example.component;

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 { // Spring 4.3.x利用者はHandlerInterceptorAdapterを継承するとよい

  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のフレームワーク機能に適用します。

@Configuration
@EnableWebMvc // 注意:Spring Bootの場合は、@EnableWebMvcはつけちゃダメ!!
public class WebMvcConfig
    implements WebMvcConfigurer { // Spring 4.3.x利用者はWebMvcConfigurerAdapterを継承すると良い
    @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のリファレンスにも記述されています。

Spring提供のHandlerInterceptorの実装クラス

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

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

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

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

@ControllerAdvice / @RestControllerAdvice

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

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

controllerAdvice.png

package com.example.component;

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.*;

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

@ControllerAdvice // クラスレベルに@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 "error/system";
  }

}

Note:

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

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

Note: @ModelAttributeメソッドのシグネチャ
@ModelAttributeメソッドの返り値をvoidにし、引数で受け取ったModelに直接オブジェクトを追加することもできます。これは、ひとつのメソッドで複数のオブジェクトをModelに追加したい場合に使えます。

@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

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

Spring AOPのセットアップ

Spring AOPを使用する場合は、spring-aopモジュールとaspectjweaverが必要になります。

pom.xml
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.7.RELESAE</version> <!-- 投稿時の最新バージョン -->
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version> <!-- 投稿時の最新バージョン -->
</dependency>

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

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

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

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

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

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

対象クラスの作成

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

package com.example.component;

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 void goodMorning() {
    logger.debug("Good Morning !!");
  }

  public void failing() {
    logger.debug("Failing !!");
    throw new UnsupportedOperationException();
  }

}

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

package com.example.component;

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 goodMorning() {
    greetingService.goodMorning();
  }

  @Test
  void failing() {
    Assertions.assertThrows(UnsupportedOperationException.class, () -> greetingService.failing());
  }

}

Note:

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

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

コンソール
date:2019-05-19 01:51:28.619    thread:main level:DEBUG message:Failing !!
date:2019-05-19 01:51:28.632    thread:main level:DEBUG message:Good Morning !!
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>

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

pom.xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <version>5.4.2</version> <!-- 投稿時の最新バージョン -->
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>5.1.7.RELEASE</version> <!-- 投稿時の最新バージョン -->
  <scope>test</scope>
</dependency>

Aspectクラスの作成

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

package com.example.component;

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を付与したメソッドに実装します。

@Before("execution(* *..*Service.*(..))")
public void startLog(JoinPoint jp) {
  logger.debug("@Before                   : {}", jp.getSignature());
}
コンソール
date:2019-05-19 01:58:35.561    thread:main level:DEBUG message:@Before                   : void com.example.spring.web.service.GreetingService.failing()
date:2019-05-19 01:58:35.586    thread:main level:DEBUG message:Failing !!
date:2019-05-19 01:58:35.600    thread:main level:DEBUG message:@Before                   : void com.example.spring.web.service.GreetingService.goodMorning()
date:2019-05-19 01:58:35.601    thread:main level:DEBUG message:Good Morning !!

AfterReturning Adviceの実装

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

@AfterReturning(pointcut = "execution(* *..*Service.*(..))", returning = "ret")
public void normalEndLog(JoinPoint jp, Object ret) {
  logger.debug("@AfterReturning           : {}", jp.getSignature() + " ret: " + ret);
}
コンソール
date:2019-05-19 02:00:39.060    thread:main level:DEBUG message:Failing !!
date:2019-05-19 02:00:39.079    thread:main level:DEBUG message:Good Morning !!
date:2019-05-19 02:00:39.094    thread:main level:DEBUG message:@AfterReturning           : void com.example.spring.web.service.GreetingService.goodMorning() ret: null

AfterThrowing Adviceの実装

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

@AfterThrowing(pointcut = "execution(* *..*Service.*(..))", throwing = "t")
public void failureEndLog(JoinPoint jp, Throwable t) {
  logger.debug("@AfterThrowing            : {}", jp.getSignature() + " t: " + t);
}
コンソール
date:2019-05-19 02:03:34.661    thread:main level:DEBUG message:Failing !!
date:2019-05-19 02:03:34.664    thread:main level:DEBUG message:@AfterThrowing            : void com.example.spring.web.service.GreetingService.failing() t: java.lang.UnsupportedOperationException
date:2019-05-19 02:03:34.681    thread:main level:DEBUG message:Good Morning !!

After Adviceの実装

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

@After("execution(* *..*Service.*(..))")
public void completeLog(JoinPoint jp) {
  logger.debug("@After                    : {}", jp.getSignature());
}
コンソール
date:2019-05-19 02:04:58.247    thread:main level:DEBUG message:Failing !!
date:2019-05-19 02:04:58.249    thread:main level:DEBUG message:@After                    : void com.example.spring.web.service.GreetingService.failing()
date:2019-05-19 02:04:58.257    thread:main level:DEBUG message:Good Morning !!
date:2019-05-19 02:04:58.258    thread:main level:DEBUG message:@After                    : void com.example.spring.web.service.GreetingService.goodMorning()

Around Advice

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

@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;
}
コンソール(正常時)
date:2019-05-19 02:07:26.916    thread:main level:DEBUG message:Before by @Around         : void com.example.spring.web.service.GreetingService.goodMorning()
date:2019-05-19 02:07:26.918    thread:main level:DEBUG message:Good Morning !!
date:2019-05-19 02:07:26.919    thread:main level:DEBUG message:AfterReturning by @Around : void com.example.spring.web.service.GreetingService.goodMorning() ret:null
date:2019-05-19 02:07:26.920    thread:main level:DEBUG message:After by @Around          : void com.example.spring.web.service.GreetingService.goodMorning()
コンソール(例外発生時)
date:2019-05-19 02:09:24.318    thread:main level:DEBUG message:Before by @Around         : void com.example.spring.web.service.GreetingService.failing()
date:2019-05-19 02:09:24.334    thread:main level:DEBUG message:Failing !!
date:2019-05-19 02:09:24.336    thread:main level:DEBUG message:AfterThrowing by @Around  : void com.example.spring.web.service.GreetingService.failing()
java.lang.UnsupportedOperationException: null
    at com.example.spring.web.service.GreetingService.failing(GreetingService.java:22)
...
date:2019-05-19 02:09:24.337    thread:main level:DEBUG message:After by @Around          : void com.example.spring.web.service.GreetingService.failing()

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の公式リファレンスを参照してください。

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

ここでは、Spring Boot Starter Webをベースに、Spring Boot固有の仕組みや実装方法について説明します。
Spring Bootは、Spring-Boot向けの開発プロジェクトを生成するためのWeb Service「SPRING INITIALIZR」を提供しています。プロジェクトの作成方法は、こちらをご覧ください(なお、Dependenciesは「Web」に、バージョンは最新バージョンに読み替えてください)。

javax.servlet.ServletRequestListener

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

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

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

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

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

自作のServletRequestListenerの登録

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

@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(OrderedCharacterEncodingFilter) /*
HiddenHttpMethodFilter(OrderedHiddenHttpMethodFilter) /*
FormContentFilter(OrderedFormContentFilter) /*
RequestContextFilter(OrderedRequestContextFilter) /*

以前のバージョンではログで登録されたFilterが確認できたのですが・・・最新のSpring Bootだとログに出力されなくなった模様です。なので・・・今回は、以下のようなコードを追加してDIコンテナおよびサービレットコンテナに登録されたFilterをダンプして確認してみました。

@SpringBootApplication
public class WebDemoSpringbootApplication implements ServletContextAware {

  public static void main(String[] args) {
    SpringApplication.run(WebDemoSpringbootApplication.class, args);
  }

  @Autowired // DIコンテナに登録されているFilterクラスの確認
  public void dumpFilters(List<Filter> filters) {
    System.out.println("--- filters in DI container ----");
    filters.forEach(System.out::println);
  }

  @Override // サーブレットコンテナに登録されているFilterクラスの確認
  public void setServletContext(ServletContext servletContext) {
    System.out.println("--- filters in servlet context ----");
    servletContext.getFilterRegistrations().values().forEach(r ->
        System.out.println(String.format("name:%s url-mappings:%s servlet-mappings:%s",
            r.getName(), r.getUrlPatternMappings(), r.getServletNameMappings())));
  }

}
コンソール
...
--- filters in DI container ----
org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter@35764bef
org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter@43b40233
org.springframework.boot.web.servlet.filter.OrderedFormContentFilter@2184b4f4
org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter@5477a1ca
--- filters in servlet context ----
name:requestContextFilter url-mappings:[/*] servlet-mappings:[]
name:Tomcat WebSocket (JSR356) Filter url-mappings:[/*] servlet-mappings:[]
name:hiddenHttpMethodFilter url-mappings:[/*] servlet-mappings:[]
name:characterEncodingFilter url-mappings:[/*] servlet-mappings:[]
name:formContentFilter url-mappings:[/*] servlet-mappings:[]
...

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

javax.servlet.Filterの登録

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

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

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

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

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

@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を使用することもできます。

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

自作のFilterの登録

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

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

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

CharacterEncodingFilterのカスタマイズ

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

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

Spring AOP

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

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

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

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

Proxy生成にCGLIBの利用を強制

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

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

まとめ

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

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

参考サイト

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away