10k

Spring Cookbook3 - Circular Dependency

Circular Dependency And How To Avoid

What is a circular dependency?

Bean A depends on bean B and bean B depends on bean A.

When creating bean A, Spring find it depends on bean B and bean B is not created, so Spring start to create bean B, but when creating bean B, it finds that bean B depends on bean A and and bean A hasn't created, so it will go to create beanA. Thus, there is a cycle. -> This is hypothesis, if Spring doesn't handle the circular dependency. -> Spring don't know which to create first. In these cases, Spring will raise a BeanCurrentlyInCreationException while loading context.

This happens at constructor injection., e.g.

```java @Component public class CircularDependencyA {

private CircularDependencyB circB;

@Autowired
public CircularDependencyA(CircularDependencyB circB) {
    this.circB = circB;
}

} ```

And another class:

```java @Component public class CircularDependencyB {

private CircularDependencyA circA;

@Autowired
public CircularDependencyB(CircularDependencyA circA) {
    this.circA = circA;
}

} ```

How does Spring handle it?

Two situations :

Without AOP(assume)

  1. When spring creates a bean, there will be basically three steps: createBeanInstance, attribute injection(populate bean), initialization(initializeBean)

    image-20200706092738559

  2. For the example above, the create A with createBean, it may create an instance or get from cache.(now we don't have A in cache).

Call getSingleton(beanName)

getSingleton(beanName, true) will try to get bean from cache, where there are three tiers:

  1. singletonObjects, first tier , stores all the created singleton bean;
  2. earlySingletonObject, instantiated but not attribute injected objects;
  3. singletonFactories, expose a singleton in advance, tier2 stores object get from here.

Spring try to get bean from cache but there is no such bean in 1 and 2 cache, so it will call another overwritten function getSingleton(beanName, singletonFactory)

Call getSingleton(beanName, singletonFactory)

Here a bean will be created

```java public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); synchronized (this.singletonObjects) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) {

        // ....
        // 省略异常处理及日志
        // ....

        // 在单例对象创建前先做一个标记
        // 将beanName放入到singletonsCurrentlyInCreation这个集合中
        // 标志着这个单例Bean正在创建
        // 如果同一个单例Bean多次被创建,这里会抛出异常
        beforeSingletonCreation(beanName);
        boolean newSingleton = false;
        boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
        if (recordSuppressedExceptions) {
            this.suppressedExceptions = new LinkedHashSet<>();
        }
        try {
            // 上游传入的lambda在这里会被执行,调用createBean方法创建一个Bean后返回
            singletonObject = singletonFactory.getObject();
            newSingleton = true;
        }
        // ...
        // 省略catch异常处理
        // ...
        finally {
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = null;
            }
            // 创建完成后将对应的beanName从singletonsCurrentlyInCreation移除
            afterSingletonCreation(beanName);
        }
        if (newSingleton) {
            // 添加到一级缓存singletonObjects中
            addSingleton(beanName, singletonObject);
        }
    }
    return singletonObject;
}

} ```

Here the bean is created and put to level 1 cache.

Call addSingletonFactory

The bean is wrapped in a factory and be put to level 3 cache. image-20200706105535307

java // 这里传入的参数也是一个lambda表达式,() -> getEarlyBeanReference(beanName, mbd, bean) protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { // 添加到三级缓存中 this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }

At this point, Spring is going to inject attributes for A and it finds B, who does not exist, so it goes into B's procedure.But when creating B and do attributes injection, Spring can get A from cache.image-20200706115959250Here, the internal bean A was returned by getEarlyBeanReference

java protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }

In other words, here we have no AOP, then this code is:

java protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; return exposedObject; }

Whole process :image-20200706133018669

In the normal case where there is no AOP, the tier3 cache layer does nothing and only returns singleton A generated in tier2.

With AOP

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

If there is AOP, then we go into line 7:

public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    // 如果需要代理,返回一个代理对象,不需要代理,直接返回当前传入的这个bean对象
    return wrapIfNecessary(bean, beanName, cacheKey);
}

When getEarlyBeanReference return a proxy of A, rather than the instance A created previously.

image-20200706161709829

getSingleton() has a false parameter which banned second level cache:

image-20200706160542584

Above code shows after initialization, getSingleton is called again, with a false parameter , as we can remember, when inject A to B, proxy of A is move from tier3 cache to tier2. So here we get a proxy object of A from tier2 cache.

Why tier 3 cache use factory rather than direct reference: delay the proxy of instance. -> it doesn't know so far if there will be a circular dependency -> the philosophy behind Spring design. Not considering circular dependency, it's still like this.

How to resolve this?

Redesign

Redesign code which is important. We need to rethink if the design is reasonable. However, sometimes it's legacy code, or we have limited time to redesign, we may need some workarounds.

Use @Lazy

A simple way to break the cycle is by telling Spring to initialize one of the beans lazily(later after the current bean creation). So, instead of fully initializing the bean, it will create a proxy to inject it into the other bean. The injected bean will only be fully created when it’s first needed.

@Component public class CircularDependencyA {

```java private CircularDependencyB circB;

@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
  this.circB = circB;
}

} ```

Use setter/field injection

This way, Spring creates the beans, but the dependencies are not injected until they are needed.

```java @Component public class CircularDependencyA {

private CircularDependencyB circB;

@Autowired
public void setCircB(CircularDependencyB circB) {
    this.circB = circB;
}

public CircularDependencyB getCircB() {
    return circB;
}

} ```

```java @Component public class CircularDependencyB {

private CircularDependencyA circA;

private String message = "Hi!";

@Autowired
public void setCircA(CircularDependencyA circA) {
    this.circA = circA;
}

public String getMessage() {
    return message;
}

} ```

Use @PostConstruct

Another way to break the cycle is by injecting a dependency using @Autowired on one of the beans and then using a method annotated with @PostConstruct to set the other dependency.

```java @Component public class CircularDependencyA {

@Autowired
private CircularDependencyB circB;

@PostConstruct
public void init() {
    circB.setCircA(this);
}

public CircularDependencyB getCircB() {
    return circB;
}

} ```

```java @Component public class CircularDependencyB {

private CircularDependencyA circA;

private String message = "Hi!";

public void setCircA(CircularDependencyA circA) {
    this.circA = circA;
}

public String getMessage() {
    return message;
}

} ```

Implement ApplicationContextAware and InitializingBean

If one of the beans implements ApplicationContextAware, the bean has access to Spring context and can extract the other bean from there.

By implementing InitializingBean, we indicate that this bean has to do some actions after all its properties have been set. In this case, we want to manually set our dependency.

Here’s the code for our beans:

```java @Component public class CircularDependencyA implements ApplicationContextAware, InitializingBean {

private CircularDependencyB circB;

private ApplicationContext context;

public CircularDependencyB getCircB() {
    return circB;
}

@Override
public void afterPropertiesSet() throws Exception {
    circB = context.getBean(CircularDependencyB.class);
}

@Override
public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
    context = ctx;
}

} ```

```java @Component public class CircularDependencyB {

private CircularDependencyA circA;

private String message = "Hi!";

@Autowired
public void setCircA(CircularDependencyA circA) {
    this.circA = circA;
}

public String getMessage() {
    return message;
}

} ```

Reference

  1. Circular Dependency
  2. Java Spring filed injection cons
Thoughts? Leave a comment