Frameworks generate proxies at runtime to add transactions, security, or caching. How does that generation work, and what are its documented limitations?
answer
- Interface proxy vs subclass proxy vs weaving
- Self-invocation skips the proxy
- final/private/static can't be intercepted
- Subclass proxy fields are unset — use methods
- Interceptor order: cache vs security matters
basics
~20 sThe framework returns a generated stand-in instead of your object: either a class implementing your interfaces, or a subclass of your class, that runs extra logic then delegates. Limitations: only calls that go through the returned reference are intercepted, so internal self-calls, final/private/static methods, and direct field access are not.
solid answer
~60 sTwo dominant mechanisms. **Interface-based proxies** generate a class implementing the target's interfaces; calls dispatch into a handler that runs the interceptor chain and then invokes the real object. Limitation: only interface methods are visible, and the object cannot be cast to its concrete class. **Subclass-based proxies** generate a subclass of the target and override its methods; this works without interfaces but requires a non-final class with overridable methods and an accessible constructor, and the subclass's inherited fields are unpopulated, so direct field reads bypass the logic. A third option, **compile/load-time weaving**, rewrites the bytecode of the class itself, removing most restrictions at the cost of build/agent complexity. Shared limitations: **self-invocation** — an internal `this.method()` inside the target does not pass through the proxy, so transactions/security/caching silently don't apply; **final, private, static, and constructor-time** calls can't be intercepted by wrapping approaches; **identity** — the injected reference isn't the raw bean, so `instanceof`, reflection over declared fields, and identity comparisons see the proxy; and each interception adds a dispatch hop plus, sometimes, boxing/reflection overhead.
code
java · 9 lines// Conceptual shape of a generated interface proxy
Object invoke(Object proxy, Method m, Object[] args) {
for (Interceptor i : chain) i.before(m, args); // security, tx begin, cache lookup...
Object result = m.invoke(target, args); // the real object
for (Interceptor i : reversed(chain)) i.after(result);
return result;
}
// The trap: this.helper() inside `target` never re-enters the chain above.go deeper
Say the framework hands you a generated stand-in that runs extra code before delegating, and that only calls made through that reference get the extra behavior.
Distinguish interface-based from subclass-based generation with their requirements, and name self-invocation and final/private methods as the common limits.
Add weaving as the third mechanism, interceptor ordering bugs, identity/reflection consequences, subclass field-initialization traps, and how to test that interception is live.
Argue about where cross-cutting concerns belong at all — implicit interception versus explicit composition/middleware — weighing hidden behavior and debuggability against boilerplate, and set team rules (boundaries, ordering, unwrap policy).
This is the Proxy pattern applied *automatically*: instead of hand-writing `SecureAccounts`, a container gives your collaborators a generated surrogate that layers cross-cutting behavior (transaction demarcation, authorization, caching, retries, metrics, lazy loading) over your object. It's the runtime backbone of aspect-oriented programming and dependency-injection containers, and it appears in some form in most mainstream ecosystems. ## Mechanism A — interface-based (dynamic) proxies At runtime the framework builds a class that implements the target's interface(s). Every method body funnels into one handler: ``` handler.invoke(proxy, method, args) { runBeforeInterceptors(method, args) result = method.invokeOn(target, args) runAfterInterceptors(result) return result } ``` - **Requires** the target to expose its behavior through an interface, and collaborators to depend on that interface. - **Cannot** be cast to the concrete implementation class; methods not on the interface are invisible. - Historically reflective (slower), though modern implementations cache method handles. ## Mechanism B — subclass-based proxies The framework generates a **subclass** of the concrete type and overrides each interceptable method to run the chain and then call `super` or delegate. - **Requires** a non-final class, non-final/non-private methods, and a usable constructor. Some libraries can bypass constructors via low-level object allocation. - **Works without interfaces**, and `instanceof ConcreteType` succeeds. - **Danger:** the generated subclass has its *own* copy of inherited fields that were never initialized (or the object is delegated to). Code that reads a field directly (`other.name`) rather than through a method sees null/zero. Always go through methods. ## Mechanism C — weaving A build step or JVM/runtime agent rewrites the target class's own bytecode, inlining the advice. No wrapper object exists, so: - Self-invocation **is** intercepted. - final/private methods and constructors can be advised. - Costs: extra build tooling or a startup agent, harder debugging, and the class you ship is not the class you wrote. Some ecosystems instead do this at compile time via source generation or macros. ## The limitations everyone hits ### 1. Self-invocation (the big one) ``` class Orders { void batch(list) { for (o in list) this.place(o); } // NOT intercepted @Transactional void place(o) { ... } } ``` Calling `batch()` through the proxy intercepts `batch`, but `this.place(o)` runs on the raw target — no new transaction, no security check, no cache lookup. Symptoms: "the annotation does nothing". Fixes: inject the proxied self-reference, split into two collaborators (usually the right design answer — the cross-cutting boundary *is* a collaboration boundary), or switch to weaving. ### 2. Non-interceptable members Final classes/methods (subclass proxies can't override), private methods (not visible to overriding or interfaces), static methods (no instance dispatch), and anything invoked from a constructor or field initializer while the object is still being built. ### 3. Identity and reflection The reference you hold is not the raw object. Consequences: identity comparisons differ; `getClass()` returns a generated name; annotations/generics may be present on the proxy or only on the target depending on the tool; reflection scanning declared fields finds the proxy's; serialization frameworks may choke. Most frameworks provide an `unwrap`/`getTargetObject` utility. ### 4. Ordering and composition When several concerns wrap the same object (security, transaction, cache, metrics), **order matters** and is usually configurable via a precedence value. Getting it wrong produces real bugs: caching outside security serves cached data to unauthorized callers; a transaction inside a retry replays the retry rather than the transaction; metrics inside the transaction miss commit time. ### 5. Cost and debuggability Each layer adds a dispatch hop and stack frames; deep interceptor chains bloat stack traces and obscure the real call site. Reflective invocation is slower than a direct call, though rarely the bottleneck compared with the I/O these concerns usually guard. The real cost is comprehension: behavior that isn't in the source of the class you're reading. ### 6. Where the behavior applies Because interception attaches to a *bean/managed object*, code paths that create the object with `new` get none of it. Anything constructed outside the container is unproxied. ## How to work with it well - Prefer the cross-cutting boundary to coincide with a real collaboration boundary — that makes self-invocation a non-issue by construction. - Keep interceptable methods public on an interface (or the class non-final) and never read fields across objects. - Assert interception in tests: write a test that proves the transaction rolls back / the unauthorized call is denied *through the injected reference*, not by calling the class directly. - Make the interceptor order explicit rather than accidental. - When behavior "doesn't apply", check self-invocation and object creation path first — they account for most reports.
- Why does putting a caching interceptor outside an authorization interceptor create a security bug?Because a cache hit returns without ever reaching the authorization check, so a later, unauthorized caller receives data produced for an authorized one. Authorization must run before the cache is consulted (or the cache key must include the principal).
- You add an annotation to a private method and nothing happens. Why?Wrapping proxies intercept only externally dispatched, overridable/interface methods. A private method is neither visible on an interface nor overridable in a generated subclass, and it is normally reached by self-invocation anyway — doubly unreachable. Move it to a public method on a separate collaborator, or use weaving.
- How would you prove in a test that the interception is actually active?Exercise the object through the injected/managed reference and assert the observable effect of the concern — a rollback after a thrown exception, a denial for an unauthorized principal, a second call served without hitting the backing store — rather than instantiating the class directly, which bypasses every proxy.
saying these in an interview costs you the question
- Believing an annotation on a method applies no matter how the method is called
- Reading another object's fields directly when subclass-based proxies are in play
- Assuming `instanceof ConcreteClass` holds for an interface-based proxy
- Ignoring interceptor ordering (caching before authorization is a real vulnerability)
- Thinking `new MyService()` in a test exercises the same behavior as the container-managed instance