Can Spring only use L2 cache to deal with circular dependency?

What is circular dependency?

Let's talk about circular dependency first. Spring needs to inject B when initializing A, and A when initializing B. after spring starts, these two beans must be initialized

There are four scenarios for Spring's circular dependency

  1. singleton (prototype) of constructor
  2. Property (singleton, prototype)

"spring currently only supports attribute circular dependency of singleton type"

Loop dependency of constructor

@Component
public class ConstructorA {

 private ConstructorB constructorB;

 @Autowired
 public ConstructorA(ConstructorB constructorB) {
  this.constructorB = constructorB;
 }
}
@Component
public class ConstructorB {

 private ConstructorA constructorA;

 @Autowired
 public ConstructorB(ConstructorA constructorA) {
  this.constructorA = constructorA;
 }
}
@Configuration
@ComponentScan("com.javashitang.dependency.constructor")
public class ConstructorConfig {
}
public class ConstructorMain {

 public static void main(String[] args) {
  AnnotationConfigApplicationContext context =
    new AnnotationConfigApplicationContext(ConstructorConfig.class);
  System.out.println(context.getBean(ConstructorA.class));
  System.out.println(context.getBean(ConstructorB.class));
 }
}

When running the main method of ConstructorMain, an exception will be reported in the first line, indicating that Spring cannot initialize all beans, that is, the above form of circular dependency cannot be solved by Spring.

"For the cyclic dependency of the constructor, you can use @ Lazy annotation in the constructor to delay loading. When injecting the dependency, first inject the proxy object, and then create the object when it is used for the first time to complete the injection"

@Autowired
public ConstructorB(@Lazy ConstructorA constructorA) {
 this.constructorA = constructorA;
}

Because we mainly focus on the circular dependency of attributes, the circular dependency of constructors will not be analyzed too much

Circular dependency of attributes

First, let's demonstrate what is the circular dependency of attributes

@Data
@Component
public class A {

    @Autowired
    private B b;
}
@Data
@Component
public class B {

    @Autowired
    private A a;
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.javashitang.dependency")
public class Config {
}
public class Main {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(Config.class);
        System.out.println(context.getBean(A.class).getB() == context.getBean(B.class));
        System.out.println(context.getBean(B.class).getA() == context.getBean(A.class));
    }
}

The Spring container starts normally and the running result is true. It's not difficult to realize similar functions. I'll write a demo to demonstrate it

public class DependencyDemoV1 {

    private static final Map<String, Object> singletonObjects =
            new HashMap<>(256);

    @SneakyThrows
    public static <T> T getBean(Class<T> beanClass) {
        String beanName = beanClass.getSimpleName();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        // Instantiate bean
        Object object = beanClass.getDeclaredConstructor().newInstance();
        singletonObjects.put(beanName, object);
        // Start initializing the bean, that is, populating the properties
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // Get the class of the field to be injected
            Class<?> fieldClass = field.getType();
            field.set(object, getBean(fieldClass));
        }
        return (T) object;
    }

    public static void main(String[] args) {
        // Fake scanned classes
        Class[] classes = {A.class, B.class};
        for (Class aClass : classes) {
            getBean(aClass);
        }
        System.out.println(getBean(A.class).getB() == getBean(B.class));
        System.out.println(getBean(B.class).getA() == getBean(A.class));
    }

}

"Before starting the following content, let's clarify two concepts"

Instantiation: call the constructor to create the object. Initialization: after calling the constructor to create the object, the attributes of the object are also assigned

It can be seen that the implementation of circular dependency is implemented with only one map, but this implementation has a small defect. The classes in singletonObjects may only complete instantiation and not initialization

In spring, the classes in singletonObjects have completed initialization, because we take singleton beans from singletonObjects, so it is impossible for us to get objects that have not been initialized.

So let's write the second implementation, "use singletonObjects to store the initialized object, and use earlySingletonObjects to temporarily store the instantiated object. After the object is initialized, put the object into singletonObjects and delete it from earlySingletonObjects."

public class DependencyDemoV2 {

    private static final Map<String, Object> singletonObjects =
            new HashMap<>(256);

    private static final Map<String, Object> earlySingletonObjects =
            new HashMap<>(256);

    @SneakyThrows
    public static <T> T getBean(Class<T> beanClass) {
        String beanName = beanClass.getSimpleName();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        if (earlySingletonObjects.containsKey(beanName)) {
            return (T) earlySingletonObjects.get(beanName);
        }
        // Instantiate bean
        Object object = beanClass.getDeclaredConstructor().newInstance();
        earlySingletonObjects.put(beanName, object);
        // Start initializing the bean, that is, populating the properties
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // Get the class of the field to be injected
            Class<?> fieldClass = field.getType();
            field.set(object, getBean(fieldClass));
        }
        singletonObjects.put(beanName, object);
        earlySingletonObjects.remove(beanName);
        return (T) object;
    }

    public static void main(String[] args) {
        // Pretend to scan out the class
        Class[] classes = {A.class, B.class};
        for (Class aClass : classes) {
            getBean(aClass);
        }
        System.out.println(getBean(A.class).getB() == getBean(B.class));
        System.out.println(getBean(B.class).getA() == getBean(A.class));
    }

}

