Card 05/ 07
All 7 cards
GotchaDifficulty: Advanced1 min
A Request-Scoped Bean Inside a Singleton
CurrentRideContext is scoped to one request — a new instance per request, holding whatever ride that particular request is about. RideAuditor is a singleton that depends on it. Wired the obvious way, both requests below see the exact same ride.
@Component
@Scope("request")
class CurrentRideContext {
private final String rideId = currentRideId();
String describe() { return "handling " + rideId; }
}
@Component
class RideAuditor {
private final CurrentRideContext currentRideContext;
RideAuditor(CurrentRideContext currentRideContext) { this.currentRideContext = currentRideContext; }
void audit() { System.out.println(Thread.currentThread().getName() + " sees: " + currentRideContext.describe()); }
}request-1 auditor sees: handling ride-695
request-2 auditor sees: handling ride-695RideAuditor is a singleton, so it is built exactly once — at startup, on whatever thread happens to be running then. Building it means resolving its constructor argument right then, which pins the very first CurrentRideContext that happened to exist into RideAuditor forever. Every request after that sees that one request's ride.
The fix is a scoped proxy — proxyMode = ScopedProxyMode.TARGET_CLASS on the request-scoped bean's definition. RideAuditor gets injected with a proxy instead of a real instance, and the proxy defers the actual lookup to every single call, re-resolving CurrentRideContext for whichever request is asking at that moment.
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
CurrentRideContext currentRideContext() { return new CurrentRideContext(); }request-1 auditor sees: handling ride-620
request-2 auditor sees: handling ride-672