@DeclareParents
@DeclareParents は、一致する Bean のプロキシに追加のインターフェースを実装させます。実装はアスペクトが用意します(「イントロダクション」)。
なぜ必要か
多くの Bean に、そのクラスが持たない共通の機能を持たせたいことがあります。たとえば、各サービスが報告できる利用回数カウンターです。@DeclareParents は、クラスを変更せずにそのインターフェースをプロキシに追加します。
クラスにインターフェースを直接追加できる場合は不要です。そのほうが読みやすく、実行時のコストもありません。イントロダクションはまれに使う道具です。
仕組み
@Aspect クラスの static フィールド に @DeclareParents を付けます。フィールドの型が追加するインターフェースです。型パターンに一致するクラスを持つすべての Bean のプロキシがそのインターフェースを実装し、そのメンバーを defaultImpl のインスタンスに転送します。インスタンスはプロキシごとに 1 つ、最初に使われたときに作られます。
import 'package:spring_core_dart/spring_core_dart.dart';
// The capability to add.
abstract interface class UsageTracked {
int get useCount;
void increment();
}
// Its implementation: public, not abstract, with a no-argument constructor.
class DefaultUsageTracked implements UsageTracked {
int _n = 0;
@override
int get useCount => _n;
@override
void increment() => _n++;
}
@Service()
class OrderService {
void place(String isbn) {}
}
@Aspect()
@Component()
class UsageTracking {
// Every OrderService (and subtype) proxy also implements UsageTracked.
@DeclareParents('OrderService+', defaultImpl: DefaultUsageTracked)
static UsageTracked? mixin;
// `this(usage)` binds the proxy as a UsageTracked, so each call bumps
// that bean's own counter.
@Before('execution(* OrderService.*(..)) && this(usage)')
void count(UsageTracked usage) => usage.increment();
}
導入したインターフェースを通してカウンターを読みます。
const log = SpringLog('app');
final context = ApplicationContext.run([appBeans]);
final orders = context.getBean<OrderService>();
orders.place('978-0');
final usage = orders as UsageTracked; // the proxy implements it
log.info('calls: ${usage.useCount}'); // calls: 1
パラメータ
| 名前 | 型 | 既定値 | 意味 |
|---|---|---|---|
value |
String |
必須 | インターフェースを得る Bean クラスの型パターン。* ワイルドカードを含む単純名で、+ はその型とサブタイプを表します(Orders、*Service、Repository+)。 |
defaultImpl |
Type |
必須 | インターフェースを実装するクラス。public で抽象でなく、フィールドのインターフェースを実装し、引数なしのコンストラクタを持つ必要があります。 |
ルールと制限
- インターフェースを得るのは、アスペクトと同じモジュールの Bean だけです。
- Bean がインターフェースを得るのは、アスペクトが登録されている間だけです。条件やプロファイルでアスペクトを無効にすると、イントロダクションもなくなります。
- ラッパープロキシと生成されたサブクラスプロキシの両方で動作します。
- インターフェースは Bean クラスではなくプロキシにあります。
getBeanや注入で得たオブジェクトをキャストしてください。 @DeclareParentsのイントロダクションを持つ Bean は、ビルドで間引かれることはありません。- 不正な
defaultImplは、"defaultImpl Bad must be a public, non-abstract class implementing Tracked" のようなメッセージでビルドを失敗させます。
Spring (Java) では
@Aspect
@Component
class UsageTracking {
@DeclareParents(value = "com.example.service.*+",
defaultImpl = DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("execution(* com.example.service.*.*(..)) && this(usage)")
void count(UsageTracked usage) { usage.increment(); }
}
重要な違い:
- 型パターンはパッケージではなく単純名を使います。
- パターンは最初の位置引数で、
defaultImplは必須の名前付き引数です。 - Dart ではフィールドは nullable です(
static UsageTracked? mixin;)。代入されることはなく、ジェネレーターが読むだけです。 - イントロダクションは、アスペクトと同じモジュールの Bean にだけ適用されます。