skip to content

How do mocking frameworks and AOP frameworks use dynamic proxies under the hood, and what are the trade-offs versus MethodHandles or compile-time weaving?

level: principalimportance: nice to knowfreq 22%

answer

  1. Mockito/Hibernate → ByteBuddy subclass; Spring → JDK or CGLIB
  2. Interceptor chain + proceed() = around advice
  3. Proxies miss: final, self-invocation, constructors
  4. MethodHandles > reflective Method.invoke for speed
  5. AspectJ weaving = no proxy, advises self-calls & final

basics

~20 s

Frameworks generate a proxy (interface-based JDK proxy or a subclass via ByteBuddy/CGLIB) and route every call through an interceptor. Mocks record/replay calls there; AOP runs advice there. Alternatives are MethodHandles for faster dispatch and compile-time weaving for zero runtime proxies.

solid answer

~50 s

Both mocking and AOP frameworks intercept method calls. JDK dynamic proxies cover interface targets; for concrete classes frameworks generate a subclass with ByteBuddy (modern; Mockito, Hibernate) or CGLIB. The interceptor sees each call, and a mock records expectations and returns stubbed values, while AOP runs before/after/around advice and then proceeds to the real method. The trade-offs: runtime proxies are flexible and need no build step but add per-call indirection, can't touch final members, suffer self-invocation blind spots, and can clash with the module system. java.lang.invoke MethodHandles/LambdaMetafactory give faster, more direct invocation than reflective Method.invoke and underpin modern interceptor dispatch. Compile-time or load-time weaving (AspectJ) rewrites bytecode directly, so there's no proxy object at all — it can advise final methods, constructors, and field access, and self-calls are intercepted, at the cost of a build/agent step and more complexity. Choose proxies for simplicity, weaving for completeness and performance.

go deeper

for a junior

Aware that mocking and AOP libraries rely on generated proxies behind the scenes.

for a middle

Can say Mockito/Spring generate proxies and that interception routes calls to a handler/interceptor.

for a senior

Explains JDK-vs-subclass selection, the final/self-invocation limits, and that weaving is an alternative; knows ByteBuddy is the modern generator.

for a principal

Compares proxy interception, MethodHandle dispatch, and compile/load-time weaving on capability, performance, module-system, and maintainability axes, and chooses per context.

## The shared idea: interception Mocking and Aspect-Oriented Programming (AOP) both need to **intercept a method call** and run custom logic instead of, or around, the real body. Dynamic proxies are the most common runtime vehicle. ### Terms - **Mock**: a fake object used in tests that records which methods were called and returns programmed values. - **AOP / advice**: cross-cutting logic (logging, transactions, security) injected around a 'join point' (a method call). 'Advice' = the injected code; 'around advice' wraps the call and decides whether/when to `proceed()`. - **Weaving**: the act of combining advice with target code — at **compile time**, **load time** (via a Java agent that transforms bytecode as classes load), or **runtime** (via proxies). - **MethodHandle** (`java.lang.invoke`): a typed, directly-invokable reference to a method, faster than reflective `Method.invoke`. ## How mocking frameworks use proxies Mockito creates a mock by generating a subclass/implementation (via **ByteBuddy**) whose every method is intercepted. On a call it consults stubbing rules (`when(...).thenReturn(...)`), records the invocation for later `verify(...)`, and returns a default or stubbed value. For interface mocks a JDK proxy would suffice, but to mock concrete classes it must subclass — hence ByteBuddy. This is why Mockito historically **cannot mock final classes/methods** without the inline mock maker (which uses an agent to redefine bytecode). ## How AOP frameworks use proxies Spring AOP wraps a bean in a proxy: a **JDK dynamic proxy** if accessed via an interface, else a **CGLIB** subclass. The proxy's handler/interceptor builds a chain of advice; around-advice calls `proceed()` to continue to the next advice or finally the real method. Limitations follow directly from the proxy model: **self-invocation** (a bean calling its own method via `this`) bypasses the proxy and skips advice; **final** classes/methods can't be subclass-proxied; constructors/field access aren't join points. ## Why MethodHandles matter Reflective `Method.invoke` is flexible but relatively slow and does access checks each call. `java.lang.invoke` **MethodHandles** and `LambdaMetafactory` produce direct, JIT-friendly call sites; many interception/serialization frameworks now dispatch through MethodHandles for performance. A handler can still receive the `Method` for identity but invoke through a cached `MethodHandle`. ## Compile-time / load-time weaving (AspectJ) AspectJ rewrites the **bytecode of the target classes themselves** rather than wrapping them in a separate proxy object. Consequences: - **No proxy object** — the advice lives in the real class, so **self-invocation is advised**, and **final methods, constructors, and even field access** can be join points. - **Costs**: a special compiler or a load-time-weaving Java agent, longer builds, debugging into woven code, and tighter coupling to the weaving toolchain. ## Decision framework (principal-level) | Concern | Runtime proxy (JDK/ByteBuddy) | Weaving (AspectJ) | |---|---|---| | Setup | None / library only | Compiler or agent | | Self-invocation advised | No | Yes | | Final/constructors/fields | No | Yes | | Per-call overhead | Indirection (mitigated by MethodHandles) | Near-native | | Complexity / debuggability | Lower | Higher | | Module-system friction | Possible | Possible (agent) | Use runtime proxies for the common 80% (interface-based services, simple mocks) — they're simple and dependency-light. Reach for weaving when you must advise things proxies structurally can't (final, self-calls, constructors) or when proxy overhead is unacceptable. Use MethodHandles to make whichever interception layer you build fast.

  • Why can AspectJ advise final methods and self-invocations when proxies cannot?
    AspectJ weaves advice into the target class's own bytecode rather than wrapping it in a separate object. There is no proxy to bypass, so internal this-calls are advised, and since it edits the class directly it isn't blocked by final or by needing to subclass.
  • What advantage do MethodHandles offer over reflective Method.invoke inside an interceptor?
    MethodHandles from java.lang.invoke provide direct, JIT-optimizable invocation with access checks done once at lookup, so per-call dispatch is significantly faster than reflective Method.invoke, which is heavier and re-checks access.

saying these in an interview costs you the question

  • Saying Spring AOP intercepts self-invocation by default
  • Claiming Mockito can mock final classes with the default mock maker
  • Treating reflective Method.invoke as equally fast as MethodHandles
  • Believing AspectJ uses proxies (it weaves bytecode instead)
  • Ignoring the build/agent cost of weaving

context