アドバイス
アドバイスは、ポイントカットに一致した各呼び出しの決まったタイミング(前、後、または代わり)で実行されるアスペクトのメソッドです。
なぜ必要か
アドバイスのアノテーションは、コードがいつ実行されるかを決めます。目的を果たせる最も弱いものを選んでください。チェックには @Before、後始末には @After、結果の参照には @AfterReturning、エラーの参照には @AfterThrowing を使います。@Around は、引数や結果を変える、呼び出しを省く、リトライする、時間を計るといった必要があるときだけ使います。
| アノテーション | 実行されるタイミング | パラメータ(注記がなければすべて省略可) | 結果を変えられるか |
|---|---|---|---|
@Before |
メソッドの前 | JoinPoint |
いいえ。例外を投げると呼び出しが止まります |
@After |
正常終了でも例外でも、メソッドの後(finally と同様) |
JoinPoint |
いいえ |
@AfterReturning |
正常に戻った後 | JoinPoint、次に結果 |
いいえ |
@AfterThrowing |
例外を投げた後。エラーはその後再スローされます | JoinPoint、次にエラー |
いいえ |
@Around |
メソッドの代わり。呼ぶかどうか、どう呼ぶかを決めます | ProceedingJoinPoint(必須) |
はい |
パラメータは位置引数です。JoinPoint と ProceedingJoinPoint は型が完全に一致するもので判定されます。ポイントカットが束縛するパラメータ(ポイントカット を参照)は名前で判定されます。それ以外のパラメータはビルドエラーになります。
以下の例はすべて、次のようなアスペクトクラスのメソッドです。
import 'dart:async';
import 'package:spring_core_dart/spring_core_dart.dart';
// Marker annotations used below.
class Timed {
const Timed();
}
class Retry {
const Retry();
}
@Aspect()
@Component()
@Log()
class OrderAudit {
OrderAudit(this.shop); // Shop is another bean of the application
final Shop shop;
@Pointcut('within(OrderService)')
void orders() {}
// advice methods go here
}
@Before
一致した各呼び出しの前に実行されます。メソッド本体の前に必要なチェックやログに使います。引数や結果は変えられません。例外を投げるとメソッドは実行されず、呼び出し側はそのエラーを受け取ります。
// Refuses orders while the shop is closed.
@Before('execution(* OrderService.place(..))')
void checkOpen(JoinPoint call) {
if (!shop.isOpen) {
throw StateError('shop closed, ${call.signature.name} refused');
}
}
| 名前 | 型 | 既定値 | 意味 |
|---|---|---|---|
pointcut |
String |
必須 | アドバイス対象のメソッドを選ぶポイントカット式。 |
@After
一致した各呼び出しの後に、正常終了でも例外でも、finally ブロックのように実行されます。後始末や「呼び出し完了」のログに使います。結果やエラーは変えられません。
// Logs the end of every OrderService call, even a failed one.
@After('orders()')
void after(JoinPoint call) => log.info('finished ${call.toShortString()}');
| 名前 | 型 | 既定値 | 意味 |
|---|---|---|---|
pointcut |
String |
必須 | アドバイス対象のメソッドを選ぶポイントカット式。 |
@AfterReturning
一致した呼び出しが正常に戻った後に実行され、戻り値を受け取ります。結果の確認やログに使います。結果を置き換えることはできません。置き換えるには @Around を使います。
returning で指定したパラメータが値を受け取ります。returning がない場合は、JoinPoint でもポイントカットが束縛するものでもない最初のパラメータが受け取ります。パラメータの型はフィルターとして働き、値がその型を持つときだけアドバイスが実行されます。
// Runs only when the result is a double; other results pass by.
@AfterReturning('orders()')
void returned(JoinPoint call, double total) => log.info('total: $total');
// `timed` is bound by the pointcut, so `returning` names the result.
@AfterReturning('@annotation(timed)', returning: 'result')
void timedResult(Timed timed, Object? result) => log.info('$timed: $result');
| 名前 | 型 | 既定値 | 意味 |
|---|---|---|---|
pointcut |
String |
必須 | アドバイス対象のメソッドを選ぶポイントカット式。 |
returning |
String? |
null |
戻り値を受け取るアドバイスのパラメータ。null の場合は、JoinPoint でもポイントカットが束縛するものでもない最初のパラメータ。 |
@AfterThrowing
一致した呼び出しが例外を投げた後に実行され、そのエラーを受け取ります。失敗のログやカウントに使います。エラーはその後必ず再スローされるので、呼び出し側にも届きます。
throwing で指定したパラメータ、なければ JoinPoint でも束縛されたものでもない最初のパラメータがエラーを受け取ります。その型はフィルターとして働き、その型のエラーに対してだけアドバイスが実行されます。
// Runs only for StateError; other errors pass by.
@AfterThrowing('orders()')
void failed(JoinPoint call, StateError error) =>
log.warning('${call.signature.name} failed: ${error.message}');
| 名前 | 型 | 既定値 | 意味 |
|---|---|---|---|
pointcut |
String |
必須 | アドバイス対象のメソッドを選ぶポイントカット式。 |
throwing |
String? |
null |
エラーを受け取るアドバイスのパラメータ。null の場合は、JoinPoint でもポイントカットが束縛するものでもない最初のパラメータ。 |
@Around
一致した各呼び出しの代わりに実行され、本来のメソッドを実行するかどうか、どう実行するかを決めます。時間の計測、引数の変更、結果の置き換え、リトライ、メソッドの省略ができます。
アドバイスは ProceedingJoinPoint を受け取り、呼び出しの結果(通常は proceed() の戻り値)を返します。proceed(args) は別の引数でメソッドを実行します。リストの要素数は呼び出しの引数の数と同じでなければならず、違う場合は ArgumentError が投げられます。proceed() を呼ばなければメソッドは省略されます。もう一度呼べばリトライになります。
// Keeps the discount between 0 and 50 percent.
@Around('execution(* OrderService.discount(..))')
Object? clamp(ProceedingJoinPoint call) {
final [percent] = call.args;
return call.proceed([(percent! as int).clamp(0, 50)]);
}
// Retries a flaky call up to three times.
@Around('@annotation(Retry)')
Object? retry(ProceedingJoinPoint call) {
for (var attempt = 1; ; attempt++) {
try {
return call.proceed();
} on TimeoutException {
if (attempt == 3) rethrow;
}
}
}
| 名前 | 型 | 既定値 | 意味 |
|---|---|---|---|
pointcut |
String |
必須 | アドバイス対象のメソッドを選ぶポイントカット式。 |
非同期メソッド
Future を返すメンバー(または値が Future である FutureOr を返すメンバー)の場合:
@After、@AfterReturning、@AfterThrowingは、メソッドが戻ったときではなく、Future が完了したときにその値またはエラーとともに実行されます。@Aroundでは、proceed()は Future を返します。それをそのまま返すか、アドバイスをasyncにしてawaitし、値を扱います。@Beforeやその他のアドバイスもasyncにできます。呼び出しはそれを待ってから進みます。- 同期メソッドに対して
asyncな@Before、@After、@AfterReturning、@AfterThrowingを使うと、呼び出しはAopInvocationExceptionで失敗します。アドバイスを同期にするか、メソッドがFutureを返すようにしてください。
// Times every method that returns a Future, until the future completes.
@Around('execution(Future *(..))')
Future<Object?> timeAsync(ProceedingJoinPoint call) async {
final watch = Stopwatch()..start();
try {
return await (call.proceed()! as Future<Object?>);
} finally {
log.info('${call.signature.name}: ${watch.elapsedMilliseconds} ms');
}
}
順序
- 複数のアスペクト: アスペクトクラスの
@Order(またはOrdered)で決まります。値が小さいほど外側で、その@Beforeが最初に、@Afterが最後に実行されます。順序のないアスペクトは最後になります。 - 1 つのアスペクトの中: around、before、after、afterReturning、afterThrowing の順です。同じ種類のアドバイスは宣言順を保ちます。
Spring (Java) では
@Before("execution(* com.example.OrderService.place(..))")
void checkOpen(JoinPoint call) { ... }
@AfterReturning(pointcut = "orders()", returning = "total")
void returned(JoinPoint call, double total) { ... }
@AfterThrowing(pointcut = "orders()", throwing = "error")
void failed(JoinPoint call, IllegalStateException error) { ... }
@Around("execution(* com.example.OrderService.discount(..))")
Object clamp(ProceedingJoinPoint call) throws Throwable {
Object[] args = call.getArgs();
args[0] = Math.min(Math.max((int) args[0], 0), 50);
return call.proceed(args);
}
重要な違い:
- ポイントカットは最初の位置引数です:
@AfterReturning('orders()', returning: 'total')。pointcut =とvalue =の使い分けはありません。 returningとthrowingは省略できます。省略すると、最初の空いているパラメータが値またはエラーを受け取ります。Futureを返すメソッドへのアドバイスは、Future の完了時に実行されます。Java にはこのような規則はありません。proceedはThrowableを宣言しません。エラーはそのまま伝わります。