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:
- Implementing proxy. The bean class has no private members and is not
final,baseorsealed. The proxy implements the class and its interfaces, so the bean can be injected by either. - Subclass proxy. A component class that cannot be implemented (private members, operators,
base) is created as a generated subclass instead. The subclass runs its public methods, getters and setters through the advice, like Spring's CGLIB proxy. The bean is its own proxy. A member the subclass cannot override (a private type, a default value that is not a public constant,@nonVirtual) is inherited and not advised, with a build warning. - Interface proxy only. A component class that can be neither implemented nor extended (
final,sealed,interface, a factory constructor, generic) is proxied through the interfaces it implements. Inject it by one of them. - No proxy. Without such an interface, the bean keeps its signatures. Advice that matches it fails the context at startup with the reason and what to change. Add an interface with the advised methods and inject the bean by it.
// 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
- Advice runs only through the proxy. Self-calls skip it unless made through
@Lazy()self-injection orAopContext.currentProxy(). - Private members are never advised.
- A class that is
final,sealedorinterface(or has only factory or private constructors, or is generic) can be advised only through an interface it implements. executionnever matches getters or setters.- The generator must run after every change to aspects or advised beans.
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:
- Spring makes JDK dynamic proxies (interfaces) or CGLIB subclasses at runtime. Here the generator writes the proxy classes at build time. No reflection is used at runtime, and it works in AOT, Flutter and web builds.
- No
@EnableAspectJAutoProxyand noexposeProxyflag: both are always on. - Dart class modifiers (
final,sealed,interface,base) decide the proxy kind, much asfinalclasses and methods limit CGLIB. Advisedmembers areaopTargetandaopAdvisors, notgetTargetSource()andgetAdvisors().