電動車主再享便利!北京市發布公用充電設施分佈圖

2月11日,北京市發改委發佈了「北京市電動汽車社會公用充電設施分佈圖」,電動車車主可透過網站、手機APP、微信等方式查找附近的充電設施,包括查詢充電站的建設分佈、具體位置、充電樁數量、充電口空閒數等資訊。   北京市發改委介紹,自2009年以來,全市累計建成了約6600根充電樁及5座換電場站,車輛推廣與充電樁建設數量比例約為1.5:1。按服務車輛類型和服務領域不同,全市充電設施主要分為三類,包括公共專用、私人自用和社會公用充電樁。   在公車、環境清潔車、計程車等公共專用領域,已建成充換電場站234座(其中含換電場站5座),充電樁3676個,日服務能力超過1.7萬車次。在私人自用領域,目前已建自用充電設施約1500個,自用建樁率約50%。在社會公用領域,全市累計建成約1500個社會公用充電樁,50%以上佈局在北京四環路以內,60%以上佈局在北京五環路以內,初步形成了中心城區平均服務半徑5公里的快速補電網路。   目前,全市已完成了1000根社會公用充電樁調試工作並對外開放投運,其餘部分將抓緊調試並爭取在春節前後投入使用。中國發改委介紹,下一步重點將在大型商圈等社會公共停車場和京津冀高速路服務區等領域,打造社會公用充電服務網路,2015年力爭在北京六環路內建成平均服務半徑為5公里的充電網路。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

樂視進軍電動車步伐加近:砸數十億美元 美國市場為目標

(圖片來源:leiphone.com)

  2月12日,媒體報導,樂視網資訊技術(北京)股份有限公司稱計畫投資數十億美元開發電動汽車,將美國定為首要目標市場。   此前的1月19日,樂視網正式確認「SEE計畫」即樂視超級汽車計畫。公司表示,這是由控股股東樂視控股所開展的一項旨在打造一個全球獨有的垂直整合汽車互聯網生態系統的重要戰略。   受超級汽車計畫等一系列利好影響,2月11日,樂視網以584億元人民幣(下同)的市值躍升創業板首位。自去年12月23日起,其股價從28.2元的底部漲至11日的69.43元,大漲146%。  
動態:正就專案資金與投資方磋商   上任伊始的樂視汽車業務中國負責人呂征宇接受電話採訪時表示,樂視計畫招聘更多人才以加強現有大約260人的電動汽車專案團隊。他說,樂視將這個專案視為一項長期投資,正在就專案資金與投資方進行磋商。樂視計畫投資數十億美元,將美國定為首要目標市場。他沒有透露更多細節。   樂視董事長賈躍亭誓言「顛覆」傳統汽車行業,宣佈要打造一款智慧化和互聯化程度更高的汽車。其目標是比亞迪和長城汽車等更成熟的中國車企未能實現的目標:把汽車賣給美國消費者。   呂征宇曾在日產汽車旗下英菲尼迪供職。他確信,就像賈躍亭所說的那樣,對於每件新生事物,當人們第一次看到的時候,首先會忽視你,然後會笑話你,再然後會反對你,然後你就贏了;利用自己在設計、製造和分銷方面的優勢,樂視有機會顛覆傳統汽車行業。  
回顧:北汽曾表示願意代工   2014年7月21日,國務院公佈《關於加快新能源汽車推廣應用的指導意見》,阿里巴巴與樂視網分別傳出要進軍汽車領域,一個牽手上汽,一個牽手北汽。   2014年4月份,北汽董事長徐和誼在一次公開場合就提出,「汽車企業未來可能會成為互聯網企業的貼牌製造商」這一觀點,甚至點名「樂視網」,表示願意「代工生產樂視汽車」。   在徐和誼看來,新能源汽車按照傳統汽車模式肯定不行。新能源汽車產業與傳統汽車產業不只是在燒油與用電方面的區別,「新能源汽車是一個全新的行業,必須要用全新的模式與之相適應」,而這個新模式的合作物件目前來看首選樂視的可能很大。   知名產業評論家信海光在微信中披露,樂視CEO賈躍亭在美期間,除了佈局海外市場,在美國創立兩家子公司,推動樂視生態業務全球化全面啟動外,另一高度保密專案,就是樂視汽車專案。據悉,賈躍亭在美國和徐和誼密會,初步達成合作汽車的意向,至於細節不得而知。   2014年4月9日,賓士CLA與樂視強強聯合上演了一場時尚行銷秀。賓士發言人表示其品牌看中了樂視網中極具潛力、並願為生活品質買單的高端用戶,而這批用戶對於北汽開拓新能源汽車市場十分關鍵。此外,寶馬、英菲尼迪等高端車企品牌也都上過樂視超級電視廣告。樂視一直強調的「生態」建設,是樂視進軍汽車業的驅動力,也是樂視推出互聯網汽車的良好土壤。   按照目前的樂視產品思路,互聯網行銷方式與產品價格將是撒手鐧,評論人士認為,樂視汽車一旦推向市場,會很大程度推動新能源車或者互聯網汽車的市場普及,而樂視網本身也會很快走向年營收千億元這一量級。  
影響:超級計畫引爆股價   2015年2月11日,樂視網收穫本週第2個、今年第5個漲停板。憑藉密集發力榮登創業板老大的樂視網,以市值584億元遠遠甩開第二名東方財富,後者總市值為498億元。   樂視網市值躍升首位,用了35個交易日。自去年12月23日起,其股價從28.2元的底部漲至11日的69.43元,大漲146%。   市場人士認為,樂視網已經與京東、小米、360一起,組成繼百度、阿里巴巴和騰訊之後的中國互聯網第二梯隊。樂視的總市值已經超越奇虎360,資料顯示,在納斯達克上市的360市值為78億美元,約合人民幣488億元。   樂視網股價任性飆升的背後,是電動超級汽車等計畫獲得了市場認同。   1月14日,美國影響力最大的商業科技媒體Business Insider刊發文章,認為中國互聯網公司樂視CEO賈躍亭將憑藉電動超級汽車,與推出特斯拉電動汽車的埃隆•馬斯克一樣,成為全球矚目的明星企業家。Business Insider文中稱:賈躍亭將是中國的「埃隆•馬斯克」。   1月19日,樂視網正式確認「SEE計畫」即樂視超級汽車計畫。公司表示,這是由控股股東樂視控股所開展的一項旨在打造一個全球獨有的垂直整合汽車互聯網生態系統的重要戰略,樂視網與其運營主體並無股權關係,短期內也不會對公司業績構成影響。   連結:特斯拉跌落神壇敲響警鐘   2月11日,特斯拉中國1月銷量僅為120輛的消息幾乎佔據了各大財經和科技媒體的頭條。這家曾經令中國媒體不惜溢美之詞、引得自主品牌車企紛紛一探究竟的電動車製造企業,在一年後跌落神壇。   特斯拉的失敗不可避免地讓一大批揮師進軍汽車行業的互聯網公司坐立難安。通過「電腦+四個輪子」的品牌形象和打破4S經銷體系、主打直營的銷售模式,特斯拉吸引了不少中國擁躉,其中不乏小米和樂視這樣靠垂直整合起家的手機和電視廠商。   從目前已經公佈的樂視超級汽車等方案來看,特斯拉的這批中國學徒們均給自己貼上了「智慧汽車」、「互聯網汽車」的標籤,但在電動車最核心的電池、電機、電控及底盤技術上卻諱莫如深。   特斯拉希望通過推出廉價版的Model X來緩解單產品線的窘境,而電池成本的下降是實現低價版車型量產的基礎。馬斯克曾樂觀預計10年內特斯拉能夠將電池成本降到100美元/千瓦時,但包括電池專家在內紛紛對馬斯克的預期予以駁斥:不僅在2025年之前,電池成本不可能降低至167美元/千瓦時,而且特斯拉Model X價格將從原本的3萬美元定價提高到5-8萬美元。   特斯拉在中國市場陷入泥潭戳破了電動車的泡沫,當樂視小米們在造車的道路上越走越遠時,不得不回頭看看特斯拉的警示。   (文章來源:長江商報)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

Spring IoC bean 的創建

前言

本系列全部基於 Spring 5.2.2.BUILD-SNAPSHOT 版本。因為 Spring 整個體系太過於龐大,所以只會進行關鍵部分的源碼解析。

本篇文章主要介紹 Spring IoC 容器是怎麼創建 bean 的實例。

正文

在上一篇Spring IoC bean 的加載中有這麼一段代碼:

if (mbd.isSingleton()) {
    // 創建和註冊單例 bean
    sharedInstance = getSingleton(beanName, () -> {
        try {
            // 創建 bean 實例
            return createBean(beanName, mbd, args);
        }
       	// 省略異常處理...
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

如果 bean 的作用域是單例,會調用 getSingleton() 方法並傳入 beanNameObjectFacoty作為參數;而 getSingleton() 方法會調用 ObjectFactorygetObject() 方法也就是上面代碼中的 createBean() 方法,返回 bean

這裏的 ObjectFactorybean 的延遲依賴查找接口,定義如下:

@FunctionalInterface
public interface ObjectFactory<T> {

    T getObject() throws BeansException;

}

只有在調用 getObject() 方法時才會真正去獲取 bean。下面我們正式開始分析 createBean() 方法。

AbstractAutowireCapableBeanFactory#createBean

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {

    RootBeanDefinition mbdToUse = mbd;

    // 將String類型的class字符串,轉換為Class對象,例如在XML中配置的class屬性
    Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
    if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
        mbdToUse = new RootBeanDefinition(mbd);
        mbdToUse.setBeanClass(resolvedClass);
    }

    try {
        // 進行定義的方法覆蓋
        mbdToUse.prepareMethodOverrides();
    }
    catch (BeanDefinitionValidationException ex) {
        throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), beanName, "Validation of method overrides failed", ex);
    }

    try {
        // 如果bean的實例化前回調方法返回非null,直接返回實例,跳過後面步驟。見下文詳解
        Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
        if (bean != null) {
            return bean;
        }
    }
    catch (Throwable ex) {
        throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "BeanPostProcessor before instantiation of bean failed", ex);
    }

    try {
        // 真正去創建bean的方法
        Object beanInstance = doCreateBean(beanName, mbdToUse, args);
        if (logger.isTraceEnabled()) {
            logger.trace("Finished creating instance of bean '" + beanName + "'");
        }
        // 返回bean的實例
        return beanInstance;
    }
    // 省略異常處理...
}

bean 實例化前置處理

我們先看一下 InstantiationAwareBeanPostProcessor 接口的定義:

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {

    /**
     * Bean 實例化前調用,返回非 {@code null} 回調過後面流程
     * 返回 {@code null} 則進行 IoC 容器對 Bean 的實例化
     */
    @Nullable
    default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        return null;
    }

    /**
     * Bean 實例化之後,屬性填充之前調用,返回 {@code true} 則進行默認的屬性填充步驟,
     * 返回 {@code false} 會跳過屬性填充階段,同樣也會跳過初始化階段的生命周期方法的回調。
     */
    default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        return true;
    }

    /**
     * Bean 實例化后屬性賦值前調用,PropertyValues 是已經封裝好的設置的屬性值,返回 {@code null} 繼續
     */
    @Nullable
    default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        return null;
    }

    /**
     * 5.1 版本后已經被上面 postProcessProperties 方法所替代,功能與上面方法一樣
     */
    @Deprecated
    @Nullable
    default PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
        return pvs;
    }

}

上面接口繼承於 BeanPostProcessorBeanPostProcessor 中定義了 bean 的初始化階段生命周期回調方法,會在後續介紹)提供了三個擴展點,如下:

  • bean 實例化前
  • bean 實例化后
  • bean 屬性賦值前

這也是 bean 實例化階段的生命周期回調方法。

AbstractAutowireCapableBeanFactory#resolveBeforeInstantiation

protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
    Object bean = null;
    // 判斷bean在實例化之前是否已經解析過
    if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
        // 如果bean是合成的 && 有實現 InstantiationAwareBeanPostProcessor 接口
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            // 解析bean的類型
            Class<?> targetType = determineTargetType(beanName, mbd);
            if (targetType != null) {
                // 執行bean的實例化前回調
                bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
                // 如果實例化前生命周期回調方法返回的不是null
                if (bean != null) {
                    // 執行bean的實例化后回調,因為會跳過後續步驟,所以只能在此處調用了
                    bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
                }
            }
        }
        // 如果bean不為空,則將beforeInstantiationResolved賦值為true,代表在實例化之前已經解析
        mbd.beforeInstantiationResolved = (bean != null);
    }
    return bean;
}

創建 bean

AbstractAutowireCapableBeanFactory#doCreateBean

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {

    // 實例化 bean
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
        // 如果bean的作用域是singleton,則需要移除未完成的FactoryBean實例的緩存
        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
        // 通過構造函數反射創建bean的實例,但是屬性並未賦值,見下文詳解
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    // 獲取bean的實例
    final Object bean = instanceWrapper.getWrappedInstance(); 
    // 獲取bean的類型
    Class<?> beanType = instanceWrapper.getWrappedClass(); 
    if (beanType != NullBean.class) {
        mbd.resolvedTargetType = beanType;
    }

    synchronized (mbd.postProcessingLock) {
        if (!mbd.postProcessed) {
            try {
                // BeanDefinition 合併后的回調,見下文詳解
                applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
            } 
            // 省略異常處理...
            mbd.postProcessed = true;
        }
    }

    // bean的作用域是單例 && 允許循環引用 && 當前bean正在創建中
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    // 如果允許bean提前曝光
    if (earlySingletonExposure) {
        // 將beanName和ObjectFactory形成的key-value對放入singletonFactories緩存中
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    Object exposedObject = bean;
    try {
        // 給 bean 的屬性賦值
        populateBean(beanName, mbd, instanceWrapper);
        // 初始化 bean
        exposedObject = initializeBean(beanName, exposedObject, mbd);
    } 
    // 省略異常處理...
    
    // 如果允許單例bean提前暴露
    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        // 只有在檢測到循環依賴的情況下才不為空
        if (earlySingletonReference != null) {
            // 如果exposedObject沒有在初始化方法中被改變,也就是沒有被增強
            if (exposedObject == bean) {
                exposedObject = earlySingletonReference;
            } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
                String[] dependentBeans = getDependentBeans(beanName);
                Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
                // 檢測依賴
                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.");
                }
            }
        }
    }

    try {
        // 用於註冊銷毀bean
        registerDisposableBeanIfNecessary(beanName, bean, mbd);
    } catch (BeanDefinitionValidationException ex) {
        throw new BeanCreationException(
            mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
    }
    // 返回bean實例
    return exposedObject;
}

創建 bean 的實例

AbstractAutowireCapableBeanFactory#createBeanInstance

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
    // 解析 bean 的類型
    Class<?> beanClass = resolveBeanClass(mbd, beanName);
    // 判斷beanClass是否是public修飾的類,並且是否允許訪問非公共構造函數和方法,不是拋出異常
    if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
    }
    // Spring 5新添加的,如果存在Supplier回調,則使用給定的回調方法初始化策略。
    // 可以使RootBeanDefinition#setInstanceSupplier()設置
    Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
    if (instanceSupplier != null) {
        return obtainFromSupplier(instanceSupplier, beanName);
    }
    // 如果設置工廠方法則使用給定的方法創建bean實例,這裏分為靜態工廠和實例化工廠
    if (mbd.getFactoryMethodName() != null) {
        return instantiateUsingFactoryMethod(beanName, mbd, args);
    }

    // 快捷方式創建相同的bean
    // resolved: 構造函數或工廠方法是否已經解析過
    boolean resolved = false;
    // autowireNecessary: 是否需要自動注入 (即是否需要解析構造函數)
    boolean autowireNecessary = false;
    if (args == null) {
        synchronized (mbd.constructorArgumentLock) {
            // 如果resolvedConstructorOrFactoryMethod不為空,代表構造函數或工廠方法已經解析過
            if (mbd.resolvedConstructorOrFactoryMethod != null) {
                resolved = true;
                // 根據constructorArgumentsResolved判斷是否需要自動注入
                autowireNecessary = mbd.constructorArgumentsResolved;
            }
        }
    }
    if (resolved) {
        if (autowireNecessary) {
            // 如果構造函數或工廠方法已經解析過並且需要自動注入,則執行構造器自動注入,見下文詳解
            return autowireConstructor(beanName, mbd, null, null);
        }
        else {
            // 否則使用默認構造函數進行bean實例化,見下文詳解
            return instantiateBean(beanName, mbd);
        }
    }

    // 調用SmartInstantiationAwareBeanPostProcessor的determineCandidateConstructors()方法
    // 拿到 bean 的候選構造函數
    Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
    // 候選構造函數不為空 || 構造函數依賴注入 || 定義了構造函數的參數值 || args不為空,則執行構造器自動注入
    if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
        mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
        return autowireConstructor(beanName, mbd, ctors, args);
    }

    // 如果有首選的構造函數,使用該構造函數去創建bean實例
    ctors = mbd.getPreferredConstructors();
    if (ctors != null) {
        return autowireConstructor(beanName, mbd, ctors, null);
    }

    // 沒有特殊處理,使用默認無參構造器實例化bean
    return instantiateBean(beanName, mbd);
}

上面方法主要判斷是使用構造函數自動注入,還是使用默認構造函數構造。總結起來以下幾種情況會使用構造函數自動注入:

  • 已經緩存過構造函數並且構造函數的參數已經解析過。
  • 候選的構造函數不為空,這裏的候選構造函數是通過實現 SmartInstantiationAwareBeanPostProcessor 接口中的 determineCandidateConstructors() 方法
  • 自動注入模式為構造函數自動注入
  • BeanDefinition 定義了構造函數參數,如 XML 中的 <constructor-arg index="0" value="1"/>
  • 在調用 getBean() 方法時显示指定了 args 參數

上面方法中還有一個判斷是否有緩存的過程,是因為一個 bean 對應的類中可能會有多個構造函數,而每個構造函數的參數不同,Spring 在根據參數及類型去判斷最終會使用哪個構造函數進行實例化。但是,判斷的過程是個比較消耗性能的步驟,所以採用緩存機制,如果已經解析過則不需要重複解析而是直接從 RootBeanDefinition 中的屬性 resolvedConstructorOrFactoryMethod 緩存的值去取,否則需要再次解析,並將解析的結果添加至 RootBeanDefinition 中的屬性 resolvedConstructorOrFactoryMethod 中。

這裏簡單介紹一下 SmartInstantiationAwareBeanPostProcessor 這個接口,它繼承於 InstantiationAwareBeanPostProcessor,如下:

public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor {

    /**
     * 預測 bean 的類型
     */
    @Nullable
    default Class<?> predictBeanType(Class<?> beanClass, String beanName) throws BeansException {
        return null;
    }

    /**
     * 選擇合適的構造器,比如目標對象有多個構造器,在這裏可以進行一些定製化,選擇合適的構造器
     */
    @Nullable
    default Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, String beanName) throws BeansException {
        return null;
    }

    /**
     * 獲得提前暴露的 bean 引用,主要用於解決循環引用的問題
     * 只有單例對象才會調用此方法
     */
    default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        return bean;
    }

}

其實我們熟知的 @Autowired 註解標註在構造函數上實現自動注入,也是重寫了該接口的 determineCandidateConstructors() 方法實現的。

默認無參構造器實例化 bean

AbstractAutowireCapableBeanFactory#instantiateBean

protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
    try {
        Object beanInstance;
        final BeanFactory parent = this;
        // 使用指定的策略去實力化bean
        beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
        // 將實例化后的bean封裝成BeanWrapper后返回
        BeanWrapper bw = new BeanWrapperImpl(beanInstance);
        initBeanWrapper(bw);
        return bw;
    }
    // 省略異常處理...
}

// SimpleInstantiationStrategy.java
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
    // 如果有需要覆蓋或者動態替換的方法則當然需要使用CGLIB進行動態代理,因為可以在創建代理的同時將方法織入類中
    // 但是如果沒有需要動態改變的方法,為了方便直接用反射就可以了
    if (!bd.hasMethodOverrides()) {
        Constructor<?> constructorToUse;
        synchronized (bd.constructorArgumentLock) {
            // 獲取緩存的構造方法或工廠方法
            constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
            // 緩存為空
            if (constructorToUse == null) {
                final Class<?> clazz = bd.getBeanClass();
                // 如果clazz是接口,拋出異常
                if (clazz.isInterface()) {
                    throw new BeanInstantiationException(clazz, "Specified class is an interface");
                }
                try {
                    // 獲取默認的無參構造函數
                    constructorToUse = clazz.getDeclaredConstructor();
                    // 設置緩存
                    bd.resolvedConstructorOrFactoryMethod = constructorToUse;
                }
                catch (Throwable ex) {
                    throw new BeanInstantiationException(clazz, "No default constructor found", ex);
                }
            }
        }
        // 這裏就是用指定的無參構造器去實例化該bean,不做具體分析了
        return BeanUtils.instantiateClass(constructorToUse);
    }
    else {
        // 用CGLIB生成子類動態織入重寫的方法
        return instantiateWithMethodInjection(bd, beanName, owner);
    }
}

尋找合適的構造器實例化 bean

ConstructorResolver#autowireConstructor

protected BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, @Nullable Constructor<?>[] ctors, @Nullable Object[] explicitArgs) {
	// 尋找適合的構造器,進行實例化
    return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs);
}

