スキーマベースの AOP サポート
XML ベースのフォーマットを希望する場合、Spring は aop
名前空間タグを使用してアスペクトを定義するためのサポートも提供します。@AspectJ スタイルを使用する場合とまったく同じポイントカット式とアドバイスの種類がサポートされています。このセクションでは、その構文に焦点を当て、前のセクション(@AspectJ サポート)の説明を参照して、ポイントカット式の記述とアドバイスパラメーターのバインディングについて理解します。
このセクションで説明されている aop 名前空間タグを使用するには、XML スキーマベースの構成に従って、spring-aop
スキーマをインポートする必要があります。aop
名前空間にタグをインポートする方法については、AOP スキーマを参照してください。
Spring 構成内では、すべてのアスペクトおよびアドバイザー要素を <aop:config>
要素内に配置する必要があります(アプリケーションコンテキスト構成内に複数の <aop:config>
要素を含めることができます)。<aop:config>
要素には、ポイントカット、アドバイザー、アスペクト要素を含めることができます(これらはこの順序で宣言する必要があることに注意してください)。
<aop:config> スタイルの構成では、Spring の自動プロキシメカニズムを多用します。すでに BeanNameAutoProxyCreator などを使用して明示的な自動プロキシを使用している場合、これにより課題 (アドバイスが織り込まれないなど) が発生する可能性があります。推奨される使用パターンは、<aop:config> スタイルのみまたは AutoProxyCreator スタイルのみを使用し、これらを決して混合しないことです。 |
アスペクトを宣言する
スキーマサポートを使用する場合、アスペクトは Spring アプリケーションコンテキストで Bean として定義された通常の Java オブジェクトです。状態と動作はオブジェクトのフィールドとメソッドにキャプチャーされ、ポイントカットとアドバイス情報は XML にキャプチャーされます。
次の例に示すように、<aop:aspect>
要素を使用してアスペクトを宣言し、ref
属性を使用してバッキング Bean を参照できます。
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
もちろん、他の Spring Bean と同様に、アスペクト(この場合は aBean
)をサポートする Bean を構成し、依存関係を注入できます。
ポイントカットの宣言
<aop:config>
要素内で名前付きポイントカットを宣言して、ポイントカット定義をいくつかのアスペクトとアドバイザで共有できます。
サービス層でのビジネスサービスの実行を表すポイントカットは、次のように定義できます。
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
ポイントカット式自体は、@AspectJ サポートで説明されているのと同じ AspectJ ポイントカット式言語を使用することに注意してください。スキーマベースの宣言スタイルを使用する場合、ポイントカット式内の @Aspect
型で定義された名前付きポイントカットを参照することもできます。上記のポイントカットを定義する別の方法は次のようになります。
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> (1)
</aop:config>
1 | 名前付きポイントカット定義の共有で定義された businessService という名前のポイントカットを参照します。 |
次の例に示すように、アスペクト内でポイントカットを宣言することは、トップレベルのポイントカットを宣言することと非常によく似ています。
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
@AspectJ アスペクトとほぼ同じ方法で、スキーマベースの定義スタイルを使用して宣言されたポイントカットは、ジョインポイントコンテキストを収集できます。例: 次のポイントカットは、this
オブジェクトをジョインポイントコンテキストとして収集し、アドバイスに渡します。
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
次のように、一致する名前のパラメーターを含めることにより、収集されたジョインポイントコンテキストを受け取るようにアドバイスを宣言する必要があります。
Java
Kotlin
public void monitor(Object service) {
// ...
}
fun monitor(service: Any) {
// ...
}
ポイントカットの部分式を組み合わせる場合、&&
は XML ドキュメント内で扱いにくいため、&&
、||
、!
の代わりに and
、or
、not
キーワードをそれぞれ使用できます。例: 前のポイントカットは、次のように書く方が適切です。
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
この方法で定義されたポイントカットは、XML id
によって参照され、複合ポイントカットを形成するための名前付きポイントカットとして使用できないことに注意してください。スキーマベースの定義スタイルでの名前付きポイントカットのサポートは、@AspectJ スタイルで提供されるものよりも制限されています。
アドバイスを宣言する
スキーマベースの AOP サポートは、@AspectJ スタイルと同じ 5 種類のアドバイスを使用します。これらのセマンティクスはまったく同じです。
Before アドバイス
Before アドバイスは、一致したメソッドが実行される前に実行されます。次の例に示すように、<aop:before>
要素を使用して <aop:aspect>
内で宣言されます。
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
上記の例では、dataAccessOperation
は、最上位 (<aop:config>
) レベルで定義された名前付きポイントカットの id
です ( ポイントカットの宣言を参照)。
@AspectJ スタイルの説明で記述されていたように、名前付きポイントカットを使用すると、コードの可読性が大幅に向上します。詳細については、名前付きポイントカット定義の共有を参照してください。 |
代わりにポイントカットをインラインで定義するには、次のように pointcut-ref
属性を pointcut
属性に置き換えます。
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method
属性は、アドバイスの本文を提供するメソッド(doAccessCheck
)を識別します。このメソッドは、アドバイスを含むアスペクト要素によって参照される Bean に対して定義する必要があります。データアクセス操作が実行される前に(ポイントカット式と一致するメソッド実行ジョインポイント)、アスペクト Bean の doAccessCheck
メソッドが呼び出されます。
After Returning アドバイス
After returning アドバイスは、一致したメソッドの実行が正常に完了すると実行されます。これは、アドバイスの前と同じ方法で <aop:aspect>
内で宣言されます。次の例は、宣言方法を示しています。
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
@AspectJ スタイルと同様に、アドバイス本文内で戻り値を取得できます。これを行うには、次の例に示すように、returning
属性を使用して、戻り値が渡されるパラメーターの名前を指定します。
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck
メソッドは、retVal
という名前のパラメーターを宣言する必要があります。このパラメーターの型は、@AfterReturning
について説明したのと同じ方法でマッチングを制限します。例: 次のようにメソッドシグネチャーを宣言できます。
Java
Kotlin
public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...
After Throwing アドバイス
After throwing アドバイスは、一致したメソッドの実行が例外をスローして終了したときに実行されます。次の例に示すように、after-throwing
要素を使用して <aop:aspect>
内で宣言されます。
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doRecoveryActions"/>
...
</aop:aspect>
@AspectJ スタイルと同様に、スローされた例外をアドバイス本文内で取得できます。これを行うには、次の例に示すように、throwing
属性を使用して、例外を渡す必要があるパラメーターの名前を指定します。
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions
メソッドは、dataAccessEx
という名前のパラメーターを宣言する必要があります。このパラメーターの型は、@AfterThrowing
について説明したのと同じ方法でマッチングを制限します。例: メソッドシグネチャーは次のように宣言できます。
Java
Kotlin
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
After (Finally) アドバイス
After (finally) アドバイスは、一致したメソッドの実行がどのように終了しても実行されます。次の例に示すように、after
要素を使用して宣言できます。
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
Around アドバイス
最後の種類のアドバイスはアドバイスに関するものです。Around アドバイスは、一致したメソッドの実行の「周り」で実行されます。メソッドの実行前と実行後の両方で作業を行い、いつ、どのように、メソッドが実際に実行されるかどうかを判断する機会があります。Around アドバイスは、メソッドの実行の前後でスレッドセーフな方法で状態を共有する必要がある場合によく使用されます。たとえば、タイマーの開始と停止などです。
要件を満たす最も強力でない形式のアドバイスを常に使用してください。 例: アドバイスがあなたのニーズに十分である場合は、アドバイスの周囲を使用しないでください。 |
aop:around
要素を使用してアドバイスを宣言できます。Advice メソッドはその戻り型として Object
を宣言する必要があり、メソッドの最初のパラメーターは ProceedingJoinPoint
型である必要があります。アドバイスメソッドの本体内で、基になるメソッドを実行するために、ProceedingJoinPoint
で proceed()
を呼び出す必要があります。引数なしで proceed()
を呼び出すと、呼び出し元の元の引数が、呼び出されたときに基になるメソッドに提供されます。高度なユースケースでは、引数の配列(Object[]
)を受け入れる proceed()
メソッドのオーバーロードされたバリアントがあります。配列内の値は、呼び出されたときに基になるメソッドへの引数として使用されます。Object[]
を使用して proceed
を呼び出す際の注意事項については、Around アドバイスを参照してください。
次の例は、XML でアドバイスを宣言する方法を示しています。
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling
アドバイスの実装は、次の例に示すように、@AspectJ の例とまったく同じにすることができます(もちろん、アノテーションを除く)。
Java
Kotlin
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return pjp.proceed()
}
アドバイスパラメーター
スキーマベースの宣言スタイルは、@AspectJ サポートで説明したのと同じ方法で、アドバイスメソッドのパラメーターと名前でポイントカットパラメーターを照合することによって、完全に型付けされたアドバイスをサポートします。詳細は、アドバイスパラメーターを参照してください。アドバイスメソッド (前述の検出戦略に頼らない) の引数名を明示的に指定する場合は、アドバイス要素の arg-names
属性を使用します。この属性は、アドバイスアノテーション ( 引数名の決定の説明) の argNames
属性と同じ方法で処理されます。次の例では、XML で引数名を指定する方法を示します。
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
method="audit"
arg-names="auditable" />
1 | ポイントカット式の組み合わせで定義された publicMethod という名前のポイントカットを参照します。 |
arg-names
属性は、パラメーター名のコンマ区切りリストを受け入れます。
次の XSD ベースのアプローチのやや複雑な例は、いくつかの厳密に型指定されたパラメーターと組み合わせて使用されるアドバイスを示しています。
Java
Kotlin
package com.xyz.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
package com.xyz.service
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
次はアスペクトです。profile(..)
メソッドは、多くの厳密に型指定されたパラメーターを受け入れることに注意してください。最初のパラメーターは、メソッド呼び出しを進めるために使用されるジョインポイントです。このパラメーターの存在は、次の例が示すように、profile(..)
が around
アドバイスとして使用されることを示しています。
Java
Kotlin
package com.xyz;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
package com.xyz
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch
class SimpleProfiler {
fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any? {
val clock = StopWatch("Profiling for '$name' and '$age'")
try {
clock.start(call.toShortString())
return call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
}
}
最後に、次の XML 構成の例は、特定のジョインポイントに対する前述のアドバイスの実行に影響します。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="com.xyz.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="com.xyz.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* com.xyz.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
次のドライバースクリプトを検討してください。
Java
Kotlin
public class Boot {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
PersonService person = ctx.getBean(PersonService.class);
person.getPerson("Pengo", 12);
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
val person = ctx.getBean(PersonService.class)
person.getPerson("Pengo", 12)
}
このような Boot
クラスを使用すると、標準出力で次のような出力が得られます。
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
アドバイスのオーダー
複数のアドバイスを同じジョインポイント(実行メソッド)で実行する必要がある場合、順序付けルールはアドバイスのオーダーで説明されています。アスペクト間の優先順位は、<aop:aspect>
要素の order
属性を介して、またはアスペクトをサポートする Bean に @Order
アノテーションを追加するか、Bean に Ordered
インターフェースを実装させることによって決定されます。
同じ 例: 同じジョインポイントに適用される同じ 一般的な経験則として、同じジョインポイントに適用される同じ |
導入
はじめに(AspectJ で型間宣言として知られる)アスペクトは、アドバイスされたオブジェクトが特定のインターフェースを実装し、それらのオブジェクトに代わってそのインターフェースの実装を提供することを宣言させます。
aop:aspect
内で aop:declare-parents
要素を使用して、導入を行うことができます。aop:declare-parents
エレメントを使用して、一致する型に新しい親があることを宣言できます(そのため名前があります)。例: UsageTracked
という名前のインターフェースと DefaultUsageTracked
という名前のインターフェースの実装を指定すると、次のアスペクトは、サービスインターフェースのすべての実装者も UsageTracked
インターフェースを実装することを宣言します。(たとえば、JMX を通じて統計を公開するため。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xyz.service.*+"
implement-interface="com.xyz.service.tracking.UsageTracked"
default-impl="com.xyz.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="execution(* com.xyz..service.*.*(..))
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
usageTracking
Bean を支援するクラスには、次のメソッドが含まれます。
Java
Kotlin
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
実装されるインターフェースは、implement-interface
属性によって決定されます。types-matching
属性の値は、AspectJ 型のパターンです。一致する型の Bean は、UsageTracked
インターフェースを実装します。前の例の前のアドバイスでは、サービス Bean を UsageTracked
インターフェースの実装として直接使用できることに注意してください。Bean にプログラムでアクセスするには、次のように記述できます。
Java
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
アスペクトのインスタンス化モデル
スキーマ定義のアスペクトでサポートされるインスタンス化モデルは、シングルトンモデルのみです。他のインスタンス化モデルは、将来のリリースでサポートされる可能性があります。
アドバイザー
「アドバイザー」の概念は、Spring で定義された AOP サポートに由来し、AspectJ には直接同等のものはありません。アドバイザーは、1 つのアドバイスを含む小さな自己完結型のアスペクトのようなものです。アドバイス自体は Bean によって表され、Spring のアドバイス型で説明されているアドバイスインターフェースの 1 つを実装する必要があります。アドバイザは、AspectJ ポイントカット式を利用できます。
Spring は、<aop:advisor>
エレメントでアドバイザーの概念をサポートします。最も一般的には、Spring で独自のネームスペースをサポートしているトランザクションアドバイスと組み合わせて使用されます。次の例はアドバイザーを示しています。
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice" />
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
前の例で使用した pointcut-ref
属性と同様に、pointcut
属性を使用して、ポイントカット式をインラインで定義することもできます。
アドバイスが順序付けに参加できるようにアドバイザーの優先順位を定義するには、order
属性を使用してアドバイザーの Ordered
値を定義します。
AOP スキーマの例
このセクションでは、AOP の例からの同時ロック失敗の再試行の例が、スキーマサポートで書き換えられたときの様子を示します。
ビジネスサービスの実行は、同時実行性の課題のために失敗することがあります(たとえば、デッドロックの敗者)。操作が再試行された場合、次の試行で成功する可能性があります。このような条件で再試行することが適切なビジネスサービス(競合解決のためにユーザーに戻る必要のないべき等操作)の場合、クライアントが PessimisticLockingFailureException
を認識しないように透過的に操作を再試行します。これは、サービスレイヤーの複数のサービスに明確に適用される要件であるため、アスペクトを介した実装に最適です。
操作を再試行するため、proceed
を複数回呼び出せるように、around advice を使用する必要があります。次のリストは、基本的なアスペクトの実装を示しています(これは、スキーマサポートを使用する通常の Java クラスです)。
Java
Kotlin
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}
アスペクトは Ordered
インターフェースを実装するため、アスペクトの優先順位をトランザクションアドバイスより高く設定できることに注意してください(再試行するたびに新しいトランザクションが必要です)。maxRetries
および order
プロパティは両方とも Spring によって構成されます。主なアクションは、doConcurrentOperation
アラウンドアドバイスメソッドで発生します。続行しようとします。PessimisticLockingFailureException
で失敗した場合は、すべての再試行を使い果たしていない限り、再試行します。
このクラスは、@AspectJ の例で使用したものと同じですが、アノテーションが削除されています。 |
対応する Spring 構成は次のとおりです。
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
当面は、すべてのビジネスサービスがべき等であると想定していることに注意してください。そうでない場合は、次の例に示すように、Idempotent
アノテーションを導入し、アノテーションを使用してサービスオペレーションの実装にアノテーションを付けることにより、真にべき等のオペレーションのみを再試行するようにアスペクトを調整できます。
Java
Kotlin
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent
べき等操作のみを再試行するアスペクトの変更には、次のように、@Idempotent
操作のみが一致するようにポイントカット式を改善することが含まれます。
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>