アドバイス

アドバイスは、ポイントカットに一致した各呼び出しの決まったタイミング(前、後、または代わり)で実行されるアスペクトのメソッドです。

なぜ必要か

アドバイスのアノテーションは、コードがいつ実行されるかを決めます。目的を果たせる最も弱いものを選んでください。チェックには @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 を返すメンバー)の場合:

// 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');
  }
}

順序

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);
}

重要な違い:

関連項目