public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, @Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) {

    BeanWrapperImpl bw = new BeanWrapperImpl();
    this.beanFactory.initBeanWrapper(bw);
    // 最終實例化的構造函數
    Constructor<?> constructorToUse = null;
    // 最終用於實例化的參數Holder
    ArgumentsHolder argsHolderToUse = null;
    // 最終用於實例化的構造函數參數
    Object[] argsToUse = null;
    // 如果explicitArgs不為空,則使用explicitArgs當做構造器函數參數
    if (explicitArgs != null) {
        argsToUse = explicitArgs;
    }
    else {
        Object[] argsToResolve = null;
        synchronized (mbd.constructorArgumentLock) {
            // 獲取已經緩存的構造函數或工廠方法
            constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod;
            if (constructorToUse != null && mbd.constructorArgumentsResolved) {
                // 獲取已經緩存的構造函數參數
                argsToUse = mbd.resolvedConstructorArguments;
                if (argsToUse == null) {
                    // 如果已經緩存了構造函數或工廠方法,
                    // 那麼resolvedConstructorArguments和preparedConstructorArguments必定有一個緩存了構造函數參數
                    argsToResolve = mbd.preparedConstructorArguments;
                }
            }
        }
        if (argsToResolve != null) {
            // 如果argsToResolve不為空,則對構造函數參數進行解析,也就是會進行類型轉換之類的操作
            // 例如 A(int,int),把配置中的 ("1","1") 轉換為 (1,1)
            argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true);
        }
    }
    // 如果沒有緩存構造函數或者其參數
    if (constructorToUse == null || argsToUse == null) {
        Constructor<?>[] candidates = chosenCtors;
        if (candidates == null) {
            Class<?> beanClass = mbd.getBeanClass();
            try {
                // 如果允許訪問非public的構造函數和方法(該值默認為 true),就獲取所有構造函數,否則只獲取public修飾的構造函數
                candidates = (mbd.isNonPublicAccessAllowed() ?
                              beanClass.getDeclaredConstructors() : beanClass.getConstructors());
            }
            catch (Throwable ex) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() + "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
            }
        }
        // 如果只有一個構造函數 && getBean()沒有显示指定args && 沒有定義構造函數的參數值
        if (candidates.length == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues()) {
            // 獲取構造函數
            Constructor<?> uniqueCandidate = candidates[0];
            if (uniqueCandidate.getParameterCount() == 0) {
                synchronized (mbd.constructorArgumentLock) {
                    // 設置構造函數和參數的緩存
                    mbd.resolvedConstructorOrFactoryMethod = uniqueCandidate;
                    mbd.constructorArgumentsResolved = true;
                    mbd.resolvedConstructorArguments = EMPTY_ARGS;
                }
                // 通過無參構造函數創建bean的實例,然後直接返回
                bw.setBeanInstance(instantiate(beanName, mbd, uniqueCandidate, EMPTY_ARGS));
                return bw;
            }
        }

        // 如果候選構造函數不為空 || 構造函數自動注入模式
        boolean autowiring = (chosenCtors != null || mbd.getResolvedAutowireMode() == AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
        ConstructorArgumentValues resolvedValues = null;

        int minNrOfArgs;
        // getBean()显示指定了參數,獲取參數長度
        if (explicitArgs != null) {
            minNrOfArgs = explicitArgs.length;
        }
        else {
            // 獲取定義的構造函數參數
            ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
            resolvedValues = new ConstructorArgumentValues();
            // 解析構造函數參數並賦值到resolvedValues,返回參數個數。見下文詳解
            minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
        }
        // 這裏對構造函數進行排序,規則是首先是public構造函數且參數個數從多到少,然後是非public構造函數且參數個數有多到少
        AutowireUtils.sortConstructors(candidates);
        // 最小匹配權重,權重越小,越接近我們要找的目標構造函數
        int minTypeDiffWeight = Integer.MAX_VALUE;
        Set<Constructor<?>> ambiguousConstructors = null;
        LinkedList<UnsatisfiedDependencyException> causes = null;
        // 遍歷構造函數,找出符合的構造函數
        for (Constructor<?> candidate : candidates) {
            // 獲取參數數量
            int parameterCount = candidate.getParameterCount();
            // 如果已經找到滿足的構造函數 && 目標構造函數參數個數大於當前遍歷的構造函數參數個數則終止
            // 因為構造函數已經是排過序的,後面不會再有更適合的了
            if (constructorToUse != null && argsToUse != null && argsToUse.length > parameterCount) {
                break;
            }
            // 如果目標的構造函數參數個數小於我們需要的,直接跳過
            if (parameterCount < minNrOfArgs) {
                continue;
            }

            ArgumentsHolder argsHolder;
            // 獲取到構造函數的參數類型
            Class<?>[] paramTypes = candidate.getParameterTypes();
            if (resolvedValues != null) {
                try {
                    // 評估參數名稱,就是判斷構造函數上是否標註了@ConstructorProperties註解,如果標註了,直接取其中定義的參數名稱
                    String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, parameterCount);
                    // 沒有標註@ConstructorProperties註解,使用參數名稱解析器,獲取參數名稱
                    if (paramNames == null) {
                        ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
                        if (pnd != null) {
                            paramNames = pnd.getParameterNames(candidate);
                        }
                    }
                    // 創建一個參數數組以調用構造函數或工廠方法,見下文詳解
                    // 主要是通過參數類型和參數名解析構造函數或工廠方法所需的參數(如果參數是其他bean,則會解析依賴的bean)
                    argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
                }
                catch (UnsatisfiedDependencyException ex) {
                    // Swallow and try next constructor.
                    if (causes == null) {
                        causes = new LinkedList<>();
                    }
                    causes.add(ex);
                    continue;
                }
            }
            // resolvedValues為空, explicitArgs不為空,即显示指定了getBean()的args參數
            else {
                // 如果當前構造函數參數個數不等的explicitArgs的長度,直接跳過該構造函數
                if (parameterCount != explicitArgs.length) {
                    continue;
                }
                // 把explicitArgs封裝進ArgumentsHolder
                argsHolder = new ArgumentsHolder(explicitArgs);
            }
            // 根據mbd的解析構造函數模式(true: 寬鬆模式,false:嚴格模式)
            // 將argsHolder的參數和paramTypes進行比較,計算paramTypes的類型差異權重值
            int typeDiffWeight = (mbd.isLenientConstructorResolution() ?argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
            // 差異值越小代表構造函數越匹配,則選擇此構造函數
            if (typeDiffWeight < minTypeDiffWeight) {
                constructorToUse = candidate;
                argsHolderToUse = argsHolder;
                argsToUse = argsHolder.arguments;
                minTypeDiffWeight = typeDiffWeight;
                // 如果出現權重值更小的候選者,則將ambiguousConstructors清空,允許之前存在權重值相同的候選者
                ambiguousConstructors = null;
            }
            // 兩個候選者權重值相同,並且是當前遍歷過權重值最小的
            else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) {
                // 將兩個候選者添加到ambiguousConstructors
                if (ambiguousConstructors == null) {
                    ambiguousConstructors = new LinkedHashSet<>();
                    ambiguousConstructors.add(constructorToUse);
                }
                ambiguousConstructors.add(candidate);
            }
        }
        // 沒有找到匹配的構造函數,拋出異常
        if (constructorToUse == null) {
            if (causes != null) {
                UnsatisfiedDependencyException ex = causes.removeLast();
                for (Exception cause : causes) {
                    this.beanFactory.onSuppressedException(cause);
                }
                throw ex;
            }
            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Could not resolve matching constructor (hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)");
        }
        // 如果有多個匹配的候選者,並且不是寬鬆模式,拋出異常
        else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Ambiguous constructor matches found in bean '" + beanName + "'(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " + ambiguousConstructors);
        }
        // getBean()方法沒有指定args參數 && 構造函數參數不為空
        if (explicitArgs == null && argsHolderToUse != null) {
            // 緩存解析過後的構造函數和參數
            argsHolderToUse.storeCache(mbd, constructorToUse);
        }
    }

    Assert.state(argsToUse != null, "Unresolved constructor arguments");
    // 利用反射創建bean實例
    bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse));
    return bw;
}

上面方法的功能主要如下:

  1. 構造函數參數的確定
    • 如果 explicitArgs 參數不為空,那就可以直接確定參數。因為 explicitArgs 參數是在調用 getBean() 時手動指定的,這個主要用於靜態工廠方法的調用。
    • 緩存中不為空,那麼可以直接拿過來使用。
    • BeanDefinition 中讀取,我們所定義的 bean 都會生成一個 BeanDefinition ,其中記錄了定義了構造函數參數通過 getConstructorArgumentValues() 獲取。
  2. 構造函數的確定。經過第一步已經確定構造函數的參數,接下來就是用參數個數在所有的構造函數中鎖定對應的構造函數。匹配之前會對構造函數進行排序,首先是 public 構造函數且參數個數從多到少,然後是非public 構造函數且參數個數有多到少。這樣可以迅速判斷排在後面的構造函數參數個數是否符合條件。
  3. 根據對應的構造函數轉換對應的參數類型。
  4. 根據實例化策略以及得到的構造函數和構造函數參數實例化 bean

解析構造函數參數

ConstructorResolver#resolveConstructorArguments
private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
ConstructorArgumentValues cargs, ConstructorArgumentValues resolvedValues) {
    // 獲取自定義類型轉換器
    TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
    TypeConverter converter = (customConverter != null ? customConverter : bw); 
    // 如果沒有自定義的轉換器就用bw
    BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this.beanFactory, beanName, mbd, converter);
    // minNrOfArgs初始化為indexedArgumentValues+genericArgumentValues的個數總和
    int minNrOfArgs = cargs.getArgumentCount();
    // 遍歷IndexArgumentValues,這裏的IndexArgumentValues就帶下標的,如:<constructor-arg index="0" value="1"/>
    for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cargs.getIndexedArgumentValues().entrySet()) {
        int index = entry.getKey();
        if (index < 0) {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid constructor argument index: " + index);
        } 
        // 如果index大於minNrOfArgs,修改minNrOfArgs值
        if (index > minNrOfArgs) {
            // 因為index是構造函數下標值,所以總數這邊要加1
            minNrOfArgs = index + 1; 
        }
        ConstructorArgumentValues.ValueHolder valueHolder = entry.getValue();
        // 如果參數類型已經轉換過,直接添加進resolvedValues
        if (valueHolder.isConverted()) { 
            resolvedValues.addIndexedArgumentValue(index, valueHolder);
        }
        // 參數類型沒有轉換過,進行轉換
        else { 
            Object resolvedValue =
                valueResolver.resolveValueIfNecessary("constructor argument", valueHolder.getValue());
            // 使用轉換過的參數值構建ValueHolder
            ConstructorArgumentValues.ValueHolder resolvedValueHolder = 
						new ConstructorArgumentValues.ValueHolder(resolvedValue, valueHolder.getType(), valueHolder.getName());
            resolvedValueHolder.setSource(valueHolder); 
            // 添加進resolvedValues
            resolvedValues.addIndexedArgumentValue(index, resolvedValueHolder);
        }
    }
    // 遍歷GenericArgumentValues並進行類型轉換和上面一樣,這裏的GenericArgumentValues就是沒有指定下標的
    // 如:<constructor-arg value="1"/>
    for (ConstructorArgumentValues.ValueHolder valueHolder : cargs.getGenericArgumentValues()) {
        if (valueHolder.isConverted()) {
            resolvedValues.addGenericArgumentValue(valueHolder);
        }
        else {
            Object resolvedValue =
                valueResolver.resolveValueIfNecessary("constructor argument", valueHolder.getValue());
            ConstructorArgumentValues.ValueHolder resolvedValueHolder = new ConstructorArgumentValues.ValueHolder(
                resolvedValue, valueHolder.getType(), valueHolder.getName());
            resolvedValueHolder.setSource(valueHolder);
            resolvedValues.addGenericArgumentValue(resolvedValueHolder);
        }
    }
    // 返回參數個數
    return minNrOfArgs;
}

上面方法主要將 indexedArgumentValuesgenericArgumentValues 屬性中的值通過調用 resolveValueIfNecessary() 方法進行解析;resolveValueIfNecessary() 方法主要解析參數的類型,比如 ref 屬性引用的 beanName 會通過 getBean() 返回實例。

創建參數數組

ConstructorResolver#createArgumentArray
private ArgumentsHolder createArgumentArray(String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues,BeanWrapper bw, Class<?>[] paramTypes, @Nullable String[] paramNames, Executable executable, boolean autowiring, boolean fallback) throws UnsatisfiedDependencyException {
    // 獲取類型轉換器
    TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
    TypeConverter converter = (customConverter != null ? customConverter : bw);
    // 構建ArgumentsHolder
    ArgumentsHolder args = new ArgumentsHolder(paramTypes.length);
    Set<ConstructorArgumentValues.ValueHolder> usedValueHolders = new HashSet<>(paramTypes.length);
    Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
    // 遍歷參數類型數組
    for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
        // 獲取參數類型和名稱
        Class<?> paramType = paramTypes[paramIndex]; 
        String paramName = (paramNames != null ? paramNames[paramIndex] : "");
        ConstructorArgumentValues.ValueHolder valueHolder = null;
        if (resolvedValues != null) {
            // 根據參數的下標、類型、名稱查詢是否有匹配的
            valueHolder = resolvedValues.getArgumentValue(paramIndex, paramType, paramName, usedValueHolders);
            // 沒有匹配的 && 不是自動裝配。嘗試下一個通用的無類型參數值作為降級方法
            // 它可以在類型轉換后匹配 (例如,String -> int)
            if (valueHolder == null && (!autowiring || paramTypes.length == resolvedValues.getArgumentCount())) {
                valueHolder = resolvedValues.getGenericArgumentValue(null, null, usedValueHolders);
            }
        }
        // 找到了匹配的valueHolder
        if (valueHolder != null) {
            // 添加進usedValueHolders
            usedValueHolders.add(valueHolder);
            Object originalValue = valueHolder.getValue();
            Object convertedValue;
            // 類型已經轉換過
            if (valueHolder.isConverted()) {
                // 獲取已經轉換過的值,作為args在paramIndex的預備參數
                convertedValue = valueHolder.getConvertedValue();
                args.preparedArguments[paramIndex] = convertedValue;
            }
            // 類型沒有轉換過
            else {
                // 將構造方法和參數下標封裝成MethodParameter(MethodParameter是封裝方法和參數索引的工具類)
                MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
                try {
                    // 將原始值轉換為paramType類型的值,無法轉換時拋出異常
                    convertedValue = converter.convertIfNecessary(originalValue, paramType, methodParam);
                }
                catch (TypeMismatchException ex) {
                    throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), "Could not convert argument value of type [" + ObjectUtils.nullSafeClassName(valueHolder.getValue()) + "] to required type [" + paramType.getName() + "]: " + ex.getMessage());
                }
                Object sourceHolder = valueHolder.getSource();
                if (sourceHolder instanceof ConstructorArgumentValues.ValueHolder) {
                    Object sourceValue = ((ConstructorArgumentValues.ValueHolder) sourceHolder).getValue();
                    // 標記args需要解析
                    args.resolveNecessary = true;
                    // 將sourceValue作為args在paramIndex位置的預備參數
                    args.preparedArguments[paramIndex] = sourceValue;
                }
            }
            // 將convertedValue作為args在paramIndex位置的參數
            args.arguments[paramIndex] = convertedValue;
            //  將originalValue作為args在paramIndex位置的原始參數
            args.rawArguments[paramIndex] = originalValue;
        }
        // 沒有找到匹配的valueHolder
        else {
            // 將構造方法和參數下標封裝成MethodParameter
            MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
            // 找不到明確的匹配,並且不是自動注入,拋出異常
            if (!autowiring) {
                throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), "Ambiguous argument values for parameter of type [" + paramType.getName() +
                    "] - did you specify the correct bean references as arguments?");
            }
            try {
                // 如果是自動注入,用resolveAutowiredArgument()解析參數,見下文詳解
                // 構造函數自動注入中的參數bean就是在這邊處理
                Object autowiredArgument = resolveAutowiredArgument(
                    methodParam, beanName, autowiredBeanNames, converter, fallback);
                // 將通過自動裝配解析出來的參數賦值給args
                args.rawArguments[paramIndex] = autowiredArgument;
                args.arguments[paramIndex] = autowiredArgument;
                args.preparedArguments[paramIndex] = autowiredArgumentMarker;
                args.resolveNecessary = true;
            }
            catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), ex);
            }
        }
    }
    // 如果依賴了其他的bean,則註冊依賴關係(這邊的autowiredBeanNames,就是所有依賴的beanName)
    for (String autowiredBeanName : autowiredBeanNames) {
        this.beanFactory.registerDependentBean(autowiredBeanName, beanName);
    }
	// 返回解析后的參數值
    return args;
}

