@DeclareParents

@DeclareParents makes the proxies of matching beans implement an extra interface, with an implementation that the aspect supplies (an "introduction").

Why you need it

Sometimes many beans should share a capability that their classes do not have, for example a usage counter every service can report. @DeclareParents adds the interface to their proxies without changing the classes.

You do not need it when you can simply add the interface to the classes. That is easier to read and costs nothing at runtime. Introductions are a rare tool.

How it works

Put @DeclareParents on a static field of an @Aspect class. The field's type is the interface to add. The proxy of every bean whose class matches the type pattern then implements that interface and forwards its members to an instance of defaultImpl. Each proxy gets its own instance, created on first use.

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

Read the counter through the introduced interface:

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

Parameters

Name Type Default Meaning
value String required The type pattern of the bean classes that get the interface: a simple name with * wildcards, and + for the type and its subtypes (Orders, *Service, Repository+).
defaultImpl Type required The class implementing the interface. It must be public, not abstract, implement the field's interface, and have a constructor without arguments.

Rules and limits

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

Differences that matter:

See also