Resolving AOP + IOC Circular Dependency Conflicts in Spring
The article explains why Spring's three‑level cache solves simple circular dependencies but fails for AOP‑enhanced beans, describes the timeline mismatch that causes BeanCurrentlyInCreationException, and presents tiered solutions including @Lazy injection, setter refactoring, early proxy processing, and architectural redesign.
1. Spring IOC Three‑Level Cache Principle
1.1 Three‑Level Cache Definition
Spring singleton beans are stored in three caches: singletonObjects (fully initialized beans), earlySingletonObjects (instantiated but not yet populated beans), and singletonFactories (ObjectFactory instances that can create early proxies).
// 一级缓存:完全初始化完毕、属性填充、代理完成、可直接使用的【成品Bean】
private final Map<String, Object> singletonObjects;
// 二级缓存:实例化完成、属性未填充、未初始化、未代理的【半成品裸Bean】
private final Map<String, Object> earlySingletonObjects;
// 三级缓存:Bean工厂对象,用于【提前生成代理对象】的函数工厂
private final Map<String, ObjectFactory<?>> singletonFactories;1.2 Why the Three‑Level Cache Is Needed
Second‑level cache can only store fixed objects.
Third‑level cache stores object factories (lambdas) that can execute logic dynamically.
For a normal bean the factory returns the raw object.
For an AOP‑proxied bean the factory should return a proxy, but Spring does not support this natively.
Without the third‑level cache, AOP‑enhanced beans always cause circular‑dependency failures.
1.3 Normal Circular Dependency Flow (No AOP)
Create A (new A()).
Put A into third‑level cache (expose factory).
Populate A's properties, discover dependency on B, start creating B.
Create B and put it into third‑level cache.
Populate B's properties, discover dependency on A.
Retrieve A's factory from third‑level cache, obtain the half‑finished A and place it into second‑level cache.
Inject half‑finished A into B, finish B initialization, move B to first‑level cache.
Inject fully initialized B into A, finish A initialization, move A to first‑level cache.
Application starts successfully.
Key point: For ordinary beans the half‑finished raw object and the final product are identical, so the process is safe.
2. AOP Proxy Breaks the Three‑Level Cache
2.1 When Spring Creates an AOP Proxy
Spring creates the proxy in the postProcessAfterInitialization phase, i.e., after property population and init method execution.
Circular‑dependency early exposure happens before property population, so the proxy creation timing is mismatched.
Result: Timeline mismatch leads to failure.
2.2 Full Timeline of AOP Circular‑Dependency Error
Instantiate A and put it into third‑level cache.
Populate A's properties, need to create B.
Instantiate B, its properties need A.
Attempt to retrieve A's factory from third‑level cache for early exposure.
At this moment A is not yet initialized, so AOP proxy cannot be created; only the raw object is returned.
B receives the raw A, finishes B initialization.
A continues initialization, post‑processors run, and the AOP proxy is finally generated.
The container now holds two A instances: a raw A inside B and a proxied A exposed externally.
Spring detects the inconsistency and throws BeanCurrentlyInCreationException.
2.3 Why Spring Chooses to Fail
If Spring allowed the mismatch, hidden production bugs would appear: transaction loss, intermittent AOP behavior, and impossible debugging.
Therefore Spring prefers to abort startup with a clear exception.
3. Different Proxy Types and Injection Methods
3.1 JDK Dynamic Proxy vs CGLIB
JDK proxy (interface‑based): raw object and proxy have different types – 100% conflict.
CGLIB proxy (subclass‑based): types match but object addresses differ – still conflict.
3.2 Injection‑Method Conflict Summary
Field @Autowired : normal beans compatible; AOP beans cause errors (early raw object exposure).
Setter injection : same behavior as field injection.
Constructor injection : completely unsupported for circular dependencies.
@Lazy delayed injection : temporarily compatible by breaking the creation chain.
3.3 Multi‑AOP Stacking Issues
Longer proxy chain and more post‑processor execution.
Higher probability of early‑proxy failure.
Spring more likely to detect object inconsistency and raise an error.
4. Hidden Production Bugs Caused by Circular Dependencies
4.1 Transaction Failure Bug
B receives the raw A object; when A's @Transactional method is invoked, no proxy is present, so the transaction is completely ineffective, leading to dirty reads, overselling, and data inconsistency.
4.2 Random AOP Feature Loss
Controller → A (proxy) works, but internal B → A uses the raw object, so logging, rate‑limit, and permission checks are skipped.
4.3 Sporadic Runtime Anomalies
Different bean loading orders cause the problem to appear intermittently, making it extremely hard to trace.
5. Tiered Solutions
Solution 1 – @Lazy Delayed Injection (Emergency Fix)
Underlying principle
@Lazy injects a delayed‑proxy factory instead of the real bean, cutting the circular creation chain.
Usage
@Service
public class UserService {
@Autowired
@Lazy
private OrderService orderService;
}
@Service
public class OrderService {
@Autowired
private UserService userService;
}Pros / Cons
✅ Fast, zero code change, suitable for urgent fixes.
❌ Does not address the root cause; dual‑object risk remains in complex scenarios.
Solution 2 – Unified Setter Injection
Replace constructor injection with setter injection (optionally combined with @Lazy) to lower the probability of circular‑dependency conflicts.
@Service
public class UserService {
private OrderService orderService;
@Autowired
public void setOrderService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
}Solution 3 – Early AOP Proxy Processor (Core Idea)
Core idea
Implement a SmartInstantiationAwareBeanPostProcessor that wraps beans with an AOP proxy during the early‑exposure phase, so the object placed in the third‑level cache is already a proxy.
import org.springframework.aop.framework.autoproxy.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class EarlyAopProxyProcessor implements SmartInstantiationAwareBeanPostProcessor {
private final AnnotationAwareAspectJAutoProxyCreator proxyCreator;
public EarlyAopProxyProcessor(AnnotationAwareAspectJAutoProxyCreator proxyCreator) {
this.proxyCreator = proxyCreator;
}
// Key: directly wrap the bean when it is exposed early
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
// Force early AOP proxy, solving proxy‑inconsistency in circular dependencies
return proxyCreator.wrapIfNecessary(bean, beanName, bean);
}
}Effect
The early‑exposed object in the third‑level cache is already a proxy.
B receives the final proxied A.
All AOP features (transaction, rate‑limit, logging) work 100%.
Circular‑dependency conflicts are completely eliminated.
Solution 4 – Architectural Refactor
Treat circular dependency as an architectural defect. Recommended actions:
Extract common logic into a separate service to break bidirectional coupling.
Enforce one‑way dependencies (upper layers depend on lower layers only).
Replace synchronous calls with Spring events or message‑queue decoupling.
Move stateless utility logic out of the IoC container.
6. Special Scenarios
6.1 Constructor Injection + AOP (No Fix)
Constructor injection occurs before bean instantiation, while the three‑level cache stores after instantiation. The timelines are completely misaligned, leaving no compatible solution; code must be refactored.
6.2 Prototype Beans + AOP
The three‑level cache only applies to singleton beans. Prototype beans are created anew each time, so circular dependencies always explode.
6.3 SpringBoot 2.x vs 3.x
SpringBoot 2.x: circular dependencies are allowed by default; AOP scenarios trigger errors.
SpringBoot 3.x: circular dependencies are disabled by default, causing immediate failure regardless of AOP.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
