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()));
}
重要な違い:
- ゲッターメソッドではなくプロパティです:
getSignature()、getArgs()ではなくjoinPoint.signature、joinPoint.args。 MethodSignatureは型名を文字列で持ちます。Classオブジェクトやjava.lang.reflect.Methodではありません。- Dart のゲッターとセッターは独立したメンバーなので
MemberKindがあります。Spring AOP はメソッドだけを横取りします。 proceedはObject?を返し、Throwableを宣言しません。