上面方法判斷構造函數如果有匹配的參數會轉換成對應類型,如果沒有匹配的參數,多半是構造函數自動注入,通過 resolveAutowiredArgument() 去查找 bean 並返回實例。

ConstructorResolver#resolveAutowiredArgument
protected Object resolveAutowiredArgument(MethodParameter param, String beanName, @Nullable Set<String> autowiredBeanNames, TypeConverter typeConverter, boolean fallback) {
    // 獲取參數的類型
    Class<?> paramType = param.getParameterType();
    // 如果參數類型是InjectionPoint
    if (InjectionPoint.class.isAssignableFrom(paramType)) {
        // 拿到當前的InjectionPoint(存儲了當前正在解析依賴的方法參數信息,DependencyDescriptor)
        InjectionPoint injectionPoint = currentInjectionPoint.get();
        if (injectionPoint == null) {
            // 當前injectionPoint為空,則拋出異常:目前沒有可用的InjectionPoint
            throw new IllegalStateException("No current InjectionPoint available for " + param);
        }
        // 當前injectionPoint不為空,直接返回
        return injectionPoint;
    }
    try {
        // 解析指定依賴,DependencyDescriptor:
        // 將MethodParameter的方法參數索引信息封裝成DependencyDescriptor,見下文詳解
        return this.beanFactory.resolveDependency(
					new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
    }
    // 忽略異常處理...
}

上面方法中的 resolveDependency() 方法就是解決依賴注入的關鍵所在,在分析這個方法之前我們先簡單看一下 DependencyDescriptor 類。

public class DependencyDescriptor extends InjectionPoint implements Serializable {

    // 包裝依賴(屬性或者方法的某個參數)所在的聲明類
    private final Class<?> declaringClass;

    // 如果所包裝依賴是方法的某個參數,則這裏記錄該方法的名稱
    @Nullable
    private String methodName;

    // 如果所包裝的是方法的某個參數,則這裏記錄該參數的類型
    @Nullable
    private Class<?>[] parameterTypes;

    // 如果所包裝的是方法的某個參數,則這裏記錄該參數在該函數參數列表中的索引
    private int parameterIndex;

    // 如果所包裝的是屬性,則這裏記錄該屬性的名稱
    @Nullable
    private String fieldName;

    // 標識所包裝依賴是否必要依賴
    private final boolean required;

    // 標識所包裝依賴是否需要飢餓加載
    private final boolean eager;

    // 標識所包裝依賴的嵌套級別
    private int nestingLevel = 1;

    // 標識所包裝依賴的包含者類,通常和聲明類是同一個
    @Nullable
    private Class<?> containingClass;
    
    // 省略其他代碼...
    
}

這個類就是依賴描述符,存儲了需要注入 bean 的類型、構造器參數的下標(構造器注入該值不為空)、是否必需、字段名稱(字段注入該值不為空)、方法名稱(set 方法注入該值不為空)等。

依賴解決

DefaultListableBeanFactory#resolveDependency
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

    descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
    // Optional類型的處理,說明Spring也可以注入Optional類型的參數
    if (Optional.class == descriptor.getDependencyType()) {
        return createOptionalDependency(descriptor, requestingBeanName);
    }
    // ObjectFactory或ObjectProvider類型的處理
    else if (ObjectFactory.class == descriptor.getDependencyType() ||
             ObjectProvider.class == descriptor.getDependencyType()) {
        return new DependencyObjectProvider(descriptor, requestingBeanName);
    }
    // javax.inject.Provider類型的處理
    else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
        return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
    }
    else {
        // 獲取延遲解析代理
        Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
					descriptor, requestingBeanName);
        if (result == null) {
            // 解析依賴,返回的result為最終需要注入的bean實例,見下文詳解
            result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
        }
        return result;
    }
}
DefaultListableBeanFactory#doResolveDependency
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

    InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
    try {
        // 獲取需要注入bean的快捷方式,不為空直接返回
        Object shortcut = descriptor.resolveShortcut(this);
        if (shortcut != null) {
            return shortcut;
        }
        // 獲取需要注入bean的類型
        Class<?> type = descriptor.getDependencyType();
        // 用於支持Spring中新增的註解@Value(確定給定的依賴項是否聲明@Value註解,如果有則拿到值)
        Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
        if (value != null) {
            if (value instanceof String) {
                String strVal = resolveEmbeddedValue((String) value);
                BeanDefinition bd = (beanName != null && containsBean(beanName) ?
                                     getMergedBeanDefinition(beanName) : null);
                value = evaluateBeanDefinitionString(strVal, bd);
            }
            TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
            try {
                return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
            }
            catch (UnsupportedOperationException ex) {
                return (descriptor.getField() != null ?
                        converter.convertIfNecessary(value, type, descriptor.getField()) :converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
            }
        }
        // 解析MultipleBean,例如 Array,Collection,Map
        Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
        if (multipleBeans != null) {
            return multipleBeans;
        }
        // 根據類型找到匹配的bean
        // matchingBeans(key: beanName value: 如果bean已經緩存了實例(例如單例bean會緩存其實例),
        // 就是bean的實例,否則就是對應的class對象)
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
        if (matchingBeans.isEmpty()) {
            // 沒有找到匹配的bean,判斷是不是必需的,不是直接返回null,否則拋出異常
            if (isRequired(descriptor)) {
                raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
            }
            return null;
        }

        String autowiredBeanName;
        Object instanceCandidate;
        // 如果有多個匹配的候選者
        if (matchingBeans.size() > 1) {
            // 判斷最佳的候選者,也就是尋找最匹配的beanName
            autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
            if (autowiredBeanName == null) {
                if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
                    return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
                }
                else {
                    return null;
                }
            }
            // 拿到autowiredBeanName對應的value(bean實例或bean實例類型)
            instanceCandidate = matchingBeans.get(autowiredBeanName);
        }
        else {
            // 只找到一個符合的bean
            Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
            autowiredBeanName = entry.getKey();
            instanceCandidate = entry.getValue();
        }

        if (autowiredBeanNames != null) {
            // 將依賴的beanName添加到autowiredBeanNames中
            autowiredBeanNames.add(autowiredBeanName);
        }
        // 如果需要注入的bean沒有緩存實例,那麼instanceCandidate是一個Class對象,再根據getBean()去獲取對應的實例
        if (instanceCandidate instanceof Class) {
            instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
        }
        Object result = instanceCandidate;
        if (result instanceof NullBean) {
            if (isRequired(descriptor)) {
                raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
            }
            result = null;
        }
        if (!ClassUtils.isAssignableValue(type, result)) {
            throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
        }
        // 返回最終需要注入的bean實例
        return result;
    }
    finally {
        ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
    }
}

上面方法才是真正去獲取需要注入的 bean,大概分為以下幾個步驟:

  1. 查看是否有快捷方式獲取注入 bean 是否為空,不為空直接返回。這裏的快捷方式是通過繼承 DependencyDescriptor 並重寫 resolveShortcut() 來實現。

  2. 如果參數使用 @Value 註解修飾了,如果獲取到值直接返回。

  3. 解析 MultipleBean,這裏的 MultipleBean 一般是 ArrayCollectionMap 這種,不為空直接返回。

  4. 根據類型找到所有匹配的 beanmatchingBeanskeybeanNamevalue 的值有兩種情況,如果bean已經緩存了實例(例如單例bean會緩存其實例),就是bean的實例,否則就是對應的class對象)。

  5. matchingBeans 為空,判斷需要注入的 bean 是否是必須的,如果是拋出異常,否則返回 null

  6. matchingBeans 長度大於1,代表有多個候選者;選擇最佳的候選者,規則是:

    1. 首先查找 primary 屬性為 true 的。
    2. 查找優先級最高的,實現 PriorityOrdered 接口或者標註 @Priority 註解的。
    3. 查找名稱匹配的。
  7. 只有一個候選者,直接使用。

  8. 如果需要注入的 bean 沒有緩存實例,那麼 instanceCandidate是一個 Class 對象,再根據 getBean() 方法去獲取對應的實例。

  9. 最終返回需要注入的 bean 實例。

BeanDefinition 合併后處理

AbstractAutowireCapableBeanFactory#applyMergedBeanDefinitionPostProcessors

protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof MergedBeanDefinitionPostProcessor) {
            MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
            bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
        }
    }
}

上面代碼很簡單,無非就是拿到所有註冊的 BeanPostProcessor ,然後遍歷判斷是否是 MeragedBeanDefinitionPostProcessor 類型,是的話進行 BeanDefinition 合併后的方法回調,在這個回調方法內你可以對指定 beanBeanDefinition 做一些修改。

下面我們簡單看一下 MergedBeanDefinitionPostProcessor 接口中的方法:

public interface MergedBeanDefinitionPostProcessor extends BeanPostProcessor {

    /**
     * 對指定bean的BeanDefinition合併后的處理方法回調
     */
    void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName);

    /**
     * 通知已重新設置指定beanName的BeanDefinition,如果實現該方法應該清除受影響的bean的所有元數據
     * @since 5.1
     */
    default void resetBeanDefinition(String beanName) {
        
    }

}

總結

本文主要介紹了創建 bean 實例的流程,我們可以重新梳理一下思路:

  1. 進行 bean 的實例化前方法回調,如果返回非空,跳過後面步驟
  2. 創建 bean 的實例,如果是構造函數注入會選擇最適合的構造函數進行參數自動注入,否則調用默認的無參構造進行實例化 bean

由於 doCreateBean() 方法中操作太多,這裡會分為幾篇文章,一一分析各個階段。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

參考

  • 《Spring 源碼深度解析》—— 郝佳
  • https://blog.csdn.net/andy_zhang2007/article/details/88135669
  • https://fangjian0423.github.io/2017/06/20/spring-bean-post-processor/
  • https://blog.csdn.net/v123411739/article/details/87994934

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

從零開始認識堆排序

一、什麼是堆?

維基百科的解釋是:堆是一種特別的樹狀數據結構,它需要滿足任意的子節點必須都大於等於(最大堆)或者小於等於(最小堆)其父節點。

二、堆排序

堆排序是通過二叉堆數據結構實現,二叉堆滿足一下兩個特性:

1、滿足對的基本特性

2、完全二叉樹,除了最底層外,其它層都已填充滿,且是從左到右填充。

