Revealing Spring Cyclic Dependencies for Springboot Source Analysis

Summary:

If you are an experienced programmer, you will inevitably encounter this phenomenon in your development: transactions do not take effect.Perhaps some of your little buddies will be astonished just after talking about this.Didn't Spring solve the problem of circular dependency, and how did it happen?Let's now uncover the most fundamental causes of Spring's circular dependency.

Spring Cyclic Dependency Flowchart

Why Spring Cyclic Dependency Occurs

  • Use BeanPostProcessor with proxy characteristics
  • Typically, there are transaction annotations @Transactional, asynchronous annotations @Async, etc.

Source Analysis Disclosure

    protected Object doCreateBean( ... ){
        ...
        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }
        ...
    
        // The populateBean sentence is particularly critical because it needs to assign values to the attributes of A, so B~~is instantiated here
        // And B, as we can see from above, it's just a regular Bean (you don't need to create a proxy object), and when the instantiation is complete, it continues to assign value to its property A, and it gets an early reference to A.
        // This is where the getEarlyBeanReference() method in the Bean A process put in above is executed to get an early reference to A when assigning attribute a to B ~~
        // When the getEarlyBeanReference() method of A is executed, the automatic proxy creator is executed, but since A does not label transactions, the proxy will not be created eventually, so B qualified attribute references will be the ** original object of A**
        // It is important to note that the @Async proxy object was not created in getEarlyBeanReference(), it was created in postProcessAfterInitialization
        // We can also see from this that @Async's proxy does not support circular references by default because it does not provide early references to proxy objects ~~ (note the difference between this and automatic proxy creators ~)
    
        // Conclusion: The dependent attribute field B of A is assigned an instance of B here (because B does not need to create a proxy, it is the original object)
        // Whereas the injection of A dependent on Instance B here is still a normal instance object of Bean A (note that the original object is not a proxy object) Note: exposedObject is still the original object at this time
        populateBean(beanName, mbd, instanceWrapper);
        
        // Proxy objects labeled @Async Bean are generated here ~~reference classes: AsyncAnnotationBeanPostProcessor
        // So when this sentence is executed, exposedObject will be a proxy object instead of the original object
        exposedObject = initializeBean(beanName, exposedObject, mbd);
        
        ...
        // Here is the focus of the error ~~
        if (earlySingletonExposure) {
            // As mentioned above, A is cyclically dependent on B, so A is put into the secondary cache at this time, so earlySingletonReference is here a reference to the original object of A
            // (This also explains why I said: If A is not circularly dependent, it will not be a problem to make a mistake, because if there is no circular dependency earlySingletonReference =null, it will be return ed directly.)
            Object earlySingletonReference = getSingleton(beanName, false);
            if (earlySingletonReference != null) {
                // The above analysis shows that exposedObject is an object proxied by @Aysnc, and bean s are original objects so else logic is not equal here
                if (exposedObject == bean) {
                    exposedObject = earlySingletonReference;
                }
                // AllowRawInjection DespiteWrapping labels whether the original type of this Bean is allowed to be injected into other beans, even if it ends up being wrapped (proxy)
                // The default is false to deny permission, and if you change to true to deny permission, you won't make a mistake.This is one of the solutions we will talk about later~~
                // Additionally, dependentBeanMap records Map~~~of each Bean on which it depends
                else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
                    // Our Bean A depends on B, so here the value is ['b']
                    String[] dependentBeans = getDependentBeans(beanName);
                    Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
    
                    // Check all the dependencies one at a time~like here B will be problematic
                    // "B" It passes through removeSingletonIfCreatedForTypeCheckOnly and finally returns to false because alreadyCreated already has it inside, indicating that B has been completely created ~~
                    // And B is done, so attribute a is assigned to finish the chat, but the referenced a in B is unequal to the mainstream A. That must be a problem (the explanation is not final)~~
                    // so will eventually be added to the actual DependentBeans, indicating that A really depends on ~~
                    for (String dependentBean : dependentBeans) {
                        if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                            actualDependentBeans.add(dependentBean);
                        }
                    }
        
                    // If there's such a real dependency, it's wrong ~~An exception is the exception information you see above
                    if (!actualDependentBeans.isEmpty()) {
                        throw new BeanCurrentlyInCreationException(beanName,
                                "Bean with name '" + beanName + "' has been injected into other beans [" +
                                StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                                "] 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 often the result of over-eager type matching - consider using " +
                                "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
                    }
                }
            }
        }
        ...
    }

Problem Simplification

  • Object earlySingletonReference = getSingleton(beanName, false) when a circular dependency occurs; must have value
  • Cache factory addSingletonFactory (beanName, () -> getEarlyBeanReference (beanName, mbd, bean)); SmartInstantiationAwareBeanPostProcessor will be added to the instance object
  • AbstractAutoProxyCreator is a subclass of SmartInstantiationAwareBeanPostProcessor, remember that the subclass of SmartInstantiationAwareBeanPostProcessor is critical!!!!!
  • exposedObject = initializeBean(beanName, exposedObject, mbd); post-process BeanPostProcessor, note BeanPostProcessor!!!!

Spring's circular dependency is easily resolved by its three-level cache, but post-processing in both places creates circular dependency.

Comparing AbstractAdvisorAutoProxyCreator with AsyncAnnotationBeanPostProcessor

