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

  • 213
    いいね
  • 0
    コメント

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

動作検証バージョン

  • Spring Framework 4.3.3.RELEASE
  • Spring Boot 1.4.1.RELEASE

共通処理の実装方法

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

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

interceptorProcessing.png

javax.servlet.Filter.ServletRequestListener

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

package com.example.component;

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

public class CustomServletRequestListener implements ServletRequestListener {

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        // リクエスト開始時の処理を行う。
        // (実装は省略)
    }
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        // リクエスト終了時の処理を行う。
        // (実装は省略)
    }

}

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

src/main/webapp/WEB-INF/web.xml
<listener>
    <listener-class>com.example.component.CustomServletRequestListener</listener-class>
</listener>

Beanのインジェクション

ServletRequestListener内の処理でDIコンテナで管理しているBeanを利用したい場合は、ServletRequestListenerクラスをDIコンテナに登録し、org.springframework.web.context.ServletContextAware#setServletContext メソッドの中でServletContext#addListenerメソッドを呼び出すことで実現することができます。

@Component
public class CustomServletRequestListener implements ServletRequestListener , ServletContextAware {
    @Autowired
    MessageSource messageSource; // Beanをインジェクションする
    // ...
    @Override
    public void setServletContext(ServletContext servletContext) {
        servletContext.addListener(this);
    }
}

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

Spring提供のServletRequestListenerクラス

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

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

javax.servlet.Filter

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

package com.example.component;

import javax.servlet.*;
import java.io.IOException;

public class CustomFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初期化処理を行う。このメソッドはアプリケーション起動時に呼び出される。
        // サーブレットフィルタの初期化パラメータは引数のFilterConfigから取得できる。
        // (実装は省略)
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // ここに前処理を実装する
        // (実装は省略)
        // 後続処理(次のFilter又はServlet)を呼び出したくない場合は、このタイミングでメソッドを終了(return)すればよい。

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

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

実装したFilterクラスをサーブレットコンテナに登録し、Filterを適用するリクエストを<url-pattern>または<servle-name>タグを使用して指定します。

