JoinPoint

JoinPoint は、横取りされた 1 回の呼び出し(どのメンバーが、どの Bean で、どの引数で呼ばれたか)を表します。

なぜ必要か

アドバイスは、何に対して実行されているかを知る必要がよくあります。ログ行を書く、メソッド名でメトリクスに名前を付ける、メソッドのアノテーションを読む、といった場合です。JoinPoint パラメータを宣言すると、プロキシがそれを渡します。@Around アドバイスは、呼び出しを実行することもできる ProceedingJoinPoint を受け取ります。

すべての呼び出しで同じことをするアドバイス(@After('orders()') void after() => ... など)では不要です。このパラメータは @Around 以外のすべてのアドバイスで省略できます。

JoinPoint

// Logs each call to the `calculator` bean with its arguments.
@Before('bean(calculator)')
void before(JoinPoint joinPoint) =>
    log.info('${joinPoint.toShortString()} with ${joinPoint.args}');
メンバー 型 意味
proxy Object 呼び出し側が呼んだプロキシ。他の Bean が保持しているオブジェクトです。生成されたサブクラスプロキシでは Bean 自身です。
target Object メンバーが実行される Bean。プロキシの背後にあるオブジェクトです。
signature MethodSignature 呼ばれたメンバー。名前、型、アノテーション。
args List<Object?> 引数。位置引数、次に名前付き引数の順です。コピーなので、変更しても呼び出しは変わりません。
toShortString() String ログ用の短い説明。例: execution(Greeter.greet(..))。

ProceedingJoinPoint

@Around アドバイスのジョインポイントです。メソッドが 1 つ加わります。

メンバー 型 意味
proceed([List<Object?>? args]) Object? 次のアドバイス、または最後のアドバイスの後ではメンバー自身を実行し、その結果を返します。args は引数を置き換え、呼び出しと同じ数の要素を持つ必要があります。違う場合は ArgumentError が投げられます。Future を返すメンバーでは、結果はその Future です。
// Keeps the second factor of `multiply` between 0 and 10.
@Around('execution(* Calculator.multiply(..))')
Object? clamp(ProceedingJoinPoint joinPoint) {
  final [a, b] = joinPoint.args;
  return joinPoint.proceed([a, (b! as int).clamp(0, 10)]);
}

MethodSignature

呼び出さずに分かる、アドバイス対象のメンバーの情報です。Dart には実行時リフレクションがないため、ジェネレーターがプロキシ化される各メンバーに定数を 1 つ書き出します。型名は型引数のない単純なクラス名です(Future<int> ではなく Future)。ビルド時に書き出されるので、minify や難読化されたビルドでも動作します。アプリケーションはこれを読むだけで、作成はしません。

// Logs lines like "int Calculator.add(int, int)".
@Before('execution(* Calculator.*(..))')
void before(JoinPoint joinPoint) {
  final signature = joinPoint.signature;
  log.info('${signature.returnType} ${signature.declaringType}.'
      '${signature.name}(${signature.parameterTypes.join(', ')})');
}
メンバー 型 意味
name String メンバーの名前。例: add。セッターの名前に = は付きません。
kind MemberKind method、getter、setter のいずれか。
declaringType String メンバーを宣言している最も抽象的な型。例: インターフェース Calculator。
declaringTypes List<String> メンバーを宣言している Bean のすべての型。
targetType String Bean のクラス。例: SimpleCalculator。
targetTypes List<String> Bean クラスとそのすべてのスーパータイプ。
returnType String 戻り値の型の名前。何も返さない場合は void、不明な場合は dynamic。
parameterTypes List<String> パラメータの型の名前。
parameterNames List<String> パラメータの名前。args と同じ順序です。
parameterAnnotations List<List<Object>> 各パラメータのアノテーション。
annotations List<Object> @annotation(...) が一致の対象とするメンバーのアノテーション。実行されるメソッドと、それが実装するインターフェースメソッドのものです。
mergedAnnotations List<Object> メンバーと、それがオーバーライドまたは実装するすべての宣言のアノテーション。具体的なものが先です。
targetAnnotations List<Object> Bean クラスのアノテーション。
implementingType String メンバーの本体があるクラス。Bean クラス、またはメンバーを継承しているスーパークラスや mixin。
isProtected bool メンバーに @protected が付いているかどうか。
toShortString() String 例: Greeter.greet(..)。

MemberKind

Dart ではゲッターとセッターは独立したメンバーで、public フィールドは暗黙のゲッター(final でなければセッターも)です。プロキシはそれらにもメソッドと同じようにアドバイスします。プロパティアクセスを別扱いしたい場合は signature.kind を読みます。

// Logs the calls to Account, but not the getter reads.
@Before('within(Account)')
void audit(JoinPoint joinPoint) {
  if (joinPoint.signature.kind != MemberKind.getter) {
    log.info(joinPoint.toShortString());
  }
}
値 意味
MemberKind.method メソッド。execution(...) が一致するのはこの種類だけです。
MemberKind.getter ゲッター。明示的なもの、または public フィールドの暗黙のもの。
MemberKind.setter セッター。明示的なもの、または final でない public フィールドの暗黙のもの。

MethodInterceptor と MethodInvocation

アドバイスはアスペクトのメソッドではなくクラスとして書くこともできます。MethodInterceptor を実装し、DefaultPointcutAdvisor Bean でポイントカットと組み合わせます。ライブラリのキャッシュやセキュリティチェックのような、再利用可能なアドバイスに使います。

その invoke メソッドは MethodInvocation を受け取ります。これは args が実際の引数そのものである ProceedingJoinPoint です。インターセプターは proceed() を呼ぶ前に、引数をその場で変更できます。

// Upper-cases every String the slogans bean returns.
class UpperCase implements MethodInterceptor {
  @override
  Object? invoke(MethodInvocation invocation) =>
      (invocation.proceed()! as String).toUpperCase();
}

@Configuration()
class AopConfig {
  @Bean()
  PointcutAdvisor upperCase() =>
      DefaultPointcutAdvisor(ExpressionPointcut('bean(slogans)'), UpperCase());
}
メンバー 型 意味
MethodInterceptor.invoke(MethodInvocation invocation) Object? 呼び出しの周囲で実行され、その結果を返します。続行するには invocation.proceed() を呼びます。呼ばなければメンバーは省略されます。
DefaultPointcutAdvisor(pointcut, advice, {order}) - ポイントカット、インターセプター、順序の組。order の既定値は Ordered.lowestPrecedence(最も内側)です。Ordered を実装しているため、その @Bean メソッドの @Order は無視されます。

Spring (Java) では

@Before("execution(* com.example.Calculator.*(..))")
void before(JoinPoint joinPoint) {
  MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  log.info(signature.getReturnType() + " " + signature.getName()
      + Arrays.toString(joinPoint.getArgs()));
}

重要な違い:

関連項目