When a bean in a circular dependency is also AOP-proxied, how does Spring ensure the dependent gets the proxy (not the raw target), and when can this throw BeanCurrentlyInCreationException even for a setter/field cycle?
answer
- getEarlyBeanReference on SmartInstantiationAwareBPP
- AbstractAutoProxyCreator proxies early + earlyProxyReferences
- post-init skips re-wrap if already early-proxied
- exposedObject != bean after early ref consumed → throw
- 'wrapped … other beans do not use final version'
basics
~20 sThe level-3 ObjectFactory calls getEarlyBeanReference, which lets AOP post-processors build the proxy early, so a dependent in the cycle receives the proxy. If a different post-processor later wraps the bean into yet another object after the early reference was already exposed, the identities diverge and Spring throws BeanCurrentlyInCreationException.
solid answer
~50 sNormally AOP proxies are created at the end of initialization by AbstractAutoProxyCreator.postProcessAfterInitialization. But in a cycle a bean may be injected into a dependent before its own initialization finishes, so the proxy must be produced early. The level-3 ObjectFactory calls getEarlyBeanReference, which invokes SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference — AbstractAutoProxyCreator implements this and creates (and remembers, via earlyProxyReferences) the proxy at that moment. The dependent thus receives the proxy, and later the post-init step sees the reference was already proxied and skips re-wrapping. The consistency check in doCreateBean compares the exposed early reference against the final bean; if some other post-processor changed the object's identity after the early reference was handed out (so the exposed reference no longer equals the final bean and the early reference was actually consumed by another bean), Spring cannot guarantee consistency and throws BeanCurrentlyInCreationException with a message about the currently-in-creation bean not matching.
code
java · 15 lines// Sketch of the end-of-doCreateBean consistency check
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false); // early ref if consumed
if (earlySingletonReference != null) {
if (exposedObject == bean) {
// init didn't replace the object -> the early ref is canonical
exposedObject = earlySingletonReference;
} else if (/* other beans depend on beanName */ hasDependentBean(beanName)) {
// A different object was produced AFTER the raw early ref was handed out
throw new BeanCurrentlyInCreationException(beanName,
"injected into other beans in its raw version as part of a circular " +
"reference, but has eventually been wrapped ...");
}
}
}go deeper
Beyond scope; just know AOP proxies interact with cycles.
Know that early references may need to be proxies so dependents don't get the raw bean.
Explain getEarlyBeanReference and how re-wrapping is skipped via earlyProxyReferences.
Explain the end-of-creation consistency check and the exact scenario where a resolvable cycle still throws, plus mitigations.
## Normal proxy timing Without a cycle, an AOP proxy is created in the **last** post-processing step: `AbstractAutoProxyCreator.postProcessAfterInitialization` wraps the fully-initialized target. Dependents that receive the bean *after* this get the proxy — all good. ## The problem in a cycle In a setter/field cycle, bean A may be injected into B **before** A's own `initializeBean` (and thus its normal proxy step) has run. If A is supposed to be proxied, B must still receive the **proxy**, not the raw A — otherwise callers through B would bypass the aspect (a correctness bug). ## The solution: getEarlyBeanReference The level-3 cache stores an `ObjectFactory` that calls `getEarlyBeanReference(beanName, mbd, bean)`. This iterates `SmartInstantiationAwareBeanPostProcessor` beans. `AbstractAutoProxyCreator` implements `getEarlyBeanReference` to **create the proxy early** and records the target in an `earlyProxyReferences` map. So: 1. When B asks for A mid-cycle, the factory runs `getEarlyBeanReference` → A's proxy is created **now**, cached in level 2, and remembered in `earlyProxyReferences`. 2. B gets A's proxy. Correct. 3. Later, A finishes and reaches `postProcessAfterInitialization`. `AbstractAutoProxyCreator` checks `earlyProxyReferences`: if this bean was already early-proxied, it **skips** wrapping again and returns the existing proxy. No double-proxying. ## The consistency check that throws At the end of `doCreateBean`, Spring calls `getSingleton(beanName, false)` to fetch any **already-exposed early reference** and compares it with the `exposedObject` (the fully initialized bean/proxy): - If `earlySingletonReference != null` (someone consumed the early reference during the cycle): - If `exposedObject == bean` (initialization didn't replace the object with a different one, e.g. the proxy was the early reference all along) → fine; the early reference IS the canonical object, adopt it. - **Else** (initialization produced a *different* object than the one already handed out — some post-processor wrapped/replaced the bean **after** the early reference was exposed) → Spring has an inconsistency: other beans hold the old reference, but the 'real' bean is now something else. It throws **`BeanCurrentlyInCreationException`** ('Bean with name '…' has been injected into other beans … in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean.'). This is the classic scenario: an aspect/post-processor that proxies in `postProcessAfterInitialization` but does **not** participate in `getEarlyBeanReference`, so the early reference was the raw bean while the final bean is a proxy — divergent identities → exception. ## When you actually hit it - A custom `BeanPostProcessor` that wraps beans (proxying, decorating) but doesn't implement `SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference`, combined with a setter/field cycle involving that bean. - Some `@Async` / custom-proxy setups historically triggered this until they were taught to expose the early reference. ## Mitigations - Break the cycle (redesign / `@Lazy`) so no early raw reference is ever exposed. - Ensure custom proxying post-processors implement `getEarlyBeanReference` consistently. - For `@Async`-caused cases, `@Lazy` on the injection point is the usual fix. ## Why this matters at principal level It shows the deep coupling between the three-level cache and the AOP/post-processor pipeline: the third cache level isn't just an optimization, it's the hook that lets proxies be created at the exact moment and exactly once, and the consistency check is Spring refusing to silently ship a graph where some beans hold an un-proxied version of a bean.
- Which interface method lets an AOP proxy be created early enough for a cycle?SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference, implemented by AbstractAutoProxyCreator. The level-3 ObjectFactory invokes it, creating the proxy on demand and recording it in earlyProxyReferences so the normal post-init step doesn't wrap it again.
- A custom BeanPostProcessor proxies beans in postProcessAfterInitialization and one such bean is in a setter cycle — what can happen and how do you fix it?The early reference handed to other beans is the raw target, but the final bean is a proxy — identities diverge and Spring throws BeanCurrentlyInCreationException. Fix by having the post-processor participate in getEarlyBeanReference, or break the cycle with @Lazy / redesign.
saying these in an interview costs you the question
- Assuming proxies are always created only at post-init, even in cycles
- Thinking setter/field cycles can never throw once resolvable
- Ignoring that a raw early reference vs. final proxy mismatch is a correctness bug Spring guards against