Why do you need a scoped proxy (proxyMode = TARGET_CLASS) when injecting a request- or session-scoped bean into a singleton?
answer
- singleton wired once → stale short-lived bean
- proxy delegates per call to current instance
- TARGET_CLASS = CGLIB subclass, INTERFACES = JDK proxy
- RequestContextHolder lookup per invocation
- alt: ObjectProvider / @Lookup / Provider
basics
~20 sA singleton is created once, so if you inject a short-lived bean directly it captures one stale instance forever. A scoped proxy injects a proxy that, on each method call, looks up the correct current-request/session instance.
solid answer
~40 sSingletons are instantiated once at startup and their dependencies are wired once. A request/session-scoped bean has many instances over time, so injecting it directly would freeze a single instance into the singleton — wrong for later requests, and at startup there may be no active request at all. The fix is a scoped proxy: @Scope(value="request", proxyMode=ScopedProxyMode.TARGET_CLASS) makes Spring inject a CGLIB proxy subclass of the target. The proxy is a singleton-safe stand-in; every method call is delegated to the real scoped instance resolved lazily from RequestContextHolder for the current thread. TARGET_CLASS uses CGLIB (subclassing, works without an interface); INTERFACES uses a JDK dynamic proxy (requires an interface). The meta-annotations @RequestScope/@SessionScope default to TARGET_CLASS.
code
java · 25 lines@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestData {
private String traceId = UUID.randomUUID().toString();
public String getTraceId() { return traceId; }
}
@Service // singleton
public class AuditService {
private final RequestData requestData; // actually a CGLIB proxy
public AuditService(RequestData requestData) { this.requestData = requestData; }
public void log(String msg) {
// proxy resolves the CURRENT request's instance on this call
System.out.println(requestData.getTraceId() + ": " + msg);
}
}
// Alternative without a proxy: lazy lookup
@Service
public class AuditService2 {
private final ObjectProvider<RequestData> provider;
public AuditService2(ObjectProvider<RequestData> provider) { this.provider = provider; }
public void log(String msg) { provider.getObject().getTraceId(); }
}go deeper
Grasp that singleton is wired once so needs a proxy for fresh short-lived beans.
Explain proxy delegation per call and TARGET_CLASS vs INTERFACES.
Compare scoped proxy vs ObjectProvider/@Lookup, discuss CGLIB final-method limits and scopedTarget naming.
Weigh proxy overhead/testability vs explicit provider injection; reason about thread-context propagation implications.
## The lifecycle mismatch problem A **singleton** bean is created and dependency-injected exactly once, when the container starts (or first needed). Its injected fields are set once and never re-wired. A **request-** or **session-scoped** bean, by contrast, has a different instance per request/session. If you inject a request-scoped bean straight into a singleton: 1. At startup there may be **no active request**, so Spring can't even create the target instance to inject. 2. Even if it could, the singleton would hold **one** instance forever, serving stale data to every later request. ## The scoped-proxy solution Declare the short-lived bean with a proxy: ```java @Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class RequestData { ... } ``` Now Spring injects not the real bean but a **proxy** — a singleton-safe object that is itself long-lived. On every method invocation, the proxy resolves the **actual** request/session-scoped instance for the *current thread* (via `RequestContextHolder`) and delegates the call. So one injected proxy transparently routes to the right per-request backing object. ## proxyMode values (`ScopedProxyMode`) - **TARGET_CLASS**: CGLIB proxy — a runtime **subclass** of the target class. Works whether or not the bean implements an interface. Cannot proxy `final` classes/methods. This is the default for `@RequestScope`, `@SessionScope`, `@ApplicationScope`. - **INTERFACES**: JDK dynamic proxy — implements the bean's interface(s). Requires the injection point to be typed to an interface. - **NO**: no proxy (the default of plain `@Scope`). Only safe when the injecting bean has a compatible-or-narrower scope. - **DEFAULT**: falls back to NO unless component-scan configures otherwise. ## Under the hood Spring registers an extra `ScopedProxyFactoryBean` (via `ScopedProxyUtils`) that produces the proxy; the real target is registered under a hidden `scopedTarget.<beanName>` name. The proxy holds no state itself — it's a thin dispatcher. ## Alternatives to a scoped proxy - Inject `ObjectFactory<RequestData>` / `ObjectProvider<RequestData>` and call `getObject()`/`getIfAvailable()` per use — explicit lazy lookup, no proxy magic. - Inject `Provider<RequestData>` (JSR-330). - Use `@Lookup` method injection. ## Gotchas - The proxy delegates per **method call**, so don't cache the result of a proxy method across requests. - TARGET_CLASS can't subclass `final` classes or intercept `final` methods. - The same technique applies to injecting **prototype** beans into singletons (prototype's per-instance benefit is otherwise lost). - Fields accessed directly (not via getter/method) aren't intercepted — always go through methods; on the proxy field access wouldn't route correctly.
- What's the difference between TARGET_CLASS and INTERFACES proxy modes?TARGET_CLASS creates a CGLIB subclass proxy (no interface needed, can't proxy final classes/methods); INTERFACES creates a JDK dynamic proxy that implements the bean's interface, so the injection point must be interface-typed.
- Name a way to inject a short-lived bean into a singleton without a scoped proxy.Inject ObjectProvider<T> or ObjectFactory<T> (Spring), Provider<T> (JSR-330), or use @Lookup method injection — each performs a fresh lookup of the correctly-scoped instance on demand.
saying these in an interview costs you the question
- Thinking direct injection of a request-scoped bean into a singleton just works and stays fresh
- Believing the proxy itself holds request state
- Saying TARGET_CLASS requires the bean to implement an interface (that's INTERFACES)