1. 程式人生 > >Springboot原始碼分析之Spring迴圈依賴揭祕

Springboot原始碼分析之Spring迴圈依賴揭祕

摘要:

若你是一個有經驗的程式設計師,那你在開發中必然碰到過這種現象:事務不生效。或許剛說到這,有的小夥伴就會大驚失色了。Spring不是解決了迴圈依賴問題嗎,它是怎麼又會發生迴圈依賴的呢?,接下來就讓我們一起揭祕Spring迴圈依賴的最本質原因。

Spring迴圈依賴流程圖

Spring迴圈依賴發生原因

  • 使用了具有代理特性的BeanPostProcessor
  • 典型的有 事務註解@Transactional,非同步註解@Async等

原始碼分析揭祕

    protected Object doCreateBean( ... ){
        ...
        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }
        ...
    
        // populateBean這一句特別的關鍵,它需要給A的屬性賦值,所以此處會去例項化B~~
        // 而B我們從上可以看到它就是個普通的Bean(並不需要建立代理物件),例項化完成之後,繼續給他的屬性A賦值,而此時它會去拿到A的早期引用
        // 也就在此處在給B的屬性a賦值的時候,會執行到上面放進去的Bean A流程中的getEarlyBeanReference()方法  從而拿到A的早期引用~~
        // 執行A的getEarlyBeanReference()方法的時候,會執行自動代理建立器,但是由於A沒有標註事務,所以最終不會建立代理,so B合格屬性引用會是A的**原始物件**
        // 需要注意的是:@Async的代理物件不是在getEarlyBeanReference()中建立的,是在postProcessAfterInitialization建立的代理
        // 從這我們也可以看出@Async的代理它預設並不支援你去迴圈引用,因為它並沒有把代理物件的早期引用提供出來~~~(注意這點和自動代理建立器的區別~)
    
        // 結論:此處給A的依賴屬性欄位B賦值為了B的例項(因為B不需要建立代理,所以就是原始物件)
        // 而此處例項B裡面依賴的A注入的仍舊為Bean A的普通例項物件(注意  是原始物件非代理物件)  注:此時exposedObject也依舊為原始物件
        populateBean(beanName, mbd, instanceWrapper);
        
        // 標註有@Async的Bean的代理物件在此處會被生成~~~ 參照類:AsyncAnnotationBeanPostProcessor
        // 所以此句執行完成後  exposedObject就會是個代理物件而非原始物件了
        exposedObject = initializeBean(beanName, exposedObject, mbd);
        
        ...
        // 這裡是報錯的重點~~~
        if (earlySingletonExposure) {
            // 上面說了A被B迴圈依賴進去了,所以此時A是被放進了二級快取的,所以此處earlySingletonReference 是A的原始物件的引用
            // (這也就解釋了為何我說:如果A沒有被迴圈依賴,是不會報錯不會有問題的   因為若沒有迴圈依賴earlySingletonReference =null後面就直接return了)
            Object earlySingletonReference = getSingleton(beanName, false);
            if (earlySingletonReference != null) {
                // 上面分析了exposedObject 是被@Aysnc代理過的物件, 而bean是原始物件 所以此處不相等  走else邏輯
                if (exposedObject == bean) {
                    exposedObject = earlySingletonReference;
                }
                // allowRawInjectionDespiteWrapping 標註是否允許此Bean的原始型別被注入到其它Bean裡面,即使自己最終會被包裝(代理)
                // 預設是false表示不允許,如果改為true表示允許,就不會報錯啦。這是我們後面講的決方案的其中一個方案~~~
                // 另外dependentBeanMap記錄著每個Bean它所依賴的Bean的Map~~~~
                else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
                    // 我們的Bean A依賴於B,so此處值為["b"]
                    String[] dependentBeans = getDependentBeans(beanName);
                    Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
    
                    // 對所有的依賴進行一一檢查~    比如此處B就會有問題
                    // “b”它經過removeSingletonIfCreatedForTypeCheckOnly最終返返回false  因為alreadyCreated裡面已經有它了表示B已經完全建立完成了~~~
                    // 而b都完成了,所以屬性a也賦值完成兒聊 但是B裡面引用的a和主流程我這個A竟然不相等,那肯定就有問題(說明不是最終的)~~~
                    // so最終會被加入到actualDependentBeans裡面去,表示A真正的依賴~~~
                    for (String dependentBean : dependentBeans) {
                        if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                            actualDependentBeans.add(dependentBean);
                        }
                    }
        
                    // 若存在這種真正的依賴,那就報錯了~~~  則個異常就是上面看到的異常資訊
                    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.");
                    }
                }
            }
        }
        ...
    }

問題簡化

  • 發生迴圈依賴時候Object earlySingletonReference = getSingleton(beanName, false);肯定有值
  • 快取工廠addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));將給例項物件新增SmartInstantiationAwareBeanPostProcessor
  • AbstractAutoProxyCreatorSmartInstantiationAwareBeanPostProcessor的子類,一定記住了,一定記住,SmartInstantiationAwareBeanPostProcessor
    的子類很關鍵!!!!!
  • exposedObject = initializeBean(beanName, exposedObject, mbd);進行BeanPostProcessor後置處理,注意是BeanPostProcessor!!!!!