src/main/webapp/WEB-INF/web.xml
<filter>
    <filter-name>CustomFilter</filter-name>
    <filter-class>com.example.component.CustomFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CustomFilter</filter-name>
    <url-pattern>/*</url-pattern> <!-- リクエストパスが指定したパターンに一致するリクエストに対してのみFilterが適用されます。 -->
    <!-- <servlet-name>app</servlet-name> --> <!-- 指定したサーブレットにマッピングされたリクエストに対してのみFilterが適用されます。 -->
</filter-mapping>

デフォルトでは、Filterを適用するタイミングはリクエストを受けた時だけですが、このタイミングは変更することができます。指定可能なタイミングは以下の5つで、<dispatcher>タグを使用して指定します。

src/main/webapp/WEB-INF/web.xml
<filter-mapping>
    <filter-name>CustomFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher> <!-- 一致するパスやサーブレットへのリクエストを受けた時 -->
    <dispatcher>ASYNC</dispatcher> <!-- 一致するパスやサーブレットへの非同期リクエストを受けた時 -->
    <dispatcher>FORWARD</dispatcher> <!-- 一致するパスやサーブレットへフォワードした時 -->
    <dispatcher>INCLUDE</dispatcher> <!-- 一致するパスやサーブレットをインクルードした時 -->
    <dispatcher>ERROR</dispatcher> <!-- 一致するパスやサーブレットへエラー遷移した時 -->
</filter-mapping>

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

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

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

import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


public class CustomFilter extends OncePerRequestFilter {

    private int limit; // 名前が一致する初期化パラメータの値がバインドされる

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        this.limit = limit;
    }

    @Override
    protected void initFilterBean() throws ServletException {
        // 必要に応じて初期化処理を行う。このメソッドはアプリケーション起動時に呼び出される。
        // サーブレットフィルタの初期化パラメータは、このクラスのプロパティにバインドして取得することができる。
        // なお、getFilterConfig()を呼び出してFilterConfigを取得することも可能。
        // 初期化処理が不要な場合はオーバーライドも不要。
        // (実装は省略)
    }

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

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

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

    @Override
    public void destroy() {
        // 必要に応じて、アプリケーション終了時に行う処理を実装する。
        // 終了処理が不要な場合はオーバーライドも不要。
        // (実装は省略)
    }

}
src/main/webapp/WEB-INF/web.xml
<filter>
    <filter-name>CustomFilter</filter-name>
    <filter-class>com.example.component.CustomFilter</filter-class>
    <init-param> <!-- Filterクラスのプロパティにバインドされる -->
        <param-name>limit</param-name>
        <param-value>10</param-value>
    </init-param>
</filter>

Beanのインジェクション

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

public class CustomFilter implements Filter {
    @Autowired
    MessageSource messageSource; // Beanをインジェクションする
    // ...
}
@Configuration
public class AppConfig {
    @Bean
    public CustomFilter customFilter() { // FilterをBean登録する
        return new CustomFilter();
    }
}
src/main/webapp/WEB-INF/web.xml
<filter>
    <filter-name>customFilter</filter-name> <!-- FilterのBean名を指定する -->
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

なお、FilterのBeanは、org.springframework.web.context.ContextLoaderListener経由でDIコンテナに登録する必要があります。(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クラス。
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から追加

HandlerInterceptor

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

handlerInterceptor.png

package com.example.component;

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 {

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

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

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

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

}

Note: 冗長な空実装の排除
実際にHandlerInterceptorを作成する場合は、org.springframework.web.servlet.handler.HandlerInterceptorAdapterを継承して必要なメソッドだけ実装するようにしましょう。

public class CustomHandlerInterceptor extends HandlerInterceptorAdapter {
    // 必要なメソッドだけオーバーラードして実装する
    // ...
}

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

@Configuration
@EnableWebMvc // 注意:Spring Bootの場合は、@EnableWebMvcはつけちゃダメ!!
public class WebMvcConfig extends 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)上での静的リソースへのアクセスを理解する」をご覧ください。

@ControllerAdvice

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

メソッド 説明
@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) {
        // WebDataBinderのメソッドを呼び出してカスタマイズする
        dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    }

    @ModelAttribute("trackingId")
    public String addOneObject(@RequestHeader("X-Tracking-Id") Optional<String> 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";
    }

}

@ControllerAdviceの属性をすべて省略した場合は、すべての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>4.3.3.RELESAE</version> <!-- 投稿時の最新バージョン -->
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.9</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.springframework.stereotype.Service;

@Service
public class GreetingService {

    public void goodMorning() {
        System.out.println("Good Morning !!");
    }

    public void failing() {
        System.out.println("Failing !!");
        throw new UnsupportedOperationException();
    }

}

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

package com.example.component;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import springbook.component.GreetingService;
import springbook.config.AppConfig;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class GreetingServiceTest {

    @Autowired
    GreetingService greetingService;

    @Test
    public void goodMorning() {
        greetingService.goodMorning();
    }

    @Test(expected = UnsupportedOperationException.class)
    public void failing() {
        greetingService.failing();
    }

}

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

コンソール
Good Morning !!
Failing !!

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

pom.xml
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version> <!-- 投稿時の最新バージョン -->
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.3.3.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.springframework.stereotype.Component;

@Component
@Aspect
public class LoggingAspect {
}

Before Adviceの実装

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

@Before("execution(* *..*Service.*(..))")
public void startLog(JoinPoint jp) {
    System.out.println("@Before                   : " + jp.getSignature());
}
コンソール
@Before                   : void springbook.component.GreetingService.goodMorning()
Good Morning !!
Failing !!

AfterReturning Adviceの実装

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

@AfterReturning(value = "execution(* *..*Service.*(..))", returning = "ret")
public void normalEndLog(JoinPoint jp, Object ret) {
    System.out.println("@AfterReturning           : " + jp.getSignature() + " ret: " + ret);
}
コンソール
Good Morning !!
@AfterReturning           : void springbook.component.GreetingService.goodMorning() ret: null
Failing !!

AfterThrowing Adviceの実装

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

@AfterThrowing(value = "execution(* *..*Service.*(..))", throwing = "t")
public void failureEndLog(JoinPoint jp, Throwable t) {
    System.out.println("@AfterThrowing            : " + jp.getSignature() + " t: " + t);
}
コンソール
Failing !!
@AfterThrowing            : void springbook.component.GreetingService.failing() t: java.lang.UnsupportedOperationException

After Adviceの実装

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

@After("execution(* *..*Service.*(..))")
public void completeLog(JoinPoint jp) {
    System.out.println("@After                    : " + jp.getSignature());
}
コンソール
Good Morning !!
@After                    : void springbook.component.GreetingService.goodMorning()
Failing !!
@AfterThrowing            : void springbook.component.GreetingService.failing() t: java.lang.UnsupportedOperationException

Around Advice

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

@Around("execution(* *..*Service.*(..))")
public Object aroundLog(ProceedingJoinPoint jp) throws Throwable {
    Object ret;
    try {
        System.out.println("Before by @Around         : " + jp.getSignature());
        ret = jp.proceed();
        System.out.println("AfterReturning by @Around : " + jp.getSignature() + " ret: " + ret);
    }catch (Throwable t) {
        System.out.println("AfterThrowing by @Around  : " + jp.getSignature() + " t: " + t);
        throw t;
    } finally {
        System.out.println("After by @Around          : " + jp.getSignature());
    }
    return ret;
}
コンソール(正常時)
Before by @Around         : void springbook.component.GreetingService.goodMorning()
Good Morning !!
AfterReturning by @Around : void springbook.component.GreetingService.goodMorning() ret: null
After by @Around          : void springbook.component.GreetingService.goodMorning()
コンソール(例外発生時)
Before by @Around         : void springbook.component.GreetingService.failing()
Failing !!
AfterThrowing by @Around  : void springbook.component.GreetingService.failing() t: java.lang.UnsupportedOperationException
After by @Around          : void springbook.component.GreetingService.failing()

PointCut式

PointCut式は、Join Pointを選択する表現のことで、上記の例だとアノテーションのvalue属性に指定している「"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クラス。 ※ Spring Boot 1.4から追加

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

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

クラス マッピング
CharacterEncodingFilter /*
HiddenHttpMethodFilter /*
HttpPutFormContentFilter /*
RequestContextFilter /*

登録されたFilterはコンソールログで確認できます。

コンソール
...
2016-10-01 23:54:21.465  INFO 31029 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2016-10-01 23:54:21.465  INFO 31029 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2016-10-01 23:54:21.466  INFO 31029 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2016-10-01 23:54:21.466  INFO 31029 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
...

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 Boot 1.3まではリクエストとレスポンス両方に適用されていた
spring.http.encoding.force=false

# 
# 以下はSpring Boot 1.4+で利用可能
#

# リクエストへエンコーディングの適用を強制するかを指定(省略時は全体のデフォ値、全体のデフォ値の指定がない場合は(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を指定すれば解決できます。

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標準の非同期処理を理解する(仮名)」の中で紹介できればと思っています。

参考サイト