AOP proxies

A proxy is a generated class that stands in for an advised bean and runs the advice around its calls.

Why you need to know about it

You never write a proxy, but its rules explain what AOP can and cannot do. Advice runs only for calls that go through the proxy. The proxy must be able to stand in for the bean, which depends on how the bean class is declared.

How proxies are made

Dart cannot create classes at runtime, so spring_core_dart_generator writes a proxy class for each bean at build time. When the context starts, the AspectAutoProxyCreator (a bean post-processor the context registers itself) collects the advice of every aspect and advisor bean. After each bean is initialized, it checks the bean against the pointcuts. A matched bean is replaced by its proxy; getBean and other beans receive the proxy. A bean that no pointcut matches is not wrapped and costs nothing.

Aspects, advisors and post-processors are never proxied themselves. Aspects and advisors of parent contexts apply too.

Proxy kinds

Which kind a bean gets depends on its class:

// final: cannot be implemented or extended. Advice reaches it only
// through the Books interface, so inject Books, not BookService.
abstract interface class Books {
  Book find(String isbn);
}

@Service()
final class BookService implements Books {
  @Timed()
  @override
  Book find(String isbn) => Book(isbn);
}

Public fields are advised like getters and setters: a field int count is an implicit getter and, when not final, a setter. Their MemberKind is getter or setter. They are reached through within, bean, target, @within, @annotation(X) and custom advisors, never through execution.

Calls a bean makes to itself

Only calls from outside go through the proxy. A call through this inside the bean (findAll calling find) skips the advice, as in Spring. This also holds for a subclass proxy: while one of its advised members runs, including code after an await in a Future member, calls on the bean go straight to its code.

There are two ways to get the advice on a self-call.

Inject the bean into itself with @Lazy():

@Service()
class BookService implements Books {
  BookService(@Lazy() this._self);
  final Books _self;

  @Timed()
  @override
  Book find(String isbn) => Book(isbn);

  // _self.find goes through the proxy, so @Timed applies; this.find
  // would skip it.
  List<Book> findAll(List<String> isbns) =>
      [for (final isbn in isbns) _self.find(isbn)];
}

Or call through AopContext.currentProxy():

List<Book> findAll(List<String> isbns) => [
  for (final isbn in isbns)
    (AopContext.currentProxy() as Books).find(isbn), // advised
];

AopContext.currentProxy() returns the proxy whose call is in progress. It throws a StateError when no call through a proxy is in progress. On a subclass proxy it advises only the one call made right after it, so call it again for each call. Unlike Spring, the proxy is always exposed (there is no exposeProxy flag), also in the asynchronous code that an advised Future method starts.

The body of an async* or sync* member runs only when its result is listened to or iterated, after the member has returned, so the calls it makes on the bean are advised.

Advice without annotations

A PointcutAdvisor bean is applied like an aspect. This suits libraries that decide in code what to advise.

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

An advisor bean that is an Advisor but not a PointcutAdvisor applies to every member of every proxied bean.

Inspecting and changing a live proxy

Every generated proxy implements Advised.

Member Meaning
aopTarget The bean behind the proxy; the proxy itself for a subclass proxy.
aopAdvisors The advisors that apply, outermost first.
addAdvisor(advisor) Appends an advisor as the innermost one; the next calls use it.
removeAdvisor(advisor) Removes it; returns false when it was not there.
addAdvice(interceptor) Appends an interceptor for every member (wrapped with a TruePointcut).
removeAdvice(interceptor) Removes the first advisor whose advice is that object.

addAdvisor and addAdvice throw an AopConfigException when the build generated the proxy class for only some members (see pruning below).

AopUtils answers the common questions:

final calculator = context.getBean<Calculator>();
AopUtils.isAopProxy(calculator); // true when advised
AopUtils.getTarget(calculator); // the SimpleCalculator behind it

More proxies in code

ProxyFactory makes extra proxies of a bean with advice chosen by the caller, for example in a test. The bean in the context does not change. It reuses the proxy class the generator wrote for the bean.

// A logging view of the calculator bean, for this caller only.
final factory = ProxyFactory.forBean(context.beanFactory, 'calculator')
  ..addAdvice(LoggingInterceptor());
final calculator = factory.getProxy() as Calculator;

AspectJProxyFactory adds addAspect(aspect, metadata) to apply one aspect to one proxy only. getProxy() throws an AopConfigException for a bean without a proxy class, a pruned proxy class, or a subclass proxy (change its advice with Advised.addAdvisor instead).

Build options

Set in your package's build.yaml:

targets:
  $default:
    builders:
      spring_core_dart_generator:spring_core_dart:
        options:
          aop_proxies: all   # all (default) | interfaces | none
          aop_prune: true    # skip proxies of beans no pointcut matches
Option Values Default Meaning
aop_proxies all, interfaces, none all all: a proxy for every bean that can have one, subclass proxies included. interfaces: proxies only through a bean's abstract supertypes. none: no proxies, so AOP cannot advise the module's beans.
aop_prune true, false true With aop_proxies: all, skip the proxy class of a bean that no pointcut of the application can match, and route only the matched members through a kept proxy. false generates every proxy with every member.

A pruned bean keeps its signatures. An advisor that matches it anyway (one the build did not see) fails the startup with the reason, rather than being skipped silently.

Limits

In Spring (Java)

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
class AppConfig {}

Calculator calculator = context.getBean(Calculator.class);
AopUtils.isAopProxy(calculator);
((Advised) calculator).addAdvice(new LoggingInterceptor());
((Books) AopContext.currentProxy()).find(isbn);

Differences that matter:

See also