Spring Boot で AOP のサンプルプログラムを作ってみた。

Spring Boot で AOP( Aspect Oriented Programming ) のサンプルプログラムを作ってみました。コマンドラインから実行して文字列を画面に標準出力するだけのものですが、AOPによりメソッドの前後で標準出力が追加されることを確認しました。

以下がサンプルプログラムの実行例です。

AOPをしていない状態のときのものです(実行結果①とします)。

処理 : Hello, World. と出力しています。

AOPをしてメソッドの前後で標準出力を追加したときのものです(実行家結果②とします)。

処理 : Hello, World. の前後に 開始 と 終了 の出力が追加されています。

Spring Boot のプロジェクトは Spring Initializr で生成しました。Spring Initializr のURLは https://start.spring.io/ です。

Group、Artifact、Name は以下としました。

Group : com.example
Artifact : demo12
Name : demo12

ビルドは Gradle です。プロジェクトをダウンロードします。

今回の Spring Boot のサンプルプログラムでは、標準出力を行うサービス、および、AOPを行うコンポーネントを作成します。

Applicationクラス
Demo12Application.java

サービス
CommandService.java
CommandServiceImpl.java

AOPのコンポーネント
CommonLog.java

プロジェクト内のファイルの構成は、このようになります。赤字が追加・修正をするファイルです。

Applicationクラスの Demo12Application.java の説明からします。

Demo12Application.java は Spring Initializr により最初から生成されていますが、これを以下のように書き換えます(テキストファイルの画像ですのでソースは最後に載せておきます)。

Demo12Application は ApplicationRunner の実装をしています。ApplicationRunner のrunメソッドをオーバーライドすることによりコマンドラインの引数を取得します。引数は ApplicationArguments が保持しています。

runメソッドではサービスのメソッドを呼び出しています。画面への標準出力の処理はサービスのほうに記載しています。サービスは @Autowired でフィールドインジェクションをしています。

サンプルプログラムでは setBannerMode をオフにして Spring Boot のバナー表示を抑止しています。

サービスの説明です。

サービスはインターフェースの CommandService.java と、それを実装した CommandServiceImpl.java を用意します。サービスのパッケージは別にしているため service フォルダを作成して、その中にこれらのファイルを格納します。

CommandService.java のソースです。

display01( )、および、display02( ) というメソッドを定義しています。ApplicationクラスがサービスをDIして使うようにしているためインターフェイスを用意しています。

CommandServiceImpl.java のソースです。CommandService の実装をします。

Service アノテーションを付けています。これにより CommandServiceImpl がDIコンテナに格納されます。各メソッドでは文字列の標準出力を行っています。

AOPのコンポーネントを説明する前にAOPなしの状態で動かしてみます。

build.gradle の dependencies に太字部分を追加します。


dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-aop'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Spring Boot を起動します。

$ ./gradlew bootRun

最初に示した「実行結果①」になります。もしうまくいかない場合は --refresh-dependencies を付けて実行してみてください。

AOPで標準出力を追加します。aop フォルダを作成して、その中に CommonLog.java を新規で作成します。

CommonLog.java のソースです。

Aspect アノテーションと Component アノテーションを付けています。

アスペクト( Aspect )とはプログラムの複数の場所に散在する共通処理をまとめたものです。アスペクトという単位でモジュール化したものらしいですが意味がわからないので「まとめたもの」と書いています。AOPをするクラス(共通処理を行うクラス)に Aspect アノテーション付けておくことでアスペクトとして機能するようです。またこの共通処理のことをアドバイス( Advice )と呼んでいます。アスペクトによって実行されるアクションがアドバイスということです。

Component アノテーションを付けることでアスペクトがDIコンテナに格納されます。

Before アノテーション と AfterReturning アノテーションですが、これはAOPの実行タイミングを指定するものです。AOPの実行タイミングには以下のものがあります。

  • @Before・・・対象メソッドが行われる前に実行する。
  • @After・・・対象メソッドが行われた後に実行する(例外発生の有無に関わらない)。
  • @AfterReturning・・・対象メソッドが正常終了した場合に実行する。
  • @AfterThrowing・・・対象メソッドに例外が発生した場合に実行する。
  • @Around・・・対象メソッドの前後で実行する。