二叉堆的高度即為根節點到恭弘=叶 恭弘子節點的最長簡單路徑長度,即為θ(lgn)。

二叉堆上的操作時間複雜度為O(lgn)。

1、二叉堆中的元素個數

根據二叉堆的特性2,我們知道高度為h的二叉堆重元素個數如下:

根節點為1

第一層為2=21

第二層為4=22

第h-1層為2h-1

第h層元素個數範圍為[1,2h]

最底層之外的元素個數和為1+2+22+…+2h-1=(1-2h-1)/(1-2)=2h-1

高度為h的二叉堆元素個數範圍:[2h-1 + 1,2h-1+2h]=[2h,2h+1-1]

以高度為3的最大堆為例:

圖1

 圖2

2、二叉堆的高度

由二.1推導,我們知道高度為h的二叉堆的元素個數n滿足:

2≦ n ≦ 2h+1-1

=>

2≦ 2lgn ≦ 2h+1-1

=>

h ≦ lgn < h+1

由此可得,含有n個元素的二叉堆的高度為θ(lgn)

3、使用數組表示堆存儲

節點下標 i,則父節點下標為 i/2,左子節點下標為 2i,右子節點下標 2i + 1。

以圖1最大堆為例:

從根節點開始,根節點下標 1。

第一層節點下標:2、3

第二層節點下標:4、5、6、7

第三層節點下標:8

圖3

數組形式:

 圖4

具體到特定的編程語言,數組以0開始下標的,推導:

對於節點 i,則其父節點為 (i – 1)/2,左子節點下標為 2i + 1,右子節點下標 2i + 2。

4、堆的恭弘=叶 恭弘子節點

對於有n個元素的二叉堆,最後一個元素的下標為為n,根據二叉堆的性質,其父節點下標為n/2,因為每一層是由左向右進行構建,所以其父節點也是倒數第二層的最後一個節點,所以,其後的節點都為最底層節點,為恭弘=叶 恭弘子節點,下標為n/2 + 1、n/2 + 2… n。

具體到特定的編程語言,數組以0開始下標的,推到:

恭弘=叶 恭弘子節點下標為(n-1)/2 + 1、(n-1)/2 + 2… n。

5、堆維護

所謂堆維護,即保持堆的基本特性,以最大堆為例:給定某個節點,維護使得以其為根節點的子堆為滿足子節點都小於等於父節點。

如下,給定堆構建數組,及特定元素下標i:

public static void maxHeapify(int[] arr, int i) {
        int size = arr.length; //堆大小
        int maxIndex = i; //記錄當前節點及其子節點的最大值節點索引
        int left = 2 * i + 1; //左子節點索引
        int right = 2 * i + 2; //右子節點索引

        //對比節點及其左子節點
        if (left < size && arr[left] > arr[maxIndex]) {
            maxIndex = left;
        }

        //對比節點及其右子節點
        if (right < size && arr[right] > arr[maxIndex]) {
            maxIndex = right;
        }

        //不滿足最大堆性質,則進行下沉節點i,遞歸處理
        if (maxIndex != i) {
            int tmp = arr[i];
            arr[i] = arr[maxIndex];
            arr[maxIndex] = tmp;
            maxHeapify(arr, maxIndex);
        }
    }

如下圖,堆中元素9的維護過程:

 

 圖5

堆維護過程的時間複雜度:O(lgn)。

6、構建堆

根據二.4我們可以得到所有恭弘=叶 恭弘子節點的下標。我們可以使用二.5中的堆維護過程,對所堆中所有的非恭弘=叶 恭弘子節點執行堆維護操作進行堆的構建。

public static void buildHeap(int[] arr) {
        for (int i = (arr.length - 1) / 2; i >= 0; i--) {
            maxHeapify(arr, i);
        }
    }

以數組 {27,17,3,16,13,10,1,5,7,12,4,8,9,0} 為例進行堆構建,結果為:{27,17,10,16,13,9,1,5,7,12,4,8,3,0}

圖6

構建最大堆的時間複雜度為O(n)。

7、堆排序

首先執行最大堆構建,當前堆中最大值會上升到根節點,也就是堆數組的首節點。

我們可以通過交換首尾節點,使得最大值轉移至尾部,然後對除尾部元素外的堆數組執行根元素堆維護,上浮堆最大值。

然後,將最大值交換至數組尾部倒數第二個元素位置,重新執行剩餘堆數組的根元素堆維護,依次類推,直至剩餘堆數組大小變為2為止。

以二.6中數組為例:{27,17,3,16,13,10,1,5,7,12,4,8,9,0}

第一次執行:

{27,17,10,16,13,9,1,5,7,12,4,8,3,0},max:27

第二次執行:

{17,16,10,7,13,9,1,5,0,12,4,8,3},max:17

第三詞執行:

{16,13,10,7,12,9,1,5,0,3,4,8},max:16

第四次執行:

{13,12,10,7,8,9,1,5,0,3,4},max:13

第五次執行:

{12,8,10,7,4,9,1,5,0,3},max:12

第六次執行:

{10,8,9,7,4,3,1,5,0},max:10

第七次執行:

{9,8,3,7,4,0,1,5},max:9

第八次執行:

{8,7,3,5,4,0,1},max:8

第九次執行:

{7,5,3,1,4,0},max:7

第十次執行:

{5,4,3,1,0},max:5

第十一次執行:

{4,1,3,0},max:4

第十二次執行:

{3,1,0},max:3

第十三次執行:

{1,0},max:1

改造代碼實現:

    /**
     * 最大堆維護
     *
     * @param arr 堆數組
     * @param i 維護元素下標
     * @param offSet 原址偏移量
     */
    public static void maxHeapify(int[] arr, int i, int offSet) {
        int size = arr.length - offSet; //堆大小
        int maxIndex = i; //記錄當前節點及其子節點的最大值節點索引
        int left = 2 * i + 1; //左子節點索引
        int right = 2 * i + 2; //右子節點索引

        //對比節點及其左子節點
        if (left < size && arr[left] > arr[maxIndex]) {
            maxIndex = left;
        }

        //對比節點及其右子節點
        if (right < size && arr[right] > arr[maxIndex]) {
            maxIndex = right;
        }

        //不滿足最大堆性質,則進行下沉節點i,遞歸處理
        if (maxIndex != i) {
            int tmp = arr[i];
            arr[i] = arr[maxIndex];
            arr[maxIndex] = tmp;
            //因為交換了子節點的值,則以子節點為根節點的子堆特性可能發生變化,需要維護
            maxHeapify(arr, maxIndex, offSet);
        }
    }

    /**
     * 構建最大堆
     *
     * @param arr
     */
    public static void buildHeap(int[] arr) {
        for (int i = (arr.length - 1) / 2; i >= 0; i--) {
            maxHeapify(arr, i, 0);
        }
    }

    /**
     * 交換最大值
     *
     * @param arr 堆數組
     * @param maxIndex 最大值元素待交換位置
     */
    public static void swapMax(int[] arr, int maxIndex) {
        int tmp = arr[maxIndex];
        arr[maxIndex] = arr[0];
        arr[0] = tmp;
    }

    /**
     * 堆排序
     *
     * @param arr
     */
    public static void heapSort(int[] arr) {
        buildHeap(arr); //構建堆
        swapMax(arr, arr.length - 1); //交換最大值
        for (int i = 0; i < arr.length - 2 ; i++) {
            maxHeapify(arr, 0, i + 1); //根節點堆維護 offset 偏移元素個數
            swapMax(arr, arr.length - 1 - (i + 1)); //交換最大值
        }
    }

堆排序時間複雜度:O(nlgn)

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

查找算法系列文(一)一文入門二叉樹

微信公眾號:小超說

這是查找算法系列文章的第一篇,助你快速入門二叉樹

什麼是樹(Tree)?

我們首先來看一些圖片:

其中,第一、二、四個都是樹,第三個不是。樹的特點很明顯吧!

其中每個元素叫做“節點”;用來連接相鄰節點之間的關係,我們稱之為“父子關係”。例如在圖一中,A節點是B節點的父節點,B節點是A節點的子結點,同時,B節點和Q節點是同一個父節點A的子節點,所以它們之間互相成為兄弟節點。我們把沒有父節點的節點稱為根節點,也就是圖一中的A節點。我們把沒有子節點的節點稱為恭弘=叶 恭弘子節點,比如圖一中的D、E、F、G節點。這些概念都是顯而易見,但卻是最基本的東西。

二叉樹

二叉樹,自然就是每個節點最多有兩個分支,即兩個子節點的一種樹,兩個分支分別稱為左子樹右子樹。還是那上面那張圖舉例,圖一、圖二和圖四都是二叉樹,因為它們每個節點都最多含有兩個子節點。其中,圖一又稱為滿二叉樹,圖四又稱為完全二叉樹。而之所以出現完全二叉樹的概念,其實是基於二叉樹的物理存儲方式。

二叉樹的存儲方法

  • 基於鏈表的鏈式存儲法
  • 基於數組的順序存儲法

鏈式存儲法:

我們為每個節點創建一個Node對象:

class Node{
		int data;
		Node left,right;
}

每個節點都是一個Node對象,它包含我們所需要存儲的數據,指向左子節點的引用,直向右子節點的引用,就像鏈表一樣將整個樹串起來。如果該節點沒有左子節點,則Node.left==null或者Node.right==null.

順序存儲法

我們把根節點儲存在下標為i=1的位置,那麼左子節點儲存在2*i=2的位置,右子節點儲存在下標為2*i+1=2的位置。依此類推,完成樹的存儲。藉助下標運算,我們可以很輕鬆的從父節點跳轉到左子節點和右子節點或者從任意一個子節點找到它的父節點。如果X的位置為i,則它的兩個子節點的下標分別為2i2i+1,它的父節點的位置為i/2(這裏結果向下取整)。

具體如下圖所示:可以發現,只有完全二叉樹存儲的效率才最高,最省內存

二叉樹的遍歷

二叉樹的遍歷操作主要有三種

  • 前序遍歷
  • 中序遍歷
  • 後序遍歷

前序遍歷是指,對於樹中的任意節點來說,先打印這個節點,然後再打印它的左子樹,最後打印它的右子樹。

中序遍歷是指,對於樹中的任意節點來說,先打印它的左子樹,然後再打印它本身,最後打印它的右子樹。

後序遍歷是指,對於樹中的任意節點來說,先打印它的左子樹,然後再打印它的右子樹,最後打印這個節點本身。

注意,這其中有點遞歸的味道

還是以剛才的圖為例:

  • 前序遍歷:A->B->D->E->C->F
  • 中序遍歷:D->B->E->A->F->C
  • 後序遍歷:D->E->B->F->C->A

具體的代碼實現(寫出遞歸即可):

public void preOrder(Node root){
	if(root==null) return;
	System.out.println(root.data);//打印root節點的值
    preOrder(root.left);
    preOrder(root.right);
}

public void inOrder(Node root){
	if(root==null) return;
	inOrder(root.left);
	Systrm.out.println(root.data);
	inOrder(root.right);
}

public void inOrder(Node root){
	if(root==null) return;
	inOrder(root.left)
	inOrder(root.right);
	Systrm.out.println(root.data);
}

二叉樹遍歷的時間複雜度是O(n),這是因為每個節點最多會被訪問兩次,(遞歸時函數進棧和出棧),所以遍歷操作的訪問次數跟節點的個數 n 成正比,也就是說二叉樹遍歷的時間複雜度是 O(n)。

展望

經過上面的介紹,我們已經大致對二叉樹有了一個基本的認識,那麼,二叉樹存在的意義是什麼呢?我們可以基於這種數據結構設計出哪些高效的算法呢?下一次我們將介紹二叉查找樹(Binary Search Tree),我們將定義一種數據結構並維護它的性質。

題外話:對於算法初學者,推薦一本十分 nice 的書籍 《算法第四版》,裏面各種配圖十分詳細。如果你需要电子版文件,後台回復算法4即可獲得下載鏈接。後台回復 算法01 送你一份 算法與數據結構思維導圖。最後,希望我們一起進步,一起成長!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

程序員敲代碼時耳機里聽的到底是什麼?

我是風箏,公眾號「古時的風箏」,一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。

