トッカンソフトウェア

SpringFrameworkでAOP(アスペクト指向プログラミング)(Javaアプリケーション)

今回はAOP(アスペクト指向プログラミング)をやります。これを使うことでメソッドの前後に処理を追加できます。


AOPを使用すると・・・

本来の動作とは関係ない処理(ログ出力など)をメソッドから抜き出し、AOPのメソッドに移動させることにより、
メソッドの中身がすっきりします。

イメージ

元のexecuteメソッド内には開始、終了のログ出力処理が入っていますが、これは本来の動作とは関係ない処理
なので別メソッドにしてしまいます。AOPを使用することでexecuteメソッドが呼ばれたときにbeforeメソッド、
afterメソッドが自動的に呼ばれます。

呼び出し元はexecuteメソッドを呼ぶだけでAOP使用前、後で処理の修正が必要なく、executeメソッド内に
beforeメソッド、afterメソッドを呼ぶ処理を記述する必要はありません。

AOPを使用することによりまったく別の場所にbeforeメソッド、afterメソッドを書くことが出来ます。

まずは動かしてみましょう


pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>springTest</groupId>
	<artifactId>springTest</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<build>
		<sourceDirectory>src</sourceDirectory>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<release>17</release>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>5.3.22</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>5.3.22</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aspects</artifactId>
			<version>5.3.22</version>
		</dependency>
	</dependencies>
</project>
				
過去に追加したものはシンプルにするため消しました。今回用にspring-aspectsを追加しています。


Spring用の設定ファイル(SpringTest.xml)
				
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-4.2.xsd
	http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

	<bean id="helloWorld" class="spring.test.SpringBean">
	</bean>

	<context:component-scan base-package="spring.test" />

	<aop:aspectj-autoproxy />

</beans>

			
過去に追加したものはシンプルにするため消しました。今回用にaspectj-autoproxyを追加してます。


実行対象となるJavaクラス(HelloWorldTest.java)
				
package spring.test;

import org.springframework.context.support.FileSystemXmlApplicationContext;

public class HelloWorldTest {
	public static void main(String[] args) {
		FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("C:\\workspace\\springApp\\SpringTest.xml");
//		FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("classpath:SpringTest.xml");

		SpringBean bean = (SpringBean) context.getBean("helloWorld");
		bean.show("Test");

		context.close();
	}
}


			
このソースも一部変えました。DIしてから"Test"を引数に指定してshowメソッドを読んでいるだけです。


設定ファイルよりSpringがデータをセットするBeanクラス(SpringBean.java)
				
package spring.test;

public class SpringBean {

	public String show(String msg) {
		System.out.println("Hello World!!" + msg);
		return "Called";
	}
}


			
引数に文字列を追加してコンソールに出力するだけの処理をしています。


AOPの処理を行うクラス(SpringAspect.java)
				
package spring.test;

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

@Aspect
@Component
public class SpringAspect {

	@Before("execution(* spring..*(..))")
	private void before(JoinPoint jp) {
		System.out.println("Before:" + jp.getSignature().getName());
		Object[] args = jp.getArgs();
		for (Object o : args) {
			System.out.println("Before:" + o);
		}
	}

}


			
処理開始前に呼ばれる処理になります。@Beforeアノテーションの中で、どの条件で呼ばれるかの指定を行っています。


それでは実行してみましょう。

Before が付いているのがAOPのメソッドが出力しています。
"Hello World!!" が本来のメソッドで出力してます。



上の例では@Beforeだけをやりましたが、他にも@Around、@After、@AfterReturning などがあります。

アノテーション 説明
@Before メソッド実行前に呼ばれます
@After メソッド実行後に呼ばれます
@AfterReturning メソッド終了時に正常終了の場合、呼ばれます
@Around メソッド実行前に呼ばれ、この中でメソッドを実行します
@AfterThrowing 例外発生時に呼ばれます


演算子の指定もできます。
例:@Before("execution(* spring..*(..)) && !execution(* spring..show(..))")
演算子 説明
&& AND
|| OR
! 否定


実行対象となるJavaクラス(HelloWorldTest.java)
				
package spring.test;

import org.springframework.context.support.FileSystemXmlApplicationContext;

public class HelloWorldTest {
	public static void main(String[] args) {
		FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("C:\\workspace\\springApp\\SpringTest.xml");
//		FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("classpath:SpringTest.xml");

		SpringBean bean = (SpringBean) context.getBean("helloWorld");
		// 引数あり
		String ret = bean.show("Test");
		System.out.println("---------- " + ret + " ----------");

		// 引数なし
		bean.show();
		System.out.println("----------  ----------");

		// 例外発生
		bean.show(null);

		context.close();
	}
}


			
bean.showを3回実行させています。1回目は引数あり、2回目は引数なし、3回目は例外発生させています。


