Which Spring proxy-AOP limitations does AspectJ compile-time weaving remove, and why is it able to?
answer
- proxy boundary = the blind spot
- final, private, self-call, new-objects
- CGLIB subclasses can't override final
- self this.other() skips proxy
- bytecode-inline = no boundary to bypass
basics
~20 sCTW can advise final methods, private methods, constructors, and self-invoked calls, and even objects created with new. It works because ajc edits the class bytecode directly, so there is no proxy boundary that calls must pass through to be intercepted.
solid answer
~50 sSpring's proxy AOP intercepts a call only when it crosses the proxy boundary, which creates four classic blind spots: final methods/classes (CGLIB can't subclass/override them), private methods (invisible to a subclass or interface proxy), self-invocation (this.other() skips the proxy so advice on other() never fires), and non-Spring objects created with new (never proxied at all). AspectJ CTW removes every one of these because ajc weaves the advice directly into the method bytecode at build time — the cross-cutting code lives inside the method itself, so there is no boundary to bypass. That also lets it advise constructors, static initializers, and even field reads/writes, which proxies fundamentally cannot express. The costs are ajc in the build and the aspectjrt runtime dependency. Self-invocation is the one interviewers probe most, because it's a real bug source in proxy-based transactional or cached code.
code
java · 15 lines// Self-invocation: broken under proxy AOP, works under CTW.
@Service
public class ReportService {
public void generateAll() {
// Proxy AOP: this internal call skips the proxy, so any
// @Transactional/@Cacheable/advice on generateOne() does NOT fire.
// AspectJ CTW: advice is woven into generateOne()'s bytecode,
// so it DOES fire even on this self-call.
generateOne();
}
@Transactional
public void generateOne() { /* ... */ }
}go deeper
Memorize the four blind spots list; know CTW avoids them because it edits bytecode.
Explain the mechanism (proxy boundary vs inline bytecode) not just the list, and connect self-invocation to real @Transactional bugs.
Add the extra AspectJ join points (field access, constructors) and the Kotlin-final angle, plus non-AspectJ self-invocation workarounds.
Weigh whether the join-point reach is worth the build/runtime cost versus refactoring the design so proxy AOP suffices.
To answer well you must contrast the two interception models. **Proxy model (Spring default).** Spring wraps an advised bean in a **proxy**: a **JDK dynamic proxy** (implements the bean's interfaces) or a **CGLIB proxy** (a runtime-generated *subclass* of the bean). Callers hold a reference to the proxy; the proxy runs advice then delegates to the real target. Interception therefore only happens for calls that physically pass *through* the proxy reference. This yields four limitations: 1. **`final` methods and `final` classes** — CGLIB works by subclassing and overriding. You cannot override a `final` method, and you cannot subclass a `final` class, so such methods are silently left un-advised (Spring may even warn). JDK proxies don't hit this because they implement interfaces, but then only interface methods are advised. 2. **`private` methods** — not visible to a subclass and not part of any interface, so no proxy can intercept them. 3. **Self-invocation (internal calls)** — inside the target, `this` refers to the *raw* object, not the proxy. So `this.other()` (or an unqualified `other()`) bypasses the proxy entirely and no advice on `other()` runs. This is the #1 cause of "my `@Transactional`/`@Cacheable` on an internal method does nothing" bugs. 4. **Objects not managed by Spring** — anything you `new` yourself is never wrapped, so it's never advised. **Weaving model (AspectJ CTW).** `ajc` rewrites the actual `.class` bytecode at build time, inserting advice *inside* each matched method (or at field-access/constructor join points). There is no separate proxy object and no boundary; the behavior is part of the method's own body. Consequently: - **`final` works** — no subclassing is needed; the bytecode of the final method itself is modified. - **`private` works** — the weaver operates on the method body regardless of visibility. - **Self-invocation works** — `this.other()` still executes the woven-in advice because it's baked into `other()`'s bytecode, not gated by a proxy. - **`new`-created and domain objects work** — combined with Spring's `@Configurable`/`@EnableSpringConfigured`, even objects instantiated outside the container can be advised and dependency-injected. - **Extra join points** — AspectJ can weave `execution`, `call`, `get`/`set` (field access), `staticinitialization`, constructor (`initialization`/`preinitialization`) join points; Spring proxy AOP only supports method-`execution` join points on beans. **Gotchas / when to use.** CTW's power is exactly why teams reach for it: advising domain entities, final Kotlin classes (Kotlin classes/methods are `final` by default — a frequent proxy pain point), or fixing self-invocation without awkward self-injection. But it complicates the build (ajc, `aspectj-maven-plugin` or a Gradle AspectJ plugin) and ties you to `aspectjrt`. If your join points are ordinary public methods on Spring beans, plain proxy AOP is simpler and enough. A common middle-ground fix for self-invocation *without* AspectJ is to inject the bean into itself and call through the injected reference, or split the method into another bean — but AspectJ removes the problem at the mechanism level.
- Why specifically can't CGLIB advise a final method?CGLIB creates a runtime subclass and overrides methods to intercept them. A final method cannot be overridden, so the subclass can't wrap it; the call goes straight to the original, un-advised. AspectJ edits the method's own bytecode, so finality is irrelevant.
- Give a non-AspectJ workaround for the self-invocation problem.Inject the bean into itself (self-injection) and call the method via that proxied reference, use AopContext.currentProxy(), or move the advised method into a separate bean. AspectJ weaving fixes it without any of these.
saying these in an interview costs you the question
- Claiming Spring proxies can advise private methods.
- Saying self-invocation is intercepted by CGLIB proxies.
- Thinking CTW still creates a runtime proxy object.