Because the subclasses of SmartInstantiationAwareBeanPostProcessor perform post-processing in both places, they all have the same object references before and after, no circular dependencies, no asynchronous annotations, and why?Look at the above analysis by yourself, and look carefully!

How to resolve circular dependency?

  • Change Loading Order
  • @Lazy Comment
  • allowRawInjectionDespiteWrapping set to true (using the statement of judgment)
  • Don't use annotations designed by BeanPostProcessor, it's not realistic.

@Lazy

@Lazy means lazy loading and only works on BeanDefinition.setLazyInit().This adds one capability to it: deferred processing (proxy processing)

    // @since 4.0 arrived late, supporting @Lazy as the most fully functional Autoowire Candidate Resolver
    public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver {
        // This is the only thing this type of person can do, which is analyzed here 
        // Returning this lazy proxy indicates delayed initialization by checking to see if the @Lazy = true annotation is used in the @Autowired annotation 
        @Override
        @Nullable
        public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
            // Return a proxy if isLazy=true, otherwise return null
            // Equivalent to returning a proxy if the @Lazy annotation is labeled (of course the value of the @Lazy annotation cannot be false)
            return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
        }
    
        // This is relatively simple, with the @Lazy annotation on the line (the default value for the value property is true)
        // @Lazy supports labeling on attributes and method inputs ~~This will be resolved here
        protected boolean isLazy(DependencyDescriptor descriptor) {
            for (Annotation ann : descriptor.getAnnotations()) {
                Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
                if (lazy != null && lazy.value()) {
                    return true;
                }
            }
            MethodParameter methodParam = descriptor.getMethodParameter();
            if (methodParam != null) {
                Method method = methodParam.getMethod();
                if (method == null || void.class == method.getReturnType()) {
                    Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);
                    if (lazy != null && lazy.value()) {
                        return true;
                    }
                }
            }
            return false;
        }
    
        // The core content is the soul of this kind~~
        protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) {
            Assert.state(getBeanFactory() instanceof DefaultListableBeanFactory,
                    "BeanFactory needs to be a DefaultListableBeanFactory");
    
            // Implement-oriented class programming is used here, and the DefaultListableBeanFactory.doResolveDependency() method ~~
            final DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) getBeanFactory();
    
            //TargetSource is the core reason for its lazy loading, which is highlighted in the AOP section and is not described here
            // It has many well-known implementations such as HotSwappableTargetSource, SingletonTargetSource, LazyInitTargetSource,
            //SimpleBeanTargetSource, ThreadLocalTargetSource, Prototype TargetSource, and many more
            // Since only you need to use it yourself, implement ~~ using an anonymous internal class. The most important thing here is to see the getTarget method, which executes when it is used (that is, when the proxy object is actually used)
            TargetSource ts = new TargetSource() {
                @Override
                public Class<?> getTargetClass() {
                    return descriptor.getDependencyType();
                }
                @Override
                public boolean isStatic() {
                    return false;
                }
        
                // getTarget is called when proxy methods are called, so executing each proxy method executes this method, which is why doResolveDependency
                // I personally think it is inefficient and has some problems~~So I suggest using @Lazy~~as little as possible here   
                //However, it should be better in efficiency. Compared to http, serialization and deserialization, it's hardly worth mentioning, so it doesn't matter.
                @Override
                public Object getTarget() {
                    Object target = beanFactory.doResolveDependency(descriptor, beanName, null, null);
                    if (target == null) {
                        Class<?> type = getTargetClass();
                        // Friendly handling of null values for multivalue injection (do not use null)
                        if (Map.class == type) {
                            return Collections.emptyMap();
                        } else if (List.class == type) {
                            return Collections.emptyList();
                        } else if (Set.class == type || Collection.class == type) {
                            return Collections.emptySet();
                        }
                        throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
                                "Optional dependency not present for lazy injection point");
                    }
                    return target;
                }
                @Override
                public void releaseTarget(Object target) {
                }
            };   
    
            // Use ProxyFactory to generate a proxy for ts
            // This shows that the target object of the resulting proxy object is actually TargetSource, and TargetSource is the object of our business
            ProxyFactory pf = new ProxyFactory();
            pf.setTargetSource(ts);
            Class<?> dependencyType = descriptor.getDependencyType();
            
            // If the injected statement is private AInterface a; then this is an excuse value of true
            // Put this interface type in as well (otherwise, this proxy is not of this type, can't you directly report a mistake when reflecting a set????)
            if (dependencyType.isInterface()) {
                pf.addInterface(dependencyType);
            }
            return pf.getProxy(beanFactory.getBeanClassLoader());
        }
    }

When the @Lazy annotation completes the injection, the final injection is only a temporary proxy object generated here, and only when the target method is actually executed will the real bean instance be taken to the container to execute the target method.

Use the allowRawInjectionDespiteWrapping attribute to force a change in judgment

    @Component
    public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);
        }
    }

This will result in the proxy object inside the container and the original reference being exposed to other instances, making it ineffective.Since it only affects beans within a circular dependency, the scope of impact is not global, so it's a good idea when no better solution is found.

Keywords: Java Spring Attribute Programming

Added by mattbarber on Sun, 08 Sep 2019 07:43:37 +0300