The current implementation is consistent with spring and uses only level 2 cache. Why does spring have a third cache? "The third cache is mainly related to proxy objects"

I'd better improve the above example and use the implementation of level 3 cache instead

public interface ObjectFactory<T> {
    T getObject();
}
public class DependencyDemoV3 {

    private static final Map<String, Object> singletonObjects =
            new HashMap<>(256);

    private static final Map<String, Object> earlySingletonObjects =
            new HashMap<>(256);

    private static final Map<String, ObjectFactory<?>> singletonFactories =
            new HashMap<>(256);

    @SneakyThrows
    public static <T> T getBean(Class<T> beanClass) {
        String beanName = beanClass.getSimpleName();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        if (earlySingletonObjects.containsKey(beanName)) {
            return (T) earlySingletonObjects.get(beanName);
        }
        ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
        if (singletonFactory != null) {
            return (T) singletonFactory.getObject();
        }
        // Instantiate bean
        Object object = beanClass.getDeclaredConstructor().newInstance();
        singletonFactories.put(beanName, () -> {
            Object proxy = createProxy(object);
            singletonFactories.remove(beanName);
            earlySingletonObjects.put(beanName, proxy);
            return proxy;
        });
        // Start initializing the bean, that is, populating the properties
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // Get the class of the field to be injected
            Class<?> fieldClass = field.getType();
            field.set(object, getBean(fieldClass));
        }
        createProxy(object);
        singletonObjects.put(beanName, object);
        singletonFactories.remove(beanName);
        earlySingletonObjects.remove(beanName);
        return (T) object;
    }

    public static Object createProxy(Object object) {
        // Because this method may be executed twice, there should be a judgment here
        // If you have performed aop operation in advance, you can return directly. Just know the meaning and don't write it
        // If aop is required, the proxy object is returned; otherwise, the incoming object is returned
        return object;
    }

    public static void main(String[] args) {
        // Pretend to scan out the class
        Class[] classes = {A.class, B.class};
        for (Class aClass : classes) {
            getBean(aClass);
        }
        System.out.println(getBean(A.class).getB() == getBean(B.class));
        System.out.println(getBean(B.class).getA() == getBean(A.class));
    }

}

"Why wrap an ObjectFactory object?"

If the created Bean has a corresponding aop proxy, the corresponding proxy object should be injected when other objects are injected; "However, Spring cannot know whether this object has circular dependency in advance." under normal circumstances (without circular dependency), Spring creates the corresponding proxy only after the object is initialized. At this time, Spring has two choices:

  1. No matter whether there is circular dependency or not, the proxy object is directly created after instantiation, and the proxy object is put into the cache. When circular dependency occurs, other objects can directly get the proxy object and inject it (Only Level 2 cache is required, and singletonObjects and earlySingletonObjects can be used)
  2. "The proxy object is not created in advance. The proxy object is generated in advance only when the circular dependency is injected by other objects (only instantiation is completed at this time). In this way, when there is no circular dependency, the Bean still generates the proxy object after initialization" (Level 3 cache is required)

"So now you know the role of level 3 cache, mainly for the purpose that proxy objects can be generated after initialization, rather than in advance."

cache

explain

singletonObjects

The first level cache stores the beans after initialization

earlySingletonObjects

The second level cache, which stores the beans that have been instantiated, may be proxied

singletonFactories

Delay generation of proxy objects

Source code analysis

When obtaining beans, first try to obtain them from the level 3 cache, which is similar to our Demo above

DefaultSingletonBeanRegistry#getSingleton

When it cannot be obtained from the cache, it will create AbstractAutowireCapableBeanFactory#doCreateBean (part of the code is deleted)

When circular dependency occurs, the proxy object will be obtained from the factory

When the aop agent is started, an implementation class of SmartInstantiationAwareBeanPostProcessor is AbstractAutoProxyCreator

AbstractAutoProxyCreator#getEarlyBeanReference

The getEarlyBeanReference method proxies in advance. In order to prevent proxiing again later, you need to use earlyProxyReferences to record that this Bean has been proxied and is no longer proxied

AbstractAutoProxyCreator#postProcessAfterInitialization

This method is the place for aop proxy. Because it is possible to proxy in advance, first judge whether to proxy in advance according to earlyProxyReferences. If you proxy in advance, you don't need to proxy

When the bean is initialized, it will be put into the first level cache and deleted from the second and third level cache

DefaultSingletonBeanRegistry#addSingleton

When circular dependency occurs, the overall execution process is as follows

Your "like / watching / sharing" is the biggest driving force for me to adhere to!

It's not easy to insist. Sell cute and roll for encouragement (ฅ > ω<* ฅ)

Added by Ozzapoo on Tue, 08 Feb 2022 12:40:52 +0200