程序員上班戴耳機聽歌難道不是正常的嗎,真的還有公司不允許程序員戴耳機的嗎?不戴耳機能寫代碼?

那程序員的耳機里聽的是什麼呢?我採訪了一眾程序員朋友。

鋼鐵程序員王二麻子同學

聽的什麼?我根本就不知道,我只是不想讓別人打擾我

有時候開發確實是比較費腦子的,尤其是遇到複雜邏輯的時候。正當思如泉涌、靈感迸發的時候,旁人看着我坐在那裡一動不動,好像什麼都沒有做,其實我腦子里正在構思一個複雜的流程。

這時候,突然有個人走過來打斷我,前面的思考都白費了,你說傷心不,你說氣人不。

所以,為了防止上面的情況出現,不得不戴上耳機。至於聽什麼,不重要,我只是告訴別人,別過來,我現在沒時間。

生活需要儀式感的劉精神同學

聽什麼不重要,關鍵是儀式感

生活需要儀式感,寫代碼也需要儀式感啊。當我戴上耳機的那一刻,我感覺精神抖擻,彷彿遊離的三魂六魄都回來了,寫代碼更有動力。

要是不戴耳機,感覺渾身無力,只想摸魚,寫代碼什麼的,根本想都想不起來。

心疼自己的高愛己同學

其實我就是不想聽我的机械鍵盤聲音,實在太吵了

你也知道,筆記本自帶的鍵盤總感覺軟綿綿的,敲起來實在不給力,嚴重影響我的工作效率。那必須得買個鍵盤啊,在多個朋友的推薦下,我就買了一個青軸鍵盤。你別說,觸感真不錯,每按一下,都感覺指尖有一股電流滑過,同時伴隨着啪啪啪的聲音,感覺寫代碼效率明顯變高了,更神奇的是 bug 都比以前少了,你說神奇不。

但是呢,有個缺點,就是時間久了吧,感覺稍微有那麼一點點吵,所以我就配了個耳機,從此之後,既得到了直達靈魂的觸感,又不會感覺吵了。更神奇的一點是,以前罵我的同事好像都不罵我了。

真的在聽東西的同學

雖然存在以上幾位同學說的情況,但大多數情況下, 我們是真的在聽東西。比如我,為了聽高品質的,還專門買了網易雲音樂的 VIP 。

為什麼是東西,而不是音樂呢,因為有的人不是在聽音樂。

我聽說有人在寫代碼的時候聽評書,更有厲害的,竟然聽相聲。那我想只有三種可能。

  1. 根本沒在寫代碼,可能是在摸魚。
  2. 評書一點都不精彩,相聲一點都不好笑,僅僅是一門語言的藝術而已。
  3. 這是個大佬,已經進入忘我的境界,聽到的不是語言,而是聲音解析成比特位時產生的白噪音,一般人聽不到,只有某些段位的大佬可以。

當然畢竟大佬不常有,而普通群眾常用。大部分人聽的確實是音樂。比如我吧,我戴耳機真的是在聽音樂,為了降噪、減少干擾,提高專註力,提升效率。

什麼類型的音樂更受程序員歡迎呢

英文歌

英文歌是大多數程序員的最愛。請看網易雲音樂給我的每日歌曲推薦,除了伍佰的一首中文歌亂入,剩下的都是英文歌。

你真的認為我英語很好嗎,正好相反,之所以英文歌那麼受歡迎,就是因為聽不懂歌詞是什麼意思,這樣才不會被歌詞帶跑偏,沒錯,我們聽的就是這個節奏。

那要是中文歌就不一樣了,比如說當我聽到「你的酒館對我打了烊,子彈在我心頭上了膛」的時候,我就以為真的要打烊了,子彈真的要上膛了,從而引發一系列思考,酒館為什麼對我打烊,子彈為什麼要上膛,生意不做了嗎,刑法基本法則不懂嗎。

為了證明這一點,我到網易雲音樂上搜了「程序員」相關的歌單,點進去一看,大部分也都是英文歌,看來大家英文都不太好。

然後我又搜了「產品狗」相關的歌單,同樣也是英文歌為主,可見我們雖然不太對付,請參考歌單『產品狗如何說服程序猿』和『程序員如何回應產品狗』,但是方法論還是差不多的。

純音樂

純音樂也是很受歡迎的,我有個同事最喜歡聽貝多芬的命運交響曲,我就沒那麼文藝了。我一般都是聽那種激昂的小提琴或者聽完感覺自己馬上要登基了的那種,不容易犯困。

白噪音

小提琴太勁爆,不能常聽,犯困的下午聽聽可以提神,bug 太多又不想改的時候可以聽聽。大多數時候,不需要那麼亢奮,保持內心的平靜就是寫代碼最好的狀態。那就聽聽白噪音好了,比如雨聲、風聲、溪水聲、鳥唱蟬鳴。

我最喜歡的就是雨聲,嘩啦啦的大雨,再配上驚雷,簡直不要太平靜,innerpeace。

比如下面這個,一聽就是一個小時。

保留曲目

每個人都有自己家私藏的歌單,百聽不厭的那種,當然每個人的品位不一樣、口味兒不一樣,比如當年有個哥們兒異常興奮的把他的珍藏歌單分享給我,我當即表示很感動,一定要認真聽一聽。可當我硬着頭皮聽完第二首的時候,我內心是拒絕的,只能委婉的跟哥們兒說:這 TM 什麼玩意兒。當然是不會破壞友誼的小船的,如果是不熟的朋友,那肯定會豎起大拇指,並且連連點頭稱讚:真不錯,品位棒棒噠。

我沒有薦歌啊,向別人推薦歌曲犹如喂別人食物,你覺得好吃別人並一定覺得好吃。下面是我的 2018 年年度聽歌報告,Sophie Zelmani(蘇菲.珊曼妮)的熱門50單曲就是我的保留曲目,白聽不厭,而且更重要的不仔細聽,還是聽不出歌詞的意思,正好適合寫代碼用。

另外,作為一個程序員鼓勵師,為了鼓勵我自己,我也創建了一個「程序員鼓勵師」的歌單,經常拿出來聽聽接收鼓勵。

作為程序員的你,耳機里有什麼特殊的內容嗎?

壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

深入淺出騰訊BERT推理模型–TurboTransformers

Overview

TurboTransformers是騰訊最近開源的BERT推理模型,它的特點就是一個字,快。本人用BERT(huggingface/transformers)在V100上做了測試,測試結果和官宣的基本一致:TurboTransformers的推理速度要比Pytorch快上1~4倍。

它之所以快,是因為它是專用於BERT的輕量級推理模型。

分層

不管是計算機的硬件、軟件,還是現在的深度學習,它們都遵循着一個很重要的設計思想–分層:

  • 用簡單的代碼(或電路)來實現一個基本功能組件。
  • 用幾個基本組件組合成一個功能更強的複雜組件。
  • 從簡單到複雜,像搭積木一樣,一層層地搭建出擁有很強功能的組件。

開發者只需要基於PyTorch的幾個基本組件就能搭建出BERT模型,而且這些組件本身對他們來說都是透明的。正因如此,PyTorch才越來越受到研究者青睞。

分層設計的優點很多,例如,可以簡化問題、降低創新門檻、加速開發等,但它的缺點也很明顯:

  • 流程固定化
  • 存在中間層延遲

深度神經網絡里有個經典套路:一個激活函數層後面緊跟着一個dropout層。PyTorch需要lanuch兩個GPU kernel程序來完成這兩步計算。

F.dropout(F.relu(x))

實際上,這兩項計算都是element-wise的,是可以合併成一個kernel的。但目前來說,不管是PyTorch,還是其他的通用訓練框架,它們都很少有提供這種融合計算的API。

至於中間層延遲,最經典的要屬“hello world”程序。雖然只有幾行代碼,但實際上要經過的中間層數根本數不過來。

你可以閱讀深入淺出PyTorch(算子篇)來了解下矩陣相乘這個最基本的計算在PyTorch里要經過多少个中間層。

分層展開

要想將程序的低延遲最大化,就需要把分層的代碼完全展開,並重構代碼。典型例子就是嵌入式系統,為了實現某種需求,它可以打破應用程序、程序庫、操作系統甚至是硬件設備的界限,打造一個軟硬件一體化產品。

這種分層展開的設計模式當然也有它的局限性:專用。由於高度定製化,它通常只能用於完成某個特定功能。低延遲和專用化是呈絕對的正相關的。

TurboTransformers就是採用這種設計:只實現BERT模型前向傳播所需要的算子,並融合那些可以合併的算子。

turbo.Tensor

首先,它用CUDA開發了一個輕量級的tensor計算庫,所謂的輕量級,指的是不用考慮反向傳播、稀疏矩陣等操作,只實現BERT前向傳播所必需的operator。

雖然tensor庫是用C++寫的,但考慮到python在AI開發中的地位,它用pybind11將C++ API暴露給前端的python Tensor類。

# turbo_transformers/python/pybind.cpp
 72   py::class_<core::Tensor>(m, "Tensor")                      
 73       .def_static("from_dlpack",
 74                   [](py::capsule capsule) -> std::unique_ptr<core::Tensor> {
 75                     auto tensor = (DLManagedTensor *)(capsule);
 76                     PyCapsule_SetName(capsule.ptr(), "used_tensor");
 77                     return absl::make_unique<core::Tensor>(tensor);
 78                   })
 79       .def("to_dlpack",
 80            [](core::Tensor &tensor) -> py::capsule {
 81              auto *dlpack = tensor.ToDLPack();                    
 82              return py::capsule(dlpack, "dltensor", DLPack_Capsule_Destructor);
 83            })
 84       .def("n_dim", &core::Tensor::n_dim)
 85       .def("shape", &core::Tensor::shape)

從預訓練模型(PyTorch)那遷移參數時,turbo.Tensor不能直接對接torch.Tensor,需要先將PyTorch的參數轉成dlpack格式, 再通過from_dlpack()將這些數據導入生成TurboTransformers tensor。除了dlpack之外,還支持*.npz文件格式。

turbo.xxxlayer

TurboTransformers用CUDA重構了Embedding、self-attention、intermediate、output、LayerNorm和pooler等layer。turbo.layer不僅代碼結構簡潔,overhead少,還合併了一部分算子。

這裏以intermediate layer為例,來分析這些算子的特點。

intermediate layer的實現比較簡單:一個Linear layer後面緊跟着一個gelu activation layer。

PyTorch的intermediate layer的會lanuch 3個kernel來完成這部分計算:

  • #1: y = input.matmul(weight)
  • #2: y = y + bias
  • #3: y = gelu(y)

由於#2和#3都是element-wise kernel,turbo把它們進行了融合–AddBiasAct(),相同的計算操作,只需要lanuch 2個kernel,計算速度當然更快。

和PyTorch一樣,turbo的MatMul算子也是調用cuBLAS來進行矩陣運算,而且turbo還啟用了Tensor Core來加速計算(CUBLAS_TENSOR_OP_MATH)。

總結

到此,本文基本上講清了TurboTransformers的速度優勢來源,由於篇幅所限,不能分析所有的算子。BERT的核心模塊是self-attention,如果想了解更多,可以閱讀深入淺出Transformer。

更多精彩文章,歡迎掃碼關注下方的公眾號 ~~ 歡迎關注和點贊,你的鼓勵將是我創作的動力

歡迎轉發至朋友圈,公眾號轉載請後台留言申請授權~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

.NET高級調試系列-Windbg調試入門篇

Windbg是.NET高級調試領域中不可或缺的一個工具和利器,也是日常我們分析解決問題的必備。準備近期寫2篇精華文章,集中給大家分享一下如果通過Windbg進行.NET高級調試。

今天我們來一篇入門的文章。首先,Windbg是什麼?

Windows Debugger,簡稱WinDbg,.NET 最強分析調試利器。它可以用來:

  • 調試內核模式和用戶模式代碼
  • 分析Crash dump
  • 分析代碼執行時 CPU 寄存器信息

我們可以通過WinDbg調試以下具體問題:

  • 線程阻塞
  • 內存泄露
  • 分析查詢運行時線程堆棧和變量
  • 分析進程Crash原因
  • 分析消耗CPU原因
  • 查看並調試CLR異常

