@Pointcut とポイントカット式

ポイントカットは、アドバイスをどの Bean のどのメンバーに適用するかを表す式です。@Pointcut はその式に名前を付けます。

なぜ必要か

すべてのアドバイスのアノテーションはポイントカットを受け取ります(例: @Before('execution(* OrderService.*(..))'))。式は名前、型、アノテーション、Bean 名でメソッドを選びます。複数のアドバイスが同じ式を使う場合は、@Pointcut で一度だけ書き、名前で参照します。

式を一度しか使わない場合、@Pointcut は不要です。アドバイスのアノテーションに直接書いてください。

ポイントカットに名前を付ける

アスペクトの空のメソッドに @Pointcut を付けます。メソッド本体が呼ばれることはありません。同じアスペクトの他のポイントカットからは name() で参照します。別のアスペクトからは AspectClass.name() と書きます。

@Aspect()
@Component()
@Log()
class AuditAspect {
  // Every OrderService member, named once.
  @Pointcut('within(OrderService)')
  void orders() {}

  @Before('orders()')
  void before(JoinPoint call) => log.info('calling ${call.toShortString()}');

  // Combine a named pointcut with other designators.
  @After('orders() && !execution(* *.slow*(..))')
  void after() => log.info('done');
}

@Aspect()
@Component()
@Log()
class MetricsAspect {
  // AuditAspect's pointcut, reused here with the class name.
  @After('AuditAspect.orders()')
  void count() => log.info('order call');
}

アスペクトのスーパークラスや mixin の名前付きポイントカットは継承されます。

名前 型 既定値 意味
expression String 必須 メソッド名が表すポイントカット式。

式の構文

ポイントカットは、1 つ以上の指示子を演算子で組み合わせたものです。

designator(pattern)  [&& | || | !  designator(pattern) ...]

演算子: && または and、|| または or、! または not、および括弧。

指示子

指示子 一致するもの 例
execution([modifiers] ret [Type.]name(params)) 戻り値の型、宣言型、名前、パラメータで選ぶメソッド execution(* OrderService.find*(..))
within(TypePattern) 本体が一致するクラスにあるすべてのメンバー。オーバーライドされていない継承メンバーはスーパークラス内とみなされます。Type+ はサブタイプにも一致します within(*Repository)、within(Repository+)
target(Type)、this(Type) その型またはサブタイプ(インターフェースを含む)の Bean のすべてのメンバー target(PaymentGateway)
args(types) 引数がこれらの型を持つメソッド。宣言されたパラメータ型、なければ実行時の引数の型(.. は任意の個数) args(String, ..)
@args(Annotation, ..) 引数のクラスがそのアノテーションを持つメソッド @args(Audited)
bean(namePattern) その名前、エイリアス、または修飾子の値を持つ Bean のすべてのメンバー bean(order*)
@annotation(Annotation) そのアノテーションを持つメソッド。実装しているインターフェースメソッドに付いている場合も含みます @annotation(Timed)
@within(Annotation) 本体がそのアノテーションを持つクラスにあるすべてのメンバー @within(Service)
@target(Annotation) クラスがそのアノテーションを持つ Bean のすべてのメンバー @target(Service)
name()、Type.name() 同じアスペクト、またはクラス Type の名前付きポイントカット orders()、AuditAspect.orders()

それ以外の指示子(cflow、if など)は "unknown designator" エラーになります。

パターン

execution が一致しないもの

execution はメソッドにだけ一致します。明示的なものも public フィールドの暗黙のものも、ゲッターとセッターには一致しません。それらには within、bean、target、@within、@target、@annotation を使います。

メンバーがオーバーライドしているスーパークラスのメソッドにだけ X が付いている場合、@annotation(X) は一致しません。これは AspectJ と同じです。

例