beforeLog( ) には @Before を付けていますので beforeLog( ) が特定の処理の前に実行されます。同様に afterLog( ) には @AfterReturning が付いていますので特定の処理の後に afterLog( ) が実行されます。

では特定の処理とは何かと言うと execution で指定しているメソッドが該当します。サンプルでは以下です。execution で指定するメソッドにはワイルドカードが使えます。

* com.example.demo12.service.*.*(..)

  • アスタリスク ( * ) ・・・任意の1つの文字列にマッチする。
  • ドットドット ( .. ) ・・・任意の0個以上の文字にマッチする。

先頭のアスタリスクは戻り値が任意ということです。対象のクラス、メソッドは com.example.demo12.service.*.* であるため service パッケージに含まれる任意のクラス、メソッドになります。最後の ( .. ) はメソッドの引数を表しています。

サンプルでは service パッケージに CommandServiceImpl があり、このクラスには display01( )、および、display02( ) のメソッドがあります。display01( )、display02( ) とも execution の指定にマッチしますので beforeLog( ) と afterLog( ) が指定のタイミングで実行されます。

なお、このAOPされるメソッド( execution で指定されたメソッド )のことをポイントカットと呼んでいます。

beforeLog( ) と afterLog( ) の引数に JoinPoint というクラスがありますが、これはポイントカットでAOPされた実際のクラス、メソッドの情報です。outputLog( ) では JoinPoint を使ってクラス、および、メソッド名を取得しています。

それではもう一度、gradlew bootRun で Spring Boot を起動してみます。今度の結果は「実行結果②」になります。

AOPのコンポーネントである CommonLog.java を追加したのですが、既存のソースには何も手を加えないで処理の追加ができたことがわかります。AOPはログの出力をする際に役に立ちます。

実行結果①、②ですが、bootRun での実行であるため引数を受け取れていません。jarファイルを作成してコマンドラインに引数を渡します。

ビルドをします。

$ ./gradlew build

jarファイルが /demo12/build/libs 配下にできます。jarファイルを実行します。引数に --SpringBoot を付与するかどうかによって実行結果が変わります。

$ java -jar demo12-0.0.1-SNAPSHOT.jar

$ java -jar demo12-0.0.1-SNAPSHOT.jar --SpringBoot

サンプルプログラムのソースを以下に記載しておきます。

Applicationクラス


package com.example.demo12;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.ApplicationArguments;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.demo12.service.CommandService;

@SpringBootApplication
public class Demo12Application implements ApplicationRunner {

	@Autowired
	CommandService service;

	public static void main(String[] args) {

		SpringApplication app = new SpringApplication(Demo12Application.class);
		app.setBannerMode(Mode.OFF);
		app.run(args);

	}

	@Override
	public void run( ApplicationArguments args ) {

		if ( args.containsOption("SpringBoot") )
			service.display01();
		else
			service.display02();

	}

}

サービス


package com.example.demo12.service;

public interface CommandService {

	void display01();
	void display02();

}

package com.example.demo12.service;

import org.springframework.stereotype.Service;

@Service
public class CommandServiceImpl implements CommandService {

	public void display01() {
		System.out.println("処理 : Hello, Spring Boot.");
	}

	public void display02() {
		System.out.println("処理 : Hello, World.");
	}

}

AOPのコンポーネント


package com.example.demo12.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.JoinPoint;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class CommonLog {

	@Before( "execution( * com.example.demo12.service.*.*(..) )" )
	public void beforeLog( JoinPoint jp ) {
		outputLog("開始", jp);
	}

	@AfterReturning( "execution( * com.example.demo12.service.*.*(..) )" )
	public void afterLog( JoinPoint jp ) {
		outputLog("終了", jp);
	}

	private void outputLog( String str, JoinPoint jp ) {

		String className = jp.getTarget().getClass().getSimpleName();
		String methodName = jp.getSignature().getName();
		System.out.println( str + " : " + className + "." +methodName + "()" );

	}

}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です