こんにちは!
23卒新卒エンジニアのkamaです!
前回は、知識ベースだけではなく、ハンズオン形式で、DIの実装の仕方を学んでいきました。
今回は、前回作ったプログラムに、AOPで関数実行前後に文字列を表示させるようにします。
AOPに関して補足
#2でAOPについて説明しましたが、今回本を読んでいたら、いろいろと知らなかった事が書いてあったので、それを補足として書いていきます。
AOPのおさらいですが、AOPはアスペクト指向プログラミング
のことです。
各プログラムで、メインになる機能とは別に、どのプログラムでも適宜必要になる、ログ出力、データアクセス等のサブ機能を、別のところで用意しといて、その都度必要なところに渡してあげる、というものでした。
AOPの説明自体はこれで問題ないのですが、実際にAOPをプログラムに起こすうえで必要になる知識がいくつかあるので、紹介です。
AOPの用語
まず、AOPを実装するにしても、それに用いる用語がわかっていないと、意味が分からなくなってしまうと思うので、覚えときましょう!
用語 | 内容 |
---|---|
Advice | 横断的関心事の実装(メソッド)のこと。ログ出力や、トランザクション管理をする関数を指す |
Aspect | Adviceをまとめたもの(クラス)のこと。 |
JoinPoint | Adviceをメイン処理のどこに適用するかというタイミング、関数実行前後が基本 |
PointCut | Adviceを挿入できる場所。例えば、関数名にgetが含まれる時、など処理する条件の定義ができる |
Interceptor | 処理の制御を横取り(インターセプト)するための仕組み。SpringFrameworkでは、このインターセプトという仕組みで、Adviceをメイン処理に追加したように見せる |
Target | Adviceが挿入される対象のこと |
特に、インターセプタがどういう働きをしているのかは、知っておいたほうが頭がこんがらがりにくいと思います。
インターセプタの働き
Adviceの種類
SpringFrameworkには、Adviceを実行するために利用できるJoinPointが5種類あります。
Advice | 内容 | アノテーション |
---|---|---|
Before | メイン処理が実行される前に実行する | @Before |
After | メイン処理が正常に終了した後で実行する | @After |
AfterReturning | メイン処理で例外がスローされた後で実行する | @AfterReturning |
AfterThrowing | メイン処理の実行結果を問わず終了後に実行する | @AfterThrowning |
Around | メイン処理の実行の前後で実行される | @Around |
Pointcut式
先の用語の部分でAdviceを挿入できる場所
、という説明をしたPointCutですが、それを実装するために、Pointcut式というものを利用します。
Pointcut式というものを使うと、Adviceを適用するスクリプトを指定することができるようになります。
Pointcut式には、記法が複数あり、今回はexecution指示子というものを使った解説をしていきます。
execution(戻り値の型 パッケージ名.クラス名.関数名(引数))
Pointcut式にはワイルドカードを使うことができる
ワイルドカード | 効果 |
---|---|
* | 任意の文字列を表す。パッケージにつければ任意のパッケージ1階層分、引数に置けば1つ以上の引数を指す |
.. | 0個以上のパッケージを指す。引数に置くと、0個以上の引数を指す |
+ | クラス名の後に記述し、クラスとサブクラスや実装クラスすべてを表す。 |
EX.他のPointcut式指示子
指示子 | 解説 |
---|---|
execution | 関数の型や修飾子、戻り値やクラス、関数名、引数など、様々な要素から、特定の関数を対象にすることができる。 |
execution(戻り値の型 パッケージ名.クラス名.関数名(引数)) | |
within | 特定のパッケージまたは、クラス内のすべての関数を対象にする |
within(パッケージ名..*) | |
target | 特定のクラスまたは、インターフェースを対象にする |
target(クラス名) | |
annotation | 特定のアノテーションが付与されている関数を対象にする |
@annotation (アノテーション名) |
|
aegs | 特定の引数を持つメソッドを対象にする |
args(引数) |
いざ実践!!
前回使用したPrefecturesOfJapanプロジェクトを使用して進めていきます!
パッケージの作成
src/main/java
を右クリックしてcom.example.demo.aop
というパッケージを作成しましょう。
build.gradleの修正
現状まだAOPを利用するために必要な依存関係が追加されていないので、追加しましょう!
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-aop'//追加
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
AOPクラス作成
Adviceの作成を行うためのAOPクラスを作成します。
名前はSampleAspect
とします。
クラスの作成方法は前回と同様で、aopパッケージに作成します。
作成出来たら、SampleAspectクラスに@Aspect
と@Component
アノテーションを与えます。
package com.example.demo.aop;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SampleAspect {
}
Advice作成
ここで、やっとAdviceの作成に取り掛かることができます!
ただ、ここで焦って誤字をすると、かな~~~~りめんどくさいエラーが出て苦しむことになる(敗北済み)ので、ちゃんと丁寧にやっていきましょう!
まずは、@Before
アノテーションが付与されたクラスを作成するのですが、ここで注意!!
@Before
アノテーションは、引数としてPointcut式を記入する必要があります。
このPointcut式は、すべて入力アシスタントなしで入力することになるので、うち間違えてしまうと大変なことになります。
//この部分危険!!
@Before("execution(* com.example.demo.used.*Printer.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
}
続いて、Adviceの中身を書いていきましょう。
基本的には、自分で実行したいことを書けばOKです。
引数となっているJoinPoint型に.getSignature()
を使うと、呼び出した関数の情報を取得できます。
@Before("execution(* com.example.demo.used.*Printer.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("\n" + new SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date()));
System.out.println(String.format("メソッド名:%s", joinPoint.getSignature().getName()));
System.out.println("-----------------------");
}
ここでもう1つ注意しないといけないのが、JoinPointに関するimportです。
このJoinPoint
なんですが、似たものにJoinpoint
というものがあります。
PointのPがpになっただけのものですが、別のパッケージをimportしてしまうと、getSignature()
などが正常に動かないので注意が必要です。
//正しいもの
import org.aspectj.lang.JoinPoint;
//間違っているもの
import org.aopalliance.intercept.Joinpoint;
ちなみに、BeforeとAfter、After Returning、After Throwingは構文が変わらないので、割愛します。
実行
ここまで書ければ、あとは手を加える必要がありません!
びっくりするぐらい簡単に実装できましたね!!
2023/05/24
メソッド名:prefecturePrint
-----------------------
東京です
2023/05/24
メソッド名:prefecturePrint
-----------------------
大阪やで
@Before
を@After
に変更するとこうなります。
東京です
2023/05/24
メソッド名:prefecturePrint
-----------------------
大阪やで
2023/05/24
メソッド名:prefecturePrint
-----------------------
AroundAdviceの書き方
最後に、AroundAdviceのコードだけ置いておきます!
AroundAdviceだけは構文が少し違うので注意!!
@Around("execution(* com.example.demo.used.*Printer.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//実行前処理
System.out.println("私が住んでいるのは");
//メイン処理を実行する
Object result = joinPoint.proceed();
//実行後処理
System.out.println("------------------");
//戻り値を返す必要があれば、object型で返す。
return result;
}
結果
私が住んでいるのは
東京です
------------------
私が住んでいるのは
大阪やで
------------------
あとがき
DIと比べると、やることも少なくて、簡単に実装できましたね。
私は、これを最初に見た際、めちゃくちゃ便利じゃん!!と思いました。
Unityでゲーム作ってた頃は1つのコードに数十個のDebug.log();を書くことがざらだったので...
メインのコードに一切手を加えることなく、外からデバッグ用のログ出力をできるのはめちゃくちゃいいですね!!