ポイントカット アドバイス対象
execution(* *(..)) すべての Bean のすべてのメソッド
within(OrderService) OrderService のすべて(ゲッターとセッターを含む)
execution(* OrderService.*(..)) OrderService のメソッド
execution(* find*(..)) 任意の Bean の、名前が find で始まるメソッド
execution(Future *(..)) Future を返すメソッド
execution(void *(String, ..)) 最初のパラメータが String の void メソッド
target(PaymentGateway) PaymentGateway を実装するすべての Bean
bean(order*) 名前が order で始まる Bean
@annotation(Timed) @Timed() が付いたメソッド
@within(Service) @Service クラスのすべてのメンバー
within(*Service) && !execution(* get*(..)) サービス。ただし get... メソッドを除く
orders() || bean(payments) このアスペクトの名前付きポイントカット、または payments Bean

アドバイスのパラメータへの値の束縛

@annotation(x)、@within(x)、@target(x)、@args(x)、this(x)、target(x)、args(x, ..) の中の小文字で始まる識別子は、アドバイスメソッドのパラメータを指します。そのパラメータには、アノテーションのオブジェクト、プロキシ、Bean、または引数が渡されます。

大文字で始まる名前は型です。int、double、num、bool、dynamic、void も型として扱われます。

// Receives the @Timed annotation object and the first argument.
@Around('@annotation(timed) && args(isbn, ..)')
Object? time(ProceedingJoinPoint joinPoint, Timed timed, String isbn) {
  log.info('timing ${joinPoint.signature.name} for $isbn');
  return joinPoint.proceed();
}

束縛された値はパラメータの型を持つ必要があり、そうでなければその呼び出しは一致しません。束縛や引数の検査を含むポイントカットは呼び出しごとに再検査されます。それ以外は起動時に一度だけ判定されます。

ポイントカットが束縛しないパラメータや、パラメータのない束縛名はビルドエラーになります。

コードでのポイントカット

アスペクトの外では、ExpressionPointcut が同じ構文を受け付けます。DefaultPointcutAdvisor Bean の中で使います。不正な式は FormatException を投げます。

// LoggingInterceptor is your own MethodInterceptor (see JoinPoint).
@Configuration()
class AopConfig {
  @Bean()
  PointcutAdvisor serviceLogging() => DefaultPointcutAdvisor(
    ExpressionPointcut(
      'service() && !execution(* *.slow*(..))',
      // Named pointcuts are passed as a map here.
      namedPointcuts: {'service': 'within(*Service) || @within(Service)'},
    ),
    LoggingInterceptor(),
  );
}

その他の既製のポイントカット(すべて AopPointcut を実装):

クラス 一致するもの
TruePointcut() すべての Bean のすべてのメンバー
NameMatchMethodPointcut(names) 名前が * パターンのいずれかに一致するメンバー。ゲッターとセッターも含みます
AnnotationMatchingPointcut<A>({onClass = false}) A が付いたメンバー。onClass: true なら A が付いたクラスのすべてのメンバー
ComposablePointcut([pointcut]) ビルダー。union(other) は「または」、intersection(other) は「かつ」を追加します
ExpressionPointcut(expression, {namedPointcuts, ...}) 上記の式の構文

ルールを Dart で書くほうが簡単な場合は、AopPointcut を自分で実装します。唯一のメソッド bool matches(MethodSignature signature, String beanName) は、呼び出しごとではなく、起動時にプロキシ化される各 Bean のメンバーごとに一度実行されます。

// Advises the read-only members: find* methods and getters.
final class ReadOnlyPointcut implements AopPointcut {
  const ReadOnlyPointcut();

  @override
  bool matches(MethodSignature signature, String beanName) =>
      signature.name.startsWith('find') ||
      signature.kind == MemberKind.getter;
}

Spring (Java) では

@Aspect
@Component
class AuditAspect {
  @Pointcut("within(com.example.OrderService)")
  void orders() {}

  @Before("orders()")
  void before(JoinPoint call) { ... }

  @Before("@annotation(timed) && args(isbn, ..)")
  void timed(Timed timed, String isbn) { ... }
}

重要な違い:

関連項目