割れた腹筋を目指して密かに筋トレを始めました小畑です。 (直近、ボディビル大会に出る予定はありません。)
先日の社内のSpring Boot研修でアスペクト指向プログラミングについて学びました。ぜひ活用できるようになりたいと思ったので、記憶整理を兼ねてブログを書いてみます。
AOP(アスペクト指向プログラミング)
AOP(Aspect Oriented Programming)とは簡単に説明をするとメソッドの開始・終了時のログを出力させる処理などを共通化して、コード内の至る所から呼び出して本質的機能と予備的機能を分離させる手法のことを指します。様々なところから呼び出されるという点から「横断的」という言葉がよく使用されます。
例えば、メソッドの実行時に実行開始の旨をログに出力させ、時刻変換処理を行い、メソッドの処理実行後に実行終了の旨をログに出力する処理があったとします。このメソッドの本質的機能は「時刻変換」であり、ログ出力は予備的機能にあたります。この予備的機能のログ出力をAOPを用いて分離することで、本質的機能のみをメソッドに持たせることが可能になります。
用語
AOPを学ぶ際に頻出した用語をまとめます。
- アスペクト: セキュリティ、ログ記録、トランザクション管理などの機能ごとにまとめたモジュール
- アドバイス: 特定の場所で実行されるコードや処理
- ジョインポイント: アドバイスを実行するタイミング、ポイント
- ポイントカット: 特定のメソッドや実行ポイント
- ポイントカット式: どのメソッドがアスペクトによって制御されるかを定義する式、条件
実装方法
AOPではアノテーションを利用して実装を行います。主なアノテーションを用いた実装方法についてまとめます。
@Aspect
アドバイスを保持するクラスには@Aspect
を付与します。
@Aspect public class LoggingAspect {
ポイントカット式には、beanや引数、アノテーションなどで指定する場合もありますが、今回は特定のメソッドを指定する場合に使用するexecutionのポイントカット式を使用します。
executionのポイントカット式として記載するフォーマットは下記の通りとなっています。
"execution(戻り値の型 パッケージ.クラス.メソッド(引数の型,引数の型))"
ワイルドカードには*
を利用します。
下記のコード例ではポイントカットは 下記の箇条書きの通りとなるので、com.example.demo.controller
パッケージ内のクラスの全てのメソッドに対してアスペクトを適用します。
@Before("execution(* com.example.demo.controller.*.*(..))")
ポイントカットの各項目内容
- 戻り値:
*
ワイルドカード - パッケージ名:com.example.demo.controller
- クラス名:
*
ワイルドカード - メソッド名:
*
ワイルドカード - 引数の型:すべての引数
@Before
ポイントカットで指定したメソッドの実行前にアドバイスを実行します。
下記の場合、com.example.demo.controller
パッケージ内のクラスのメソッドが実行される直前に「preMethodを呼んだよ」とコンソールログに出力されます。
@Before("execution(* com.example.demo.controller.*.*(..))") public void preMethod(JoinPoint jp) { log.info("preMethodを呼んだよ"); }
@After
ポイントカットで指定したメソッドの実行結果に関わらず、メソッドの実行後にアドバイスを実行します。
下記の場合、com.example.demo.controller
パッケージ内のクラスのメソッドが実行された直後に「afterMethodを呼んだよ」とコンソールログに出力されます。
@After("execution(* com.example.demo.controller.*.*(..))") public void afterMethod(JoinPoint jp) { log.info("AfterMethodを呼んだよ"); }
@Around
ポイントカットで指定したメソッドの実行前後を制御します。
下記の場合、com.example.demo.controller
パッケージ内のクラスのメソッドが実行される直前に「preAfterMethodを呼んだよ1」とコンソールログに出力され、メソッド実行後に「preAfterMethodを呼んだよ2」と出力されます。6行目の pjp.proceed();
で制御しているメソッドを呼び出し、メソッドを実行します。呼び出したメソッドの中で例外が発生した場合はtry,catchで受け取り、「例外発生」とコンソールログに出力します。
@Around("execution(* com.example.demo.controller.*.*(..))") public Object preAfterMethod(ProceedingJoinPoint pjp) throws Throwable { log.info("preAfterMethodを呼んだよ1"); Object result = new Object(); try { result = pjp.proceed(); //制御しているメソッドの実行 }catch(Throwable e){ log.info("例外発生"); throw e; }finally { log.info("preAfterMethodを呼んだよ2"); } return result; }
@AfterReturning
ポイントカットで指定したメソッドが正常に完了した後にアドバイスを実行します。
下記の場合、com.example.demo.controller
パッケージ内のクラスのメソッドが正常終了した直後に「afterReturningMethodを呼んだよ」とコンソールログに出力されます。対象のメソッドの戻り値がnullであれば「Method returned null.」と出力されます。
afterReturningMethod(JoinPoint jp, Object result)
の引数のObject result
パラメータで対象メソッドの戻り値を取得しています。
@AfterReturning(pointcut = "execution(* com.example.demo.controller.*.*(..))", returning = "result") public void afterReturningMethod(JoinPoint jp, Object result) { log.info("afterReturningMethodを呼んだよ"); if (result == null) { log.info("Method returned null."); } else { log.info("Method returned: " + result.toString()); } }
@AfterThrowing
ポイントカットで指定したメソッドが例外で脱出した後にアドバイスを実行します。 @AfterReturning
は対象のメソッドが正常終了した場合に実行されることに対し、@AfterThrowing
は例外発生時に実行されます。
下記の場合、com.example.demo.controller
パッケージ内のクラスのメソッドが例外を発生させた直後に発生させたエラー内容がコンソールログに出力されます。
@AfterThrowing( pointcut = "execution(* com.example.demo.controller.*.*(..))", throwing = "e") public void afterThrowing( JoinPoint jp, Throwable e) { log.error("MethodSignature:{} Message:{}",jp.getSignature(), e.getMessage()); }
@Aroundの利用例
下記は特定のメソッドの実行時間をコンソールログに出力する場合の例です。
制御するメソッド呼び出し直前に時間計測を始め、メソッドが終了すると時間計測を終了し、コンソールログに計測時間を出力します。呼びだしたメソッドで例外が発生した場合は、「例外発生」とコンソールログに出力し、時間計測を終了して、コンソールログに計測時間を出力します。
@Around("execution(* com.example.demo.controller.*.*(..))") public Object preAfterMethod(ProceedingJoinPoint pjp) throws Throwable { StopWatch time = new StopWatch(); // StopWatchクラスのオブジェクトをインスタンス化 time.start(); //時間計測スタート Object result = new Object(); try { result = pjp.proceed(); //制御しているメソッドの実行 }catch(Throwable e){ log.info("例外発生"); //制御しているメソッドの例外をcatch throw e; }finally { time.stop(); //時間計測終了 log.info(time.prettyPrint()); //計測した時間を出力 } return result; }
おわりに
ログ出力以外にも登録日や更新日のデータ設定に現在の日時をデータベースへ登録するなどの処理もAOPを利用することで処理を共通化させることもできます。
処理の共通化や機能の分離化をすることでコードの可読性を上げたり、修正漏れやコーディングミスなどの人為的ミスを防いだりすることに繋がるので、コーディングの選択肢のひとつとして利用できるようにしたいです。