Spring的迴圈依賴被它的三級快取給輕易解決了,但是這2個地方的後置處理帶來了 迴圈依賴的問題。

對比AbstractAdvisorAutoProxyCreator和AsyncAnnotationBeanPostProcessor

由於SmartInstantiationAwareBeanPostProcessor的子類會在兩處都會執行後置處理,所以前後都會相同的物件引用,不會發生迴圈依賴問題,非同步註解就不行了 ,至於為什麼?自己看上面的分析,仔細看哦!

如何解決迴圈依賴?

  • 改變載入順序
  • @Lazy註解
  • allowRawInjectionDespiteWrapping設定為true(利用了判斷的那條語句)
  • 別使用相關的BeanPostProcessor設計到的註解,,哈哈 這不太現實。

@Lazy

@Lazy一般含義是懶載入,它只會作用於BeanDefinition.setLazyInit()。而此處給它增加了一個能力:延遲處理(代理處理)

    // @since 4.0 出現得挺晚,它支援到了@Lazy  是功能最全的AutowireCandidateResolver
    public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver {
        // 這是此類本身唯一做的事,此處精析 
        // 返回該 lazy proxy 表示延遲初始化,實現過程是檢視在 @Autowired 註解處是否使用了 @Lazy = true 註解 
        @Override
        @Nullable
        public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
            // 如果isLazy=true  那就返回一個代理,否則返回null
            // 相當於若標註了@Lazy註解,就會返回一個代理(當然@Lazy註解的value值不能是false)
            return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
        }
    
        // 這個比較簡單,@Lazy註解標註了就行(value屬性預設值是true)
        // @Lazy支援標註在屬性上和方法入參上~~~  這裡都會解析
        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;
        }
    
        // 核心內容,是本類的靈魂~~~
        protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) {
            Assert.state(getBeanFactory() instanceof DefaultListableBeanFactory,
                    "BeanFactory needs to be a DefaultListableBeanFactory");
    
            // 這裡毫不客氣的使用了面向實現類程式設計,使用了DefaultListableBeanFactory.doResolveDependency()方法~~~
            final DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) getBeanFactory();
    
            //TargetSource 是它實現懶載入的核心原因,在AOP那一章節了重點提到過這個介面,此處不再敘述
            // 它有很多的著名實現如HotSwappableTargetSource、SingletonTargetSource、LazyInitTargetSource、
            //SimpleBeanTargetSource、ThreadLocalTargetSource、PrototypeTargetSource等等非常多
            // 此處因為只需要自己用,所以採用匿名內部類的方式實現~~~ 此處最重要是看getTarget方法,它在被使用的時候(也就是代理物件真正使用的時候執行~~~)
            TargetSource ts = new TargetSource() {
                @Override
                public Class<?> getTargetClass() {
                    return descriptor.getDependencyType();
                }
                @Override
                public boolean isStatic() {
                    return false;
                }
        
                // getTarget是呼叫代理方法的時候會呼叫的,所以執行每個代理方法都會執行此方法,這也是為何doResolveDependency
                // 我個人認為它在效率上,是存在一定的問題的~~~所以此處建議儘量少用@Lazy~~~   
                //不過效率上應該還好,對比http、序列化反序列化處理,簡直不值一提  所以還是無所謂  用吧
                @Override
                public Object getTarget() {
                    Object target = beanFactory.doResolveDependency(descriptor, beanName, null, null);
                    if (target == null) {
                        Class<?> type = getTargetClass();
                        // 對多值注入的空值的友好處理(不要用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) {
                }
            };   
    
            // 使用ProxyFactory  給ts生成一個代理
            // 由此可見最終生成的代理物件的目標物件其實是TargetSource,而TargetSource的目標才是我們業務的物件
            ProxyFactory pf = new ProxyFactory();
            pf.setTargetSource(ts);
            Class<?> dependencyType = descriptor.getDependencyType();
            
            // 如果注入的語句是這麼寫的private AInterface a;  那這類就是藉口 值是true
            // 把這個介面型別也得放進去(不然這個代理都不屬於這個型別,反射set的時候豈不直接報錯了嗎????)
            if (dependencyType.isInterface()) {
                pf.addInterface(dependencyType);
            }
            return pf.getProxy(beanFactory.getBeanClassLoader());
        }
    }

標註有@Lazy註解完成注入的時候,最終注入只是一個此處臨時生成的代理物件,只有在真正執行目標方法的時候才會去容器內拿到真是的bean例項來執行目標方法。

利用allowRawInjectionDespiteWrapping屬性來強制改變判斷

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

這樣會導致容器裡面的是代理物件,暴露給其他例項的是原始引用,導致不生效了。由於它只對迴圈依賴內的Bean受影響,所以影響範圍並不是全域性,因此當找不到更好辦法的時候,此種這樣也不失是一個不錯的方案。