設定ファイルよりSpringがデータをセットするBeanクラス(SpringBean.java)
				
package spring.test;

public class SpringBean {

	public String show(String msg) {
		System.out.println("3_show msg Called ->" + msg.trim());
		return "show Return";
	}

	public void show() {
		System.out.println("3_show Called");
	}
}


			
trimメソッドを実行させることにより、引数がNullの場合、NullPointerExceptionを発生させています。


AOPの処理を行うクラス(SpringAspect.java)

package spring.test;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.CodeSignature;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SpringAspect {

	private void showJoinPoint(String from, JoinPoint jp) {

		String packageName = jp.getSignature().getDeclaringType().getPackage().getName();
		String className = jp.getSignature().getDeclaringType().getSimpleName();
		String method = jp.getSignature().getName();

		System.out.println(from + ":" + packageName + "." + className + "." + method);

		String[] paramNames = ((CodeSignature) jp.getSignature()).getParameterNames();
		Object[] args = jp.getArgs();

		for (int i = 0; i < paramNames.length; i++) {
			System.out.println(paramNames[i] + ":" + args[i]);
		}

	}

	@Before("execution(* spring..*(..))")
	private void before(JoinPoint jp) {
		showJoinPoint("2_Before", jp);
	}

	@Around("execution(* spring..*(..))")
	private String around(ProceedingJoinPoint pjp) throws Throwable {
		showJoinPoint("1_Around", pjp);
		String ret = (String) pjp.proceed();
		System.out.println("4_Around Retun:" + ret);
		return ret;
	}

	@After("execution(* spring..*(..))")
	private void after(JoinPoint jp) {
		showJoinPoint("5_After", jp);
	}

	@AfterReturning(value = "execution(* spring..*(..))", returning = "str")
	private void afterReturn(JoinPoint jp, String str) {
		showJoinPoint("6_AfterRet", jp);
		System.out.println("7_AfterRet:" + str);
	}

	@AfterThrowing(value = "execution(* spring..*(..))", throwing = "e")
	private void afterThrowing(Exception e) {
		System.out.println("8_AfterThrow:" + e.toString());
	}
}
				


実行結果


ポイントカットの指定

ポイントカットですが、staticメソッドは指定できません。
private、protestedはNGでpublicメソッドのみになります。

ワイルドカードの指定は、..はゼロ以上(ゼロもOK)で、*は対象1つ。

サンプルプログラムを少し変更してポイントカットの指定を試してみました。
				
package spring.test;

import org.springframework.context.support.FileSystemXmlApplicationContext;

public class HelloWorldTest {
	public static void main(String[] args) {
		FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("C:\\Users\\akira\\workspace2\\springTest\\SpringTest.xml");
//		FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("classpath:SpringTest.xml");

		SpringBean bean = (SpringBean) context.getBean("helloWorld");
		bean.show("hello","world");

		context.close();
	}
}

			
				
package spring.test;

public class SpringBean {

	public String show(String msg1, String msg2) {
		System.out.println("Hello World!!" + msg1 + msg2);
		return "Called";
	}
}

			

コメントで // OK となっているのが、ちゃんと動いたもので // NG は動かなかったものになります。
				
package spring.test;

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

@Aspect
@Component
public class SpringAspect {

	// OK @Before("execution(String spring..*(..))")
	// OK @Before("execution(* spring..*(..))")
	// OK @Before("execution(* *(..))")
	// OK @Before("execution(* *(*,*))")
	// OK @Before("execution(* show(*,*))")
	// OK @Before("execution(* *show(*,*))")
	// OK @Before("execution(* *..*show(*,*))")
	// OK @Before("execution(* *..show(*,*))")
	// OK @Before("execution(* *..*(*,*))")
	// NG @Before("execution(void spring..*(..))")
	// NG @Before("execution(*(..))")
	// NG @Before("execution(* (..))")
	// NG @Before("execution(* *(*))")
	// NG @Before("execution(* *())")
	// NG @Before("execution(* ..show(*,*))")
	// NG @Before("execution(* ..*show(*,*))")
	private void before(JoinPoint jp) {
		System.out.println("Before:" + jp.getSignature().getName());
		Object[] args = jp.getArgs();
		for (Object o : args) {
			System.out.println("Before:" + o);
		}
	}

}

			

ページのトップへ戻る