skip to content

When proxy-based Spring AOP can't advise the methods you need (final/private/static, self-invocation, final classes), what are your options and how do you choose?

level: principalimportance: nice to knowfreq 34%

answer

  1. Prefer refactor over AspectJ
  2. Extract a bean to cross the proxy boundary
  3. AspectJ weaves bytecode → no proxy limits
  4. LTW = -javaagent + @EnableLoadTimeWeaving
  5. currentProxy()/exposeProxy = tactical hack only

basics

~20 s

Options: refactor the code to expose a public non-final method through the proxy (e.g. split into a separate bean), or switch from proxy-based Spring AOP to AspectJ weaving, which rewrites bytecode and can advise final/private/static methods and final classes.

solid answer

~50 s

First I try to avoid the limitation by design: make the advised method public/protected and non-final, move it to a collaborator bean so calls cross a proxy boundary instead of self-invoking, and (in Kotlin) apply the all-open plugin so beans aren't final by default. That keeps the simple, well-understood proxy model. When the requirement is genuinely to advise private/final/static methods, constructors, field access, or same-object calls — for example cross-cutting instrumentation deep inside a class — I move to AspectJ, which weaves advice into bytecode at compile time (ajc) or load time (LTW agent) and has none of the proxy restrictions. The trade-off is added build/agent complexity, a steeper mental model, and weaving that isn't limited to Spring beans. I'd standardize the choice, document it, and add tests, rather than sprinkle AopContext.currentProxy() hacks, which work but obscure intent.

code

java · 23 lines
java
// Preferred: refactor so the advised call crosses a proxy boundary.

@Service
public class InvoiceFacade {
    private final InvoicePersister persister; // separate bean = separate proxy
    InvoiceFacade(InvoicePersister persister) { this.persister = persister; }

    public void process(Invoice i) {
        validate(i);
        persister.save(i);        // external call -> @Transactional advice DOES apply
    }
    private void validate(Invoice i) { /* ... */ }
}

@Service
public class InvoicePersister {
    @Transactional
    public void save(Invoice i) { /* real transaction */ }
}

// Fallback tactic (use sparingly): call self through the exposed proxy.
// @EnableAspectJAutoProxy(exposeProxy = true)
// ((InvoiceFacade) AopContext.currentProxy()).someAdvisedMethod();

go deeper

for a junior

Not expected to answer this at depth.

for a middle

Might name 'use AspectJ' but likely without weighing costs or the refactor-first preference.

for a senior

Should present refactor-first, then AspectJ, and know the self-proxy tactical option and its downsides.

for a principal

Should give a decision framework, weigh operational/team costs of AspectJ vs the simplicity of proxies, and add governance (arch tests, conventions) to prevent silent failures.

## The root constraint Proxy-based Spring AOP advises only **public/protected, non-final, non-static instance methods**, invoked **through the proxy** (external calls). Everything below stems from that. As a principal engineer you're choosing how to satisfy a cross-cutting requirement without fighting the framework. ## Option A — Refactor to fit the proxy model (preferred default) - **Fix modifiers**: make the advised method `public`/`protected` and non-`final`; don't advise `private`/`static`. - **Break self-invocation by extracting a bean**: move the advised method into a separate `@Component` and inject it, so the call crosses a proxy boundary. This is usually the cleanest fix and also improves cohesion. - **Kotlin**: apply the `kotlin-spring` / `all-open` Gradle plugin so `@Service`/`@Transactional` classes and methods become `open` automatically; otherwise everything is final-by-default and silently unproxyable. - **Rationale**: keeps the codebase on the simple, universally understood proxy model; no new build tooling; behavior is testable with plain Spring tests. ## Option B — Self-reference / exposed proxy (tactical, use sparingly) - Inject a `@Lazy` self-reference, or enable `exposeProxy=true` and call `((MyType) AopContext.currentProxy()).method()`. - Solves same-object invocation without a new bean, but it's **obscure**, couples code to AOP, and doesn't help with private/final/static. Treat as a localized workaround, not a strategy. ## Option C — Switch to AspectJ weaving (when limitations are fundamental) AspectJ is a full AOP implementation that **weaves** advice into bytecode rather than wrapping objects in proxies: - **Compile-time weaving** (`ajc` / AspectJ Maven/Gradle plugin) — advice baked in at build time. - **Load-time weaving (LTW)** — a JVM agent (`spring-instrument`, `-javaagent`) transforms classes as they load; enabled via `@EnableLoadTimeWeaving` / `aop.xml`. - **Capabilities**: advises `private`, `final`, `static` methods, `final` classes, constructors, field get/set, and same-object calls — none of the proxy restrictions apply. Self-invocation just works. - **Costs**: extra build/agent setup and startup weaving; a more powerful (and more dangerous) pointcut model; weaving applies to any matched class, not just Spring beans, so blast radius is larger; harder for newcomers to reason about; debugging woven code is less obvious. ## Decision framework 1. Can I satisfy the requirement by making the method public/non-final and calling it through the proxy (possibly via an extracted bean)? → **Do that.** ~90% of cases. 2. Is it a one-off same-object call in code I control? → extract a bean; only fall back to self-proxy if extraction is truly awkward. 3. Do I need cross-cutting behavior on private/final/static members, constructors, or field access across many classes (e.g. deep instrumentation, security on internals)? → **AspectJ**, ideally LTW, standardized and documented. 4. Is the target a `final` class I can't change (third-party)? → JDK proxy needs an interface; otherwise AspectJ or wrap it. ## Governance - Pick one approach per concern and document it; don't mix ad-hoc `currentProxy()` hacks with clean beans. - Add architecture tests / lint to catch AOP annotations on non-public or final methods (a silent-failure class of bug). - Ensure tests exercise beans **through their injected proxy**, so self-invocation regressions surface.

  • What concretely does AspectJ let you advise that proxy-based Spring AOP cannot?
    private, final, and static methods; final classes; constructors; field read/write; and same-object (self-invoked) calls — because it weaves advice directly into bytecode instead of relying on subclass/interface proxies.
  • What are the operational costs of adopting AspectJ load-time weaving?
    You add a JVM agent (-javaagent spring-instrument) and aop.xml/@EnableLoadTimeWeaving config, pay startup weaving overhead, gain a broader/more dangerous pointcut surface that isn't limited to Spring beans, and raise the learning-curve and debugging complexity for the team.

context