今回は、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]
投稿から3年くらいたっても一定のViewが継続してあるので、最新のSpring(Spring Boot)バージョンの内容に更新しました。なお、今回の更新では、web.xml
は使わずにJava Configを使うサンプルを修正しています。
検証アプリケーション
GitHub上で公開するようにしました。
共通処理の実装方法
共通処理は以下のいずれかの方法で実装します。
実装方法 | 説明 |
---|---|
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メソッドの呼び出し前後に、共通処理を実行することができます。 |
javax.servlet.ServletRequestListener
Servlet標準のAPIであるjavax.servlet.ServletRequestListener
インタフェースを実装したクラスを作成します。
作成したクラスをサーブレットコンテナに登録すると、リクエスト開始時とリクエスト終了時のタイミングで任意の処理を実行することができます。
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
クラスをサーブレットコンテナに登録します。
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をサーブレットコンテナへ登録します。
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);
// リクエスト終了時の処理を行う。
// (実装は省略)
}
}
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: ベストプラクティスについて
この方法がベストプラクティスなのかはかなり微妙です(あまり自身なし・・)
後ほど紹介しますが、Spring Bootを使う場合は、単純にDIコンテナに登録するだけで自動でサーブレットコンテナに登録されます!!
Spring提供のServletRequestListenerクラス
Spring Web及びSpring MVC提供のServletRequestListener
クラスは以下のとおりです。アプリケーションの要件に応じて使用してください。
クラス | 説明 |
---|---|
RequestContextListener |
リクエスト(HttpServletRequest とHttpServletResponse )+リクエストのロケール情報をスレッドローカルに設定するためのServletRequestListener クラス。 |
javax.servlet.Filter
Servlet標準のAPIであるjavax.servlet.Filter
インタフェースを実装したクラスを作成します。
作成したクラスをサーブレットコンテナに登録すると、Servletの呼び出し前後に任意の処理を実行することができます。
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クラスをサーブレットコンテナに登録します。
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の処理を実行します。
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);
// アプリケーション終了時に行う処理を実装する
// (実装は省略)
}
}
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 |
FormContentFilter に置き換えることがアナウンスされています) |
HiddenHttpMethodFilter |
リクエストパラメータ(hiddenパラメータ)で指定されたHTTPメソッドに変換する(なりすます)ためのFilterクラス。 |
CharacterEncodingFilter |
リクエストとレスポンスの文字エンコーディングを指定するためのFilterクラス。 ※ Spring 4.3からエンコーディングの強制フラグがリクエストとレスポンス毎に指定可能になっている。 |
RequestContextFilter |
リクエスト(HttpServletRequest とHttpServletResponse )+リクエストのロケール情報をスレッドローカルに設定するための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 |
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メソッドの呼び出し前後に任意の処理を実行することができます。
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のフレームワーク機能に適用します。
@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のリファレンスにも記述されています。
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 をリクエストスコープに追加するためのクラス。 |
@RestControllerAdvice
/ @ControllerAdvice
@RestController
(@Controller
)を付与したクラスには、Handlerメソッド(@RequestMapping
を付与したメソッド)とは別に、Controller専用の特殊なメソッド(@InitBinder
メソッド、@ModelAttribute
メソッド、@ExceptionHandler
メソッド)を実装することができます。これらのメソッドを複数のControllerクラスで共有したい場合は、@RestControllerAdvice
(@ControllerAdvice
)の仕組みを使用します。
メソッド | 説明 |
---|---|
@InitBinder メソッド |
WebDataBinder オブジェクト(リクエストデータをJavaオブジェクトにバインドするためのオブジェクト)をカスタマイズするためのメソッド。型変換、フォーマッティング、バリデーションなどをカスタマイズすることができます。 |
@ModelAttribute メソッド |
Model にオブジェクトを格納するためのメソッド。Handlerメソッド実行前に呼び出され、返却したオブジェクトはModel に格納されます。 |
@ExceptionHandler メソッド |
例外をハンドリングするためのメソッド。 |
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
に追加したい場合に使えます。@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が実行されるタイミングを図で表すと以下のようになります。
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のみサポートしています。
Spring AOPのセットアップ
Spring AOPを使用する場合は、spring-aopモジュール(spring-webmvcが依存している)とaspectjweaverが必要になります。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</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.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コンテナと連携する仕組みを利用します。
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を利用するサンプルに変更しています。
テストケースを実行すると、コンソールに以下の内容が表示されます。
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 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>
なお、上記のコードをコンパイルするためには、以下のアーティファクトを依存ライブラリに追加する必要があります。
<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として認識されます。
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
を付与したメソッドに実装します。
@Before("execution(* *..*Service.*(..))")
public void startLog(JoinPoint jp) {
logger.debug("@Before : {}", jp.getSignature());
}
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
を付与したメソッドに実装します。
@AfterReturning(pointcut = "execution(* *..*Service.*(..))", returning = "ret")
public void normalEndLog(JoinPoint jp, Object ret) {
logger.debug("@AfterReturning : {}", jp.getSignature() + " ret: " + ret);
}
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
を付与したメソッドに実装します。
@AfterThrowing(pointcut = "execution(* *..*Service.*(..))", throwing = "t")
public void failureEndLog(JoinPoint jp, Throwable t) {
logger.debug("@AfterThrowing : {}", jp.getSignature() + " t: " + t);
}
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
を付与したメソッドに実装します。
@After("execution(* *..*Service.*(..))")
public void completeLog(JoinPoint jp) {
logger.debug("@After : {}", jp.getSignature());
}
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
を付与したメソッドに実装します。
@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: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の公式リファレンスを参照してください。
Spring Boot上での共通処理の実装
ここでは、Spring Boot Starter Webをベースに、Spring Boot固有の仕組みや実装方法について説明します。
Spring Bootは、Spring-Boot向けの開発プロジェクトを生成するためのWeb Service「SPRING INITIALIZR」を提供しています。
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 ) |
/* |
yes | - |
FormContentFilter ( OrderedFormContentFilter ) |
/* |
yes | - |
RequestContextFilter ( OrderedRequestContextFilter ) |
/* |
yes | - |
HiddenHttpMethodFilter ( OrderedHiddenHttpMethodFilter ) |
/* |
no | 2.2.0よりデフォルトで登録されなくなった |
NOTE:
HiddenHttpMethodFilter
は、Spring Boot提供のプロパティを使って有効化することができます。spring.mvc.hiddenmethod.filter.enabled=true
登録されたフィルターを確認するには、以下のようにログレベルを変更を変えてみてください。
logging.level.web=trace
...
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コンテナに登録します。
@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)の指定で変更することができます。
# 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」を指定すれば追加してくれます。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@EnableAspectJAutoProxy
はSpring Bootがデフォルトで適用してくれる仕組みになっています。
Spring Bootの自動コンフィギュレーションによって@EnableAspectJAutoProxy
を適用したくない場合は、以下のような設定を追加してください。
spring.aop.auto=false
Proxy生成にCGLIBの利用を強制
CGLIBの仕組みを使ってProxyを作ることを強制したい場合は、以下のような設定を追加してください。
spring.aop.proxy-target-class=true
まとめ
今回は、リクエスト処理内の任意のポイントで共通処理を実行する方法を紹介しました。
Filter
、HandlerInterceptor
、AOP(特にAOP)は共通処理を実装するのに強力な仕組みですが、乱用するとメンテナンス性を損なう可能性があることを意識しておいた方がよいでしょう。例えば、入力値(メソッド引数)や出力値(メソッドの返り値)を更新してしまうような共通処理(その共通処理がないと後続処理の動作がかわってしまうような処理)は原則さけるべきです。この手の処理を共通化したい場合は、共通処理を行うコンポーネントを作成し、明示的にアプリケーションコードの中からメソッドを呼び出すスタイルの方が私はよいと思います。
ちなみに、Spring AOPの説明は、全貌のほんの一部です。この投稿では、Spring AOPの雰囲気を掴んでもらえれば幸いです。Spring AOPについて詳しく知りたい方は、Spring Framework公式リファレンスをご覧ください。
また、「Servlet 3.0でサポートされた非同期処理利用時の共通処理の実装」については、後日「Spring MVC(+Spring Boot)上でのServlet標準の非同期処理を理解する(仮名)」の中で紹介できればと思っています。