那麼,首先我們先進行Windbg下載安裝、配置。

一、下載安裝WinDbg,配置調試環境

1. 推薦下載鏈接

https://raw.githubusercontent.com/EasyDarwin/Tools/master/Windbg_x86_x64/dbg_amd64.msi

或者從Windows Store下載 WingDbg Preview版本

下載后一步一步安裝即可

 2. 配置調試符號

大家會問一個問題:為什麼要配置調試符號?

若要使用 WinDbg 提供的所有高級功能,必須加載適當的符號:比如說我們可以調試、查看.NET CLR程序堆棧,此時要加載對應的調試符號。

 微軟提供了統一的調試服務服務器地址:

http://msdl.microsoft.com/download/symbols,將這個地址提供的調試符號,下載緩存到本地,Windbg調試的時候可以用上。
srv*c:\symcache*http://msdl.microsoft.com/download/symbols;c:\symcache

  

3. 下載並使用WinDbg調試器擴展

 Windbg調試器擴展是Windbg調試的精華和核心,可以這麼說,掌握各類Windbg調試器擴展,你就掌握了各類調試技能。

 默認情況下,WinDbg的調試指令是有限的,通過一些WinDbg調試器擴展,可以方便我們進行.NET 程序調試

 SOS調試擴展 : 隨着.NET Framework安裝,可以直接加載:  .load sos clr

 SOS這個dll在哪裡呢(分32位和64位)?

 4.0, 32-bit –> C:\Windows\Microsoft.NET\Framework\v4.0.30319
 4.0, 64-bit –> C:\Windows\Microsoft.NET\Framework64\v4.0.30319

  MEX調試擴展:This extension is widely used by Microsoft Support Engineers in troubleshooting process applications

 下載地址:https://www.microsoft.com/en-us/download/details.aspx?id=53304

 下載完成后,將32/64位的Mex.dll 拷貝到windbg安裝目錄中

 例如:C:\Program Files\Debugging Tools for Windows (x64)\Mex.dll

 詳細使用說明:https://github.com/REhints/WinDbg/tree/master/MEX

 下載、安裝、配置完成Windbg之後,接下來我們了解一下一些基本的調試命令。

二、基本的WinDbg調試指令

1. WinDbg自帶的調試指令

 

   

    更多指令,可以查看一下鏈接:

    https://docs.microsoft.com/zh-cn/windows-hardware/drivers/debugger/getting-started-with-windbg

 2. SOS調試擴展常用的調試指令

   

    

    

 

  

  

  

  

 

  

   3. Mex調試擴展常用的調試指令

   

     

    

    

    

    

     更多Mex調試指令,可以查看鏈接:https://github.com/REhints/WinDbg/tree/master/MEX

 

  以上是整個Windbg調試入門篇的介紹,希望大家能夠掌握,下一篇我們將通過一些具體的案例,示意各個指令的使用場景。

 

周國慶

2020/6/27

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

GCC編譯和鏈接過程

GCCGNU Compiler CollectionGNU編譯器套件),是由 GNU 開發的編程語言編譯器。它是以GPL許可證所發行的自由軟件,也是 GNU計劃的關鍵部分。GCC原本作為GNU操作系統的官方編譯器,現已被大多數類Unix操作系統(如LinuxBSDMac OS X等)採納為標準的編譯器,GCC同樣適用於微軟的Windows

 

一、源代碼

 

 

二、編譯過程

 

1)預處理:

 

 預處理就是將要包含(include)的文件插入原文件中、將宏定義展開、根據條件編譯命令選擇要使用的代碼,最後將這些代碼輸出到一個“.i”文件中等待進一步處理。

結果:見文件0617_Demo.i

 

 2)編譯

 

 

 編譯就是把C/C++代碼(比如上面的“.i”文件)“翻譯”成彙編代碼。

結果:見文件0617_Demo.s

 

 3)彙編

 

 

 紅色箭頭處的relocatable表示可重定位,也即是可以和庫等依賴文件鏈接。

彙編就是將第二步輸出的彙編代碼翻譯成符合一定格式的機器代碼,在Linux系統上一般表現位ELF目標文件(OBJ文件)

結果:見文件0617_Demo.o

 

 這個就是沒有連接的目標代碼,也是01序列,需要使用二進制查看器如Hex Editor Neo等查看。

 

4)鏈接

 

 鏈接就是將彙編生成的OBJ文件、系統庫的OBJ文件、庫文件鏈接起來,最終生成可以在特定平台運行的可執行程序。如圖中紅色箭頭所示。

結果:見文件0617_Demo

 

 總結:在編譯過程中。除非使用了“-c”,“-S,“-E”選項(或者編譯錯誤阻止了完整的過程),否則統一完整鏈接步驟。

 

三、鏈接原理

gcc -c -o 0617_Demo.o 0617_Demo.c 不作最後一步鏈接,得到0617_Demo.o二進制OBJ文件

gcc -v -o 0617_Demo 0617_Demo.o 先看一下鏈接過程是怎樣的:

 

  

l  crt1.ocrti.ocrtbegin.ocrtend.ocrtn.ogcc加入的系統標準啟動文件,對於一般應用程序,這些啟動是必需的。

l  -lc:鏈接libc庫文件,其中libc庫文件中就實現了printf等函數。

 

 

 

 

 

GCC編譯和鏈接過程

 

GCCGNU Compiler CollectionGNU編譯器套件),是由 GNU 開發的編程語言編譯器。它是以GPL許可證所發行的自由軟件,也是 GNU計劃的關鍵部分。GCC原本作為GNU操作系統的官方編譯器,現已被大多數類Unix操作系統(如LinuxBSDMac OS X等)採納為標準的編譯器,GCC同樣適用於微軟的Windows

 

一、源代碼

 

二、編譯過程

1)預處理:

預處理就是將要包含(include)的文件插入原文件中、將宏定義展開、根據條件編譯命令選擇要使用的代碼,最後將這些代碼輸出到一個“.i”文件中等待進一步處理。

結果:見文件0617_Demo.i

 

2)編譯

編譯就是把C/C++代碼(比如上面的“.i”文件)“翻譯”成彙編代碼。

結果:見文件0617_Demo.s

 

3)彙編

紅色箭頭處的relocatable表示可重定位,也即是可以和庫等依賴文件鏈接。

彙編就是將第二步輸出的彙編代碼翻譯成符合一定格式的機器代碼,在Linux系統上一般表現位ELF目標文件(OBJ文件)

結果:見文件0617_Demo.o

這個就是沒有連接的目標代碼,也是01序列,需要使用二進制查看器如Hex Editor Neo等查看。

 

4)鏈接

鏈接就是將彙編生成的OBJ文件、系統庫的OBJ文件、庫文件鏈接起來,最終生成可以在特定平台運行的可執行程序。如圖中紅色箭頭所示。

結果:見文件0617_Demo

 

總結:在編譯過程中。除非使用了“-c”,“-S,“-E”選項(或者編譯錯誤阻止了完整的過程),否則統一完整鏈接步驟。

 

三、鏈接原理

gcc -c -o 0617_Demo.o 0617_Demo.c 不作最後一步鏈接,得到0617_Demo.o二進制OBJ文件

gcc -v -o 0617_Demo 0617_Demo.o 先看一下鏈接過程是怎樣的:

 

l  crt1.ocrti.ocrtbegin.ocrtend.ocrtn.ogcc加入的系統標準啟動文件,對於一般應用程序,這些啟動是必需的。

l  -lc:鏈接libc庫文件,其中libc庫文件中就實現了printf等函數。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

基於Docker Compose的.NET Core微服務持續發布

是不是現在每個團隊都需要上K8s才夠潮流,不用K8s是不是就落伍了。今天,我就通過這篇文章來回答一下。

一、先給出我的看法和建議

我想說的是,對於很多的微小團隊來說,可能都不是一定要上K8s,畢竟上K8s也是需要成本和人力的。對像我司一樣的傳統企業做数字化轉型的信息團隊來講,人數不多,沒有專門的Ops人員,領導又想要儘快迭代支持公司業務發展,而且關鍵還要節省成本(內心想法是:WTF)。

在此之下,信息團隊需要綜合引入先進技術帶來的價值以及需要承擔的成本和風險。任何架構的產生,都會解決一定的問題,但是同樣也會引入新的複雜度,正如微服務架構風格,看着香實際吃着才知道需要承受很多的“苦”(比如數據一致性又比如服務的治理等等)。

因此,結合考慮下來,我的建議是開發測試環境使用Docker Compose進行容器編排即可,而UAT或生產環境則建議使用雲廠商的K8s服務(比如阿里雲ACK服務)而不選擇自建K8s集群

那麼,今天就跟大家介紹一下如何使用Docker Compose這個輕量級的編排工具實現.NET Core微服務的持續發布。

二、Docker Compose

Docker主要用來運行單容器應用,而Docker Compose則是一個用來定義和應用多容器應用的工具,如下圖所示:

使用Docker Compose,我們可以將多容器的定義和部署方式定義在一個yml文件中,這種方式特別是微服務這種架構風格,可以將多個微服務的定義及部署都規範在一個yml文件中,然後一鍵部署、啟動或銷毀整個微服務應用。所有的一切操作,只需要下面的一句話:

$docker-compose up

Compose 的安裝請參考:https://docs.docker.com/compose/install/#install-compose,這裏就不再贅述,它不是本文重點。

安裝后驗證:

$docker-compose --version
docker-compose version 1.25.1, build a82fef07

三、一個簡單的發布流程示例

本文演示示例的流程大概會如下圖所示:

閱讀過我之前的一篇文章《基於Jenkins Pipeline的ASP.NET Core持續集成實踐》的童鞋應該對這個流程比較熟悉了。這裏,我仍然延續這個流程,作為一個平滑過渡。首先,我們在Jenkins上觸發容器的發布流水線任務,此任務會從Git服務器上拉取指定分支(一般都是測試分支)的最新代碼。

其次,在CI服務器上使用.NET Core SDK執行Build編譯和發布Release文件,基於發布后的Release文件進行鏡像的打包(確保你的項目裏面都有Dockerfile且設置為“始終複製”)。然後,基於打包后的鏡像,將其推送到企業的私有Registry服務器上(即本地鏡像倉庫,可以基於Harbor搭建一個,也可以直接用Docker Registry搭建一個,不建議使用docker hub的公有庫,如何搭建私有鏡像倉庫可以參考我的這一篇文章:《Docker常用流行鏡像倉庫的搭建》)。

最後,在測試服務器或要運行容器的服務器上執行docker compose up完成容器的版本更新。當然,也可以直接在docker-compose.yml文件內設置編譯路徑完成編譯和發布的操作(Dockerfile裏面定義進行Build和Publish)。這裏目的在於讓實例更簡單,且能讓初學者更容易理解,於是我就分開了。

四、.NET Core微服務發布示例

微服務示例準備

假設我們有一堆使用ASP.NET Core開發的微服務,這些微服務主要是為了實現諸如API網關、Identity鑒權、Notification通知、Job中心等基礎設施服務,因此我們將他們整合在一起進行持續集成和部署。

這裏為了讓示例盡可能簡單,每個微服務的Dockerfile只有以下幾句(這裏以一個通知API服務為例):

FROM reg.xdp.xi-life.cn/xdp-service-runtime:2.2
WORKDIR /app
COPY . /app
EXPOSE 80
ENTRYPOINT ["dotnet", "XDP.Core.Notification.API.dll"]

其中這裏的容器鏡像來自於私有鏡像倉庫,是一個封裝過的用於ASP.NET Core Runtime的容器鏡像。當然,上面說過,也可以在Dockerfile裏面進行服務的編譯和發布。

流水線任務腳本

同樣,為了在Jenkins上快速進行微服務的鏡像構建和推送以及部署,我們也需要編寫一個流水線構建任務。

下面是這個示例流水線任務的腳本:

pipeline{
    agent any
    environment {
        API_CODE_BRANCH="*/master"
        SSH_SERVER_NAME_REGISTRY="XDP-REGISTRY-Server"
        SSH_SERVER_NAME_DEV="XDP-DEV-Server"
        SSH_SERVER_NAME_AT="XDP-AT-Server"
        SSH_SERVER_NAME_SIT="XDP-SIT-Server"
    }
    stages {
        stage('XDP Core APIs Checkout & Build') {
            steps{
             checkout([$class: 'GitSCM', branches: [[name: env.API_CODE_BRANCH]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '35b9890b-2338-45e2-8a1a-78e9bbe1d3e2', url: 'http://192.168.18.150:3000/XDP.Core/XDP.Core.git']]])
             echo 'Core APIs Dev Branch Checkout Done' 
             bat  '''
               dotnet build XDP.Core-InfraServices.sln
               dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Components\\XDP.Core.ApiGateway\\XDP.Core.ApiGateway.csproj" -o "%WORKSPACE%\\XDP.Core.ApiGateway.API\\publish" --framework netcoreapp2.2
               dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Components\\XDP.Core.ApiGateway.Internal\\XDP.Core.ApiGateway.Internal.csproj" -o "%WORKSPACE%\\XDP.Core.ApiGateway.Internal.API\\publish" --framework netcoreapp2.2
               dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Authorization.API\\XDP.Core.Authorization.API.csproj" -o "%WORKSPACE%\\XDP.Core.Authorization.API\\publish" --framework netcoreapp2.2
               dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Authorization.Job\\XDP.Core.Authorization.Job.csproj" -o "%WORKSPACE%\\XDP.Core.Authorization.Job\\publish" --framework netcoreapp2.2
               dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Identity.API\\XDP.Core.Identity.API.csproj" -o "%WORKSPACE%\\XDP.Core.Identity.API\\publish" --framework netcoreapp2.2
               dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.Notification.API\\XDP.Core.Notification.API.csproj" -o "%WORKSPACE%\\XDP.Core.Notification.API\\publish" --framework netcoreapp2.2
               dotnet publish "%WORKSPACE%\\src\\services\\XDP.Core\\Services\\XDP.Core.JobCenter\\XDP.Core.JobCenter.csproj" -o "%WORKSPACE%\\XDP.Core.JobCenter.API\\publish" --framework netcoreapp2.2
               '''
             echo 'Core APIs Build & Publish Done'
            }
        }
        stage('XDP API Gateway Docker Image') {
            steps{
                bat '''
                    docker rmi reg.xdp.xi-life.cn/core-apigateway-portal:latest;
                    cd XDP.Core.ApiGateway.API/publish;
                    docker build -t reg.xdp.xi-life.cn/core-apigateway-portal:latest .;
                    docker push reg.xdp.xi-life.cn/core-apigateway-portal:latest;
                '''
                echo 'XDP Portal API Gateway Deploy Done'    

                bat '''
                    docker rmi reg.xdp.xi-life.cn/core-apigateway-internal:latest;
                    cd XDP.Core.ApiGateway.Internal.API/publish;
                    docker build -t reg.xdp.xi-life.cn/core-apigateway-internal:latest .;
                    docker push reg.xdp.xi-life.cn/core-apigateway-internal:latest;
                '''
                echo 'XDP Internal API Gateway Deploy Done'
            }
        }
        stage('Core Identity API Docker Image') {
            steps{
                ......
            }
        }
        stage('Core Authorization Job Docker Image') {
            steps{
                ......
            }
        }
        stage('Core Notification API Docker Image') {
            steps{
                ......  
            }
        }
        stage('Core JobCenter API Docker Image') {
            steps{
                ......     
            }
        }
        stage('Deploy to Local SIT Server') {
            steps{
                sshPublisher(publishers: [sshPublisherDesc(configName: env.SSH_SERVER_NAME_SIT, 
                    transfers: [sshTransfer(cleanRemote: false, excludes: '', 
                    execCommand: '''
                     cd compose/xdp;
                     IMAGE_TAG=latest docker-compose down;
                     docker rmi $(docker images -q)
                     IMAGE_TAG=latest docker-compose up -d;
                    ''', 
                    execTimeout: 120000, flatten: false, makeEmptyDirs: false, 
                    noDefaultExcludes: false, patternSeparator: '[, ]+', 
                    remoteDirectory: 'compose/xdp/', remoteDirectorySDF: false, 
                    removePrefix: '', 
                    sourceFiles: '', 
                    excludeFiles: '')], 
                    usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                echo 'Deploy to XDP SIT Server Done'     
            }
        }
    }
}

這個腳本我省去了一些重複的內容,只需要了解它的職責即可。

需要注意的地方有幾點:

(1)在進行dotnet build的時候,要明確SDK使用哪個版本,比如因為這裏的示例代碼是基於.NET Core 2.2開發的因此這裏使用的是2.2。如果你使用的是2.1,則標註2.1,如果是3.1,則標註3.1。

(2)在進行docker build的時候,要明確鏡像使用哪個Tag,這裏因為是本地開發測試環境,所以直接簡單暴力的直接使用了latest這個Tag。

(3)在進行sshPublish的時候,要提前將docker-compose.yml配置拷貝到對應的指定目錄下。當然,這一塊建議也將其納入git倉庫進行統一管理和統一發布到不同的環境的指定目錄下。

(4)如果你的Jenkins是裝在Windows Server上,要記住只有Windows Server 2016及以上版本才支持Docker,否則無法直接進行docker的命令行操作。如果低於2016,Windows 10專業版也可以,不過不建議。

擴展點:

是否可以一套docker-compose方案標準化部署到多個測試環境?是可以的,我們可以在Jenkins構建任務中配置Parameters,這樣就可以一次性部署到多個環境。例如,下面的示例中我設置了一個每次發布可以選擇到底要發布到哪個環境,這裡是單選,你也可以設置為多選。

效果如下:

docker-compose.yml

終於來到了compose的重點內容:docker-compose.yml

這裏我給出上面這個示例的yml示例內容(同樣,也省略了重複性的內容):

version: '2'

services:
  core_apigateway_portal:
    image: reg.xdp.xi-life.cn/core-apigateway-portal:${IMAGE_TAG}
    container_name: xdp_core_apigateway_portal
    restart: always
    privileged: true
    mem_limit: 1024m
    memswap_limit: 1024m
    env_file:
      - ../docker-variables.env
    ports:
      - 5000:80
    volumes:
      - /etc/localtime:/etc/localtime

  core_apigateway_internal:
    image: reg.xdp.xi-life.cn/core-apigateway-internal:${IMAGE_TAG}
    container_name: xdp_core_apigateway_internal
    restart: always
    privileged: true
    mem_limit: 1024m
    memswap_limit: 1024m
    env_file:
      - ../docker-variables.env
    ports:
      - 5100:80
    volumes:
      - /etc/localtime:/etc/localtime

  core_identity_api:
    image: reg.xdp.xi-life.cn/core-identity-api:${IMAGE_TAG}
    container_name: xdp_core_identity_api
    restart: always
    privileged: true
    mem_limit: 512m
    memswap_limit: 512m
    env_file:
      - ../docker-variables.env
    ports:
      - 6010:80
    volumes:
      - /etc/localtime:/etc/localtime

  core_authorization_api:
    ......

  core_authorization_job:
    ......

  core_notification_api:
    ......

  core_jobcenter_api:
    ......

  bff_xams_api:
    ......

備註:這裏使用的是version:2的語法,因為3開始不支持內存限制mem_limit等屬性設置。當然,你可以使用3的語法,去掉mem_limit和memswap_limit屬性即可。

這裏的env環境變量配置是定義在另外一個單獨的env文件裏面的,建議每個環境建立一個單獨的env文件供docker-compose.yml文件使用,比如下面是一個AT(自動化測試)環境的env文件內容示例:

# define xdp containers env
ASPNETCORE_ENVIRONMENT=at
ALIYUN_ACCESS_KEY=sxxdfdskjfkdsjkds
ALIYUN_ACCESS_SECRET=xdfsfjiwerowuoi
JWT_TOKEN=sdfsjkfjsdkfjlerwewe
IDENTITY_DB_CONNSTR=Server=192.168.16.150;Port=3306;Database=identity_at;Uid=xdpat;Pwd=xdpdba;Charset=utf8mb4
APIGATEWAY_DB_CONNSTR=Server=192.168.16.150;Port=3306;Database=services_at;Uid=xdpat;Pwd=xdpdba;Charset=utf8mb4
......
API_VERSION=AT-v1.0.0

這裏,最主要的環境變量就是ASPNETCORE_ENVIRONMENT,你需要指定這些要編排的微服務容器使用哪個環境的appSettings。同樣,這裏也引申出另一個問題,那就是配置的集中管理,可能你會說出類似Apollo,Spring Cloud Config,K8s Configmap之類的解決方案。這裏不是本文的重點,也就跳過。

快速實操體驗

現在我們來通過在Jenkins中觸發構建任務,可以看到如下圖所示的流水線任務狀態示意:

這樣,一個簡單的快速發布流水線就完成了,在單機多容器編排部署方面,Docker Compose是個不錯的選擇。

五、一些擴展

Consul服務發現容器編排

相比很多童鞋也都在使用Consul作為服務發現組件,我們也可以將Consul納入到Compose中來統一編排。例如,我們可以這樣來將其配置到docker-compose.yml中:

services:
  consul_agent_server:
    image: reg.xdp.xi-life.cn/xdp-consul-runtime:${IMAGE_TAG}
    container_name: xdp_consul_agent_server
    restart: always
    privileged: true
    mem_limit: 1024m
    memswap_limit: 1024m
    env_file:
      - ../docker-variables.env
    ports:
      - 8500:8500
    command: 
      agent -server -bootstrap-expect=1 -ui -node=xdp_local_server -client='0.0.0.0' -data-dir /consul/data -config-dir /consul/config -datacenter=xdp_local_dc
    volumes:
      - /etc/localtime:/etc/localtime
      - /docker/consul/data:/consul/data
      - /docker/consul/conf:/consul/config

這裏只使用到了一個Consul Server Agent,你可以配置一個3個Server節點的Consul Server集群,請自行查閱相關資料。此外,基於Compose我們也可以為API網關設置links從而實現服務發現的效果,當然前提是你的服務數量不多的前提下。這種方式是通過網絡層面幫你做了一層解析,從而實現多個容器之間的互連。這裏也推薦一下俺們成都地區的小馬甲老哥的一篇《docker-compose真香》的文章,他講解了docker的網橋模式。

基於Compose的編譯發布一體化

我們可以看到在很多開源項目中都是將編譯發布一體化的,因此我們可以看到在這些項目的Dockerfile中是這樣寫的:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /app

COPY ./*.sln ./NuGet.Config ./
COPY ./build/*.props ./build/

# Copy the main source project files
COPY src/*/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done

RUN dotnet restore

# Copy everything else and build app
COPY . .
RUN dotnet build -c Release

# api-publish

FROM build AS api-publish
WORKDIR /app/src/Exceptionless.Web

RUN dotnet publish -c Release -o out

# api

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS api
WORKDIR /app
COPY --from=api-publish /app/src/Exceptionless.Web/out ./
ENTRYPOINT [ "dotnet", "Exceptionless.Web.dll" ]

......

在Dockerfile中我們看到的是拉取.NET Core SDK來進行Restore、Build和Publish,進一步地提高了標準化的遷移性,也盡可能發揮Docker的集裝箱作用。
這時你可以在docker-compose.yml中定義Dockerfile告訴compose先幫我進行Build鏡像(這裏的build配置下就需要指定Dockerfile的位置):

services:
  api:
    build:
      context: .
    image: exceptionless/api:latest
    restart: always
    ......

六、小結

Docker是容器技術的核心、基礎,Docker Compose是一個基於Docker的單主機容器編排工具,功能並不像Docker Swarm和Kubernetes是基於Docker的跨主機的容器管理平台那麼豐富。

我想你看到這裏也應該有了自己的答案,結合我在最開頭給的建議,如果你處在一個小團隊中,綜合人員水平、技能儲備、運維成本 及 真實業務量要求,可以在開發測試環境(一般都是單主機環境的話)中使用Docker Compose進行初步編排。而在生產環境,即使是小團隊也建議上雲主機,利用雲的彈性為未來的業務發展做基礎,然後可以考慮使用雲上的K8s服務來進行生產級的容器編排。

 

 

作者:周旭龍

出處:https://edisonchou.cnblogs.com

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案