💬

アスペクト指向プログラミングとAspectJ

2023/07/17に公開

アスペクト指向プログラミング

SpringBoot などを利用していれば意図せずに利用しているアスペクト指向プログラミング。
今回は少々深掘りつつ、アスペクト指向プログラミングの代表的なライブラリ AspectJ 関連の仕組みを記載します。

アスペクト指向プログラミングとは?

wiki の抜粋ですが

  • 横断的関心を実装する手法によって、プログラムのモジュール性を高めることを目的にしている
  • オブジェクト指向ではうまく分離できない特徴(クラス間を横断するような機能)を「アスペクト」とみなす
  • それをアスペクト記述言語を用いて分離して記述する
  • するとプログラムに柔軟性が持たされる
    ということらしいです。

上記でいう「クラス間を横断するような機能」は一般的にはロギングとかトランザクション制御とかでしょうか。
それらを「アスペクト」として切り出して、例えば DDD でいうところのドメインモデルと分離してプログラミングできると、確かに柔軟性を得られるかなと思います。

AspectJ

Java でアスペクト指向プログラミングを実現するならば、大抵の場合は AspectJ を利用するのではないでしょうか。
SpringBoot も内部的には AspectJ を利用しているはずです。

AspectJ の仕組みと用語

AspectJ

ポイントカットで選択されたジョインポイントにアドバイスが挿入される

ことで、AOP を実現しています。

アドバイス

Advice(アドバイス) は横断的に利用したい「処理」そのものです。メソッドとして記述することが多いと思います。

ポイントカット

Pointcut(ポイントカット) は Advice を適用する条件です。
AspectJ では式で適用する条件を記述します。

ジョインポイント

JoinPoint(ジョインポイント) は Advice を適用するタイミングや場所のことです。
例えば、メソッド開始前や終了後などです。

実際のソースコード

Advice, Pointcut, JoinPoint を合わせて Aspect として記述します。
記述方法は2種類あります。

  • class ではなく aspect として記述する
  • Java の通常のクラスにアノテーションを付与して記述する

最近はほとんどが後者の方法かと思いますので、後者の簡単なコードを記述してみます。

@Aspect
public class Aspect {
    @Before("execution(* hoge.*.*(..))")
	public void before() {
		System.out.println("before!");
	}
}

クラスに @Aspect アノテーションを付与し、メソッド beforeAdvice として実装します。
この before メソッドに @Before アノテーションを付与することで、JoinPoint の役割を果たすことができます。

@Before の引数には Pointcut 式を定義します。
execution という特有の記述方法で記載します。

  • execution(メソッド修飾子 メソッド戻り値 パッケージ名.クラス名.メソッド名(引数の型, 引数の型) throws 例外)がフォーマットです。この式に該当するメソッドが実行される際にこのアスペクトが動作します。

Aspect はどのように適用されるのか?

ライブラリの効果により手順通りに記述すれば AOP は簡単に実現できます。
しかし、あまり気にする人は少ないかと思いますが、どの様に Aspect が対象のメソッドの実行前や実行後にもれなく動作するのでしょうか?

Weaving

Aspect を適用することをウィービング(Weaving)と言います。本来の業務処理に横断的な処理を「編み込む」ということです。
このウィービングという作業を、いつ、どの様にやっているのか。それが Aspect の適用方法になります。

compile-time weaving

コンパイル時に対象のコードに Aspect を編み込む方法です。対象の業務クラスとAspectを合わせた状態で class ファイルを生成します。

post-compile weaving

コンパイル済みの業務コード(classファイル) にAspectを編み込む方法です。
これは AOP 無しでビルド済みの jar ファイルに後から Aspect を編み込むことが可能な方法です。

load-time weaving

クラスファイルを JVM がロードする際に Aspect を編み込む方式です。
実際の方法は上記の2種類と変わりないですが、タイミングが java プロセス起動時のクラスロードまで延期されただけです。

SpringBoot で利用している Weaving

上述の 3 種類の Weaving はタイミングが異なるだけで、基本的な方式は一緒です。
しかし、SpringBoot が実施している方式はこれらとは異なります。

SprintBoot は起動時に Spring コンテナに Bean を登録すると思いますが、その際に「編み込まれる側のクラスのサブクラスを動的に生成して、そのインスタンスを登録する」ことで、Weaving を実現しています。

つまり、基本的な weaving とは違い class ファイルに直接手を加えるわけではなく、サブクラス(これを Proxy と言います) を生成することで、AOP を実現しています。

そのため、SpringBoot の標準 AOP で Aspect を編み込むならば、対象のクラスは Spring の Bean として登録される必要があります。
@Component が付与されているクラスが対象ということです。

Proxy の役割

サブクラスとして動的に生成される Proxy は、実際の処理の前後に Aspect の Advice 処理を呼び出します。
例として @Before として定義した Advice を呼び出すシーケンスを載せます。

上記を見ると「なぜ @Transactional なメソッドは自クラスから呼び出すとトランザクション制御されないのか」が分かると思います。
それは、SpringBoot での Aspect 処理は Proxy を経由して始めて有効になるためです。
自クラス内でのメソッド呼び出しは Proxy を経由しないため、Aspect が実行されません。

Discussion