daisuzz.log

はじめてのSpring AOP

今回は、Spring AOPについて自分の理解の整理も兼ねてまとめていきたいと思います。 詳しい仕様が知りたい方は公式リファレンスを読んでください。 バージョンは、Spring Boot 2.0.1.RELEASE(Spring 5.0.5.RELEASE)です。

AOPとは

AOPとは、Aspect Oriented Programmingの略で、「アスペクト指向プログラミング」と呼ばれています。 AOPでは、クラスを横断した処理(例外処理やロギング、トランザクション処理など)をビジネスロジックから分離するようにコードを記述します。これによって、複数の処理をシンプルに記述できたり、同じ処理をおこなうコードが色々な場所で記述されていることを防ぐことができます。

Spring AOPでよくでてくる用語

  • Aspect
  • JoinPoint
  • PointCut
  • Advice

Aspect

横断的な処理とそれを実行させる場所を定義したモジュールを指します。 Springでは、@Aspectをクラスにつけることで、そのクラスがAspectとして認識されます。 AspectをBeanとして定義してあげる必要があるので、@ComponentをつけてBeanとして宣言することを忘れないようにしましょう。(@Componentが内部で宣言されている@Serviceや@RepositoryなどでもOKです。)

@Component
@Aspect
public class HogeAspect{

}

JoinPoint

JoinPointとは、横断的な処理を挿入する場所(メソッドの呼び出しや例外を発生する箇所)を指します。

PointCut

PointCutとは、JoinPointの集まりを表現したものです。 ソースコード上では、以下のコード例のようにAdvice(後述)のアノテーションのパラメータとしてPointCutを表現します。 ↓の例では、execution()でJoinPointを指定しています。

@After(value = "execution(* com.example.fuga..Fuga.*(..))")
public void Hoge(/* .. */){
  /* .. */
}
/* Springでは、他のパラメータがない場合、valueという名前のパラメータの「value =」は省略できるので、
@After("execution(* com.example.fuga..Fuga.*(..))") と書くこともできます。*/

execution()では以下のような文法でJoinPointが表現されます。

execution(戻り値 パッケージ名.クラス名.メソッド名(引数の型))
/*
「*」 -> 任意の文字列を表す。ただし、パッケージ名で使う場合は、「任意の名前の1パッケージ」を表し、引数で使う場合は「任意の型1つ」を表す。
「..」 -> パッケージ名で使う場合は、「0以上のパッケージ」を表し、引数で使う場合は「0以上の任意の型」を表す。
*/ 

最初にあげた例では、戻り値が任意の型で「com.example.fuga」パッケージ配下(サブパッケージ含む)にある「Fuga」というクラスのメソッド全てに対して@Arroundが適用されることを表現しています。

execution()以外にもwithin()やthis()などさまざまなものがありますが、execution()を使うことが多いです。参考

Advice

JoinPointで実行される横断的な処理そのものを表現します。 @Aspectをつけたクラスのメソッドにアノテーションをつけることで、そのメソッドをAdviceとして実行させることができます。 Adviceを表すアノテーションの種類は、以下の5つです。

@Before

対象のメソッドの処理前に実行されるAdviceです。

@Before("execution(* *..*(..))")
public void beforeHandler(){
/* .. */
}

@After

対象のメソッドの処理結果に関わらず、必ず実行されるAdviceです。

@After("execution(* *..*(..))")
public void afterHandler(){
/* .. */
}

@AfterReturning

対象のメソッドの処理が正しく終了した場合にのみ実行されるAdviceです。 returningパラメータで文字列を指定すると、その文字列の変数にJoinPointで実行された処理の返り値が格納されます。これによって返り値をAdviceで利用することができます。

@AfterReturning(value = "execution(* *..*(..))", returning = "r")
public void afterReturning(Object r){
  System.out.println(r);
}

@AfterThrowing

対象のメソッドの処理中に例外が発生した場合にのみ実行されるAdviceです。 throwingパラメータで文字列を指定すると、その文字列の変数にJoinPointで実行された処理で起きた例外が格納されます。これによって例外をAdviceで利用することができます。

@AfterThrowing(value = "execution(* *..*(..))", throwing = "e")
public void afterThrowing(Throwable e){
  System.out.println(e.message);
}

@Arround

対象のメソッドの処理前や処理後など好きなタイミングで実行できるAdviceです。 ↓の例のように、前処理と後処理を自由に設定することができます。

@Arround("execution(* *..*(..))")
public void arround(ProceedingJoinPoint pjp) throws Throwable {
  System.out.println("前処理")

  //メソッド実行
  Object result = pjp.proceed();

  System.out.println("後処理")
}

使い方

pom.xmlにライブラリを追加する

Spring Bootを利用している場合、pom.xmlのdependenciesに「spring-boot-starter-aop」を追加します。「spring-boot-starter-parent」をparentに指定している場合は、すでにバージョン情報が定義されているので、spring-boot-starter-aopのversionを指定する必要はありません。

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

Spring Bootを使っていない場合は、「spring-aop」と「aspectjweaver」をpom.xmlに追加して@EnableAspectJAutoProxyをJavaConfigにつけてあげれば利用できると思います(動作未確認)。

Aspectを作成する

Aspect用のクラスを作成して、クラスの先頭に@Aspectと@Componentをつけます。

@Component
@Aspect
public class HogeAspect{

}

Adviceを作成し、PointCutを記述する

Aspectとして定義したクラスにメソッドを作成します。 Adviceを実行するタイミングに応じて上で説明したAdviceのアノテーションを適宜つけてください。 さらに、作成したAdviceをどのクラスやメソッドに適用するかをAdviceのアノテーションの中にexecution()を使って記述します。

@Component
@Aspect
public class HogeAspect{
  
  @AfterThrowing(value = "execution(* *..*(..))", throwing = "ex")
  public void handler(Throwable ex){
    /* .. */
  }
}

↑の例では、全てのメソッドで発生した例外に対する例外処理を定義しています。

参考

https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/core.html#aop