標籤: 網頁設計

  • 不敵客訴壓力 澳洲超市延後對塑膠袋收費

    摘錄自2018年8月3日蘋果日報澳洲報導

    澳洲一間大型連鎖超市原本要實施塑膠袋需收費政策,希望可推動「減塑」為環保出一分力,不料顧客對此大感不滿,政策推出後已經第2次延長收費期限。

    連鎖超市科爾斯(Coles)自上月1日起停用一次性塑膠袋,改向顧客提供可重複使用、質料更耐用的塑膠袋作過渡,直至本周三(1日)為止。超市原定顧客此後若索取塑膠袋,每個須收取15澳分(約3.4元台幣)。

    然而科爾斯周三發聲明指,自禁用一次性塑膠袋後,有顧客反映需要更多過渡時間,以適應使用可重複使用塑膠袋,集團因此決定在昆士蘭、新南威爾斯、維多利亞及西澳洲等地繼續提供免費塑膠袋至8月29日,而南澳洲、塔斯馬尼亞等地區則仍繼續收費。

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 遊客遽增破壞環境 紐西蘭國會報告示警

    摘錄自2019年12月18日中央社威靈頓報導

    紐西蘭國會今天(18日)提出報告警示,遽增的遊客數量正危害紐西蘭環境,也讓當地聞名遐邇、極具魅力的寧靜印象受到逐步破壞。

    法新社報導,紐西蘭一向標榜「100%純淨」和「乾淨又環保」的形象,但近年來遊客數量遽增,許多人朝聖「魔戒」(The Lord of the Rings)電影拍攝地點大玩自拍,還有登山客、健行者及野生動物愛好人士。

    紐西蘭國會環境事務專員厄普頓(Simon Upton)提出報告之際,紐國旅遊業正受到嚴格審查,因為白島(White Island)火山在9日爆發,造成16名外籍旅客和2名導遊喪命。

    擁有490萬人口的紐西蘭每年吸引近400萬名外國遊客,厄普頓說,這項數據到了2050年可能增加2倍,厄普頓表示,基礎設施變得吃緊、環境承受壓力,紐西蘭原有的諸多品質正在消失,他說:「大批群眾正逐漸破壞許多外國遊客赴紐西蘭旅遊所尋覓的獨處感、寧靜和親近大自然的感覺,我們必須要問:我們是否正在殺雞取卵?」

    厄普頓提到,紐西蘭人也是問題的一部分,在紐西蘭國定假期,著名景點湧現的國內遊客人潮比外國遊客還要多。

    他還說,紐西蘭人已經習慣東加里羅步道(Tongariro Crossing)等著名景點「被遊客團團包圍」,而且問題只會持續惡化。

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化

  • 阿裏面試官最喜歡問的21個HashMap面試題

    阿裏面試官最喜歡問的21個HashMap面試題

    1.HashMap 的數據結構?

    A:哈希表結構(鏈表散列:數組+鏈表)實現,結合數組和鏈表的優點。當鏈表長度超過 8 時,鏈錶轉換為紅黑樹。

    transient Node<K,V>\[\] table;
    

    2.HashMap 的工作原理?

    HashMap 底層是 hash 數組和單向鏈表實現,數組中的每個元素都是鏈表,由 Node 內部類(實現 Map.Entry接口)實現,HashMap 通過 put & get 方法存儲和獲取。

    存儲對象時,將 K/V 鍵值傳給 put() 方法:

    ①、調用 hash(K) 方法計算 K 的 hash 值,然後結合數組長度,計算得數組下標;

    ②、調整數組大小(當容器中的元素個數大於 capacity * loadfactor 時,容器會進行擴容resize 為 2n);

    ③、i.如果 K 的 hash 值在 HashMap 中不存在,則執行插入,若存在,則發生碰撞;

    ii.如果 K 的 hash 值在 HashMap 中存在,且它們兩者 equals 返回 true,則更新鍵值對;

    iii. 如果 K 的 hash 值在 HashMap 中存在,且它們兩者 equals 返回 false,則插入鏈表的尾部(尾插法)或者紅黑樹中(樹的添加方式)。(JDK 1.7 之前使用頭插法、JDK 1.8 使用尾插法)(注意:當碰撞導致鏈表大於 TREEIFY_THRESHOLD = 8 時,就把鏈錶轉換成紅黑樹)

    獲取對象時,將 K 傳給 get() 方法:①、調用 hash(K) 方法(計算 K 的 hash 值)從而獲取該鍵值所在鏈表的數組下標;②、順序遍歷鏈表,equals()方法查找相同 Node 鏈表中 K 值對應的 V 值。

    hashCode 是定位的,存儲位置;equals是定性的,比較兩者是否相等。

    3.當兩個對象的 hashCode 相同會發生什麼?

    因為 hashCode 相同,不一定就是相等的(equals方法比較),所以兩個對象所在數組的下標相同,”碰撞”就此發生。又因為 HashMap 使用鏈表存儲對象,這個 Node 會存儲到鏈表中。

    4.你知道 hash 的實現嗎?為什麼要這樣實現?

    JDK 1.8 中,是通過 hashCode() 的高 16 位異或低 16 位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度,功效和質量來考慮的,減少系統的開銷,也不會造成因為高位沒有參与下標的計算,從而引起的碰撞。

    5.為什麼要用異或運算符?

    保證了對象的 hashCode 的 32 位值只要有一位發生改變,整個 hash() 返回值就會改變。盡可能的減少碰撞。

    6.HashMap 的 table 的容量如何確定?loadFactor 是什麼?該容量如何變化?這種變化會帶來什麼問題?

    ①、table 數組大小是由 capacity 這個參數確定的,默認是16,也可以構造時傳入,最大限制是1<<30;

    ②、loadFactor 是裝載因子,主要目的是用來確認table 數組是否需要動態擴展,默認值是0.75,比如table 數組大小為 16,裝載因子為 0.75 時,threshold 就是12,當 table 的實際大小超過 12 時,table就需要動態擴容;

    ③、擴容時,調用 resize() 方法,將 table 長度變為原來的兩倍(注意是 table 長度,而不是 threshold)

    ④、如果數據很大的情況下,擴展時將會帶來性能的損失,在性能要求很高的地方,這種損失很可能很致命。

    7.HashMap中put方法的過程?

    答:“調用哈希函數獲取Key對應的hash值,再計算其數組下標;
    如果沒有出現哈希衝突,則直接放入數組;如果出現哈希衝突,則以鏈表的方式放在鏈表後面;
    如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈錶轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表;
    如果結點的key已經存在,則替換其value即可;
    如果集合中的鍵值對大於12,調用resize方法進行數組擴容。”

    8.數組擴容的過程?

    創建一個新的數組,其容量為舊數組的兩倍,並重新計算舊數組中結點的存儲位置。結點在新數組中的位置只有兩種,原下標位置或原下標+舊數組的大小。

    9.拉鏈法導致的鏈表過深問題為什麼不用二叉查找樹代替,而選擇紅黑樹?為什麼不一直使用紅黑樹?

    之所以選擇紅黑樹是為了解決二叉查找樹的缺陷,二叉查找樹在特殊情況下會變成一條線性結構(這就跟原來使用鏈表結構一樣了,造成很深的問題),遍歷查找會非常慢。

    而紅黑樹在插入新數據后可能需要通過左旋,右旋、變色這些操作來保持平衡,引入紅黑樹就是為了查找數據快,解決鏈表查詢深度的問題,我們知道紅黑樹屬於平衡二叉樹,但是為了保持“平衡”是需要付出代價的,但是該代價所損耗的資源要比遍歷線性鏈表要少,所以當長度大於8的時候,會使用紅黑樹,如果鏈表長度很短的話,根本不需要引入紅黑樹,引入反而會慢。

    10.說說你對紅黑樹的見解?

    • 每個節點非紅即黑
    • 根節點總是黑色的
    • 如果節點是紅色的,則它的子節點必須是黑色的(反之不一定)
    • 每個恭弘=叶 恭弘子節點都是黑色的空節點(NIL節點)
    • 從根節點到恭弘=叶 恭弘節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)

    11.jdk8中對HashMap做了哪些改變?

    在java 1.8中,如果鏈表的長度超過了8,那麼鏈表將轉換為紅黑樹。(桶的數量必須大於64,小於64的時候只會擴容)

    發生hash碰撞時,java 1.7 會在鏈表的頭部插入,而java 1.8會在鏈表的尾部插入

    在java 1.8中,Entry被Node替代(換了一個馬甲)。

    12.HashMap,LinkedHashMap,TreeMap 有什麼區別?

    HashMap 參考其他問題;

    LinkedHashMap 保存了記錄的插入順序,在用 Iterator 遍歷時,先取到的記錄肯定是先插入的;遍歷比 HashMap 慢;

    TreeMap 實現 SortMap 接口,能夠把它保存的記錄根據鍵排序(默認按鍵值升序排序,也可以指定排序的比較器)

    13.HashMap & TreeMap & LinkedHashMap 使用場景?

    一般情況下,使用最多的是 HashMap。

    HashMap:在 Map 中插入、刪除和定位元素時;

    TreeMap:在需要按自然順序或自定義順序遍歷鍵的情況下;

    LinkedHashMap:在需要輸出的順序和輸入的順序相同的情況下。

    14.HashMap 和 HashTable 有什麼區別?

    ①、HashMap 是線程不安全的,HashTable 是線程安全的;

    ②、由於線程安全,所以 HashTable 的效率比不上 HashMap;

    ③、HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null,而 HashTable不允許;

    ④、HashMap 默認初始化數組的大小為16,HashTable 為 11,前者擴容時,擴大兩倍,後者擴大兩倍+1;

    ⑤、HashMap 需要重新計算 hash 值,而 HashTable 直接使用對象的 hashCode

    15.Java 中的另一個線程安全的與 HashMap 極其類似的類是什麼?同樣是線程安全,它與 HashTable 在線程同步上有什麼不同?

    ConcurrentHashMap 類(是 Java併發包 java.util.concurrent 中提供的一個線程安全且高效的 HashMap 實現)。

    HashTable 是使用 synchronize 關鍵字加鎖的原理(就是對對象加鎖);

    而針對 ConcurrentHashMap,在 JDK 1.7 中採用 分段鎖的方式;JDK 1.8 中直接採用了CAS(無鎖算法)+ synchronized。

    16.HashMap & ConcurrentHashMap 的區別?

    除了加鎖,原理上無太大區別。另外,HashMap 的鍵值對允許有null,但是ConCurrentHashMap 都不允許。

    17.為什麼 ConcurrentHashMap 比 HashTable 效率要高?

    HashTable 使用一把鎖(鎖住整個鏈表結構)處理併發問題,多個線程競爭一把鎖,容易阻塞;

    ConcurrentHashMap

    • JDK 1.7 中使用分段鎖(ReentrantLock + Segment + HashEntry),相當於把一個 HashMap 分成多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基於 Segment,包含多個 HashEntry。
    • JDK 1.8 中使用 CAS + synchronized + Node + 紅黑樹。鎖粒度:Node(首結點)(實現 Map.Entry)。鎖粒度降低了。

    18.針對 ConcurrentHashMap 鎖機制具體分析(JDK 1.7 VS JDK 1.8)?

    JDK 1.7 中,採用分段鎖的機制,實現併發的更新操作,底層採用數組+鏈表的存儲結構,包括兩個核心靜態內部類 Segment 和 HashEntry。

    ①、Segment 繼承 ReentrantLock(重入鎖) 用來充當鎖的角色,每個 Segment 對象守護每個散列映射表的若干個桶;

    ②、HashEntry 用來封裝映射表的鍵-值對;

    ③、每個桶是由若干個 HashEntry 對象鏈接起來的鏈表

    JDK 1.8 中,採用Node + CAS + Synchronized來保證併發安全。取消類 Segment,直接用 table 數組存儲鍵值對;當 HashEntry 對象組成的鏈表長度超過 TREEIFY_THRESHOLD 時,鏈錶轉換為紅黑樹,提升性能。底層變更為數組 + 鏈表 + 紅黑樹。

    19.ConcurrentHashMap 在 JDK 1.8 中,為什麼要使用內置鎖 synchronized 來代替重入鎖 ReentrantLock?

    ①、粒度降低了;

    ②、JVM 開發團隊沒有放棄 synchronized,而且基於 JVM 的 synchronized 優化空間更大,更加自然。

    ③、在大量的數據操作下,對於 JVM 的內存壓力,基於 API 的 ReentrantLock 會開銷更多的內存。

    20.ConcurrentHashMap 簡單介紹?

    ①、重要的常量:

    private transient volatile int sizeCtl;

    當為負數時,-1 表示正在初始化,-N 表示 N – 1 個線程正在進行擴容;

    當為 0 時,表示 table 還沒有初始化;

    當為其他正數時,表示初始化或者下一次進行擴容的大小。

    ②、數據結構:

    Node 是存儲結構的基本單元,繼承 HashMap 中的 Entry,用於存儲數據;

    TreeNode 繼承 Node,但是數據結構換成了二叉樹結構,是紅黑樹的存儲結構,用於紅黑樹中存儲數據;

    TreeBin 是封裝 TreeNode 的容器,提供轉換紅黑樹的一些條件和鎖的控制。

    ③、存儲對象時(put() 方法):

    如果沒有初始化,就調用 initTable() 方法來進行初始化;

    如果沒有 hash 衝突就直接 CAS 無鎖插入;

    如果需要擴容,就先進行擴容;

    如果存在 hash 衝突,就加鎖來保證線程安全,兩種情況:一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入;

    如果該鏈表的數量大於閥值 8,就要先轉換成紅黑樹的結構,break 再一次進入循環

    如果添加成功就調用 addCount() 方法統計 size,並且檢查是否需要擴容。

    ④、擴容方法 transfer():默認容量為 16,擴容時,容量變為原來的兩倍。

    helpTransfer():調用多個工作線程一起幫助進行擴容,這樣的效率就會更高。

    ⑤、獲取對象時(get()方法):

    計算 hash 值,定位到該 table 索引位置,如果是首結點符合就返回;

    如果遇到擴容時,會調用標記正在擴容結點 ForwardingNode.find()方法,查找該結點,匹配就返回;

    以上都不符合的話,就往下遍歷結點,匹配就返回,否則最後就返回 null。

    21.ConcurrentHashMap 的併發度是什麼?

    程序運行時能夠同時更新 ConccurentHashMap 且不產生鎖競爭的最大線程數。默認為 16,且可以在構造函數中設置。

    當用戶設置併發度時,ConcurrentHashMap 會使用大於等於該值的最小2冪指數作為實際併發度(假如用戶設置併發度為17,實際併發度則為32)

    更多精彩面試題

    如果有想看的小夥伴就給我留言吧。這就是本文的全部內容了。如果覺得寫的不錯,請記得收藏加轉發。還想跟我看更多數據結構和算法題的小夥伴們,記得關注我公眾號:程序零世界,Java 就這麼回事。

    線程,多線程,線程池,線程上下文,鎖一鍵啟動線程

    紅黑樹其實並不難,只是你還沒看過ta

    JVM其實並沒有那麼難,你也該啃下TA了

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

    【其他文章推薦】

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

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

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

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

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

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

  • Typescript的interface、class和abstract class

    interface,class,和abstract class這3個概念,既有聯繫,又有區別,本文嘗試着結合官方文檔來闡述這三者之間的關係。

    1. Declaration Merging

    Declaration Type Namespace Type Value
    Namespace X X
    Class X X
    Enum X X
    Interface X
    Type Alias X
    Function X
    Variable X

    首先我們來講一下上面這張表格,當我們第一列的關鍵字進行聲明時,我們在做什麼。

    namespace job {
       haircut(): void;
    }
    
    class Man{
    	name: string;
    }
    let imgss = new Man();
    
    enum Color {red, blue, yellow}
    
    interface dogfood {
    
      brand: string;
      price: number
    }
    type event = 'mouse' | 'keyboard';
    
    function foo(){}
    
    let a = 2;
    var b = {};
    const c = null;
    	
    

    namespace用來聲明一個命名空間,比較著名的命名空間有lodash,裏面有一堆工具函數,統統放在一個叫_的namespace裏面,同時你也可以let $ = _;所以namespace也聲明了一個值。

    class聲明了一個值,也聲明了一種類型,你可以把Man賦值給一個變量,所以class是一種值,也可以說imgss是一個Man(類型),此時Man承擔了一種類型的角色。

    enum聲明了一個值,也聲明了一種類型。我們說red是一種Color,Color在這裏承擔類型的角色,也可以把Color賦值給一個變量

    interface聲明了一種類型,但是你不能把dogfood賦值給某個變量,否則你會得到一個報錯“dogfood’ only refers to a type, but is being used as a value here`

    其他function,let,var,const都在聲明一個值,你 不能說xxx是一個a,或者xxx是一個foo,不能把值當成類型使用。

    2. interface和class

    我們知道,不算symbol,js中有6種基本類型,number,string,boolean,null, undefined, object。但是只依靠這幾種類型,來描述某個函數需要傳什麼樣的參數,是遠遠不夠的,這也是interface的使命–描述一個值(value)的形狀(type)。

    現在我們來看class,class首先也具有interface的能力,描述一個形狀,或者說代表一種類型。此外class還提供了實現,也就是說可以被實例化;

    所以class可以implements interface:

    interface ManLike {
      speak(): void;
      leg: number;
      hand: number;
    }
    class Human implements ManLike {
      leg: number = 2;
      hand: number = 2;
      speak() {
        console.log('i can speak');
      }
    }
    

    而interface可以extends class,此時的class承擔類型的角色

    interface Chinese extends Human {
      country: string;
    }
    

    那麼interface能不能extends enum或者type alias呢,這兩個兄弟也聲明了type啊,答案是不行的,官方報錯的信息:

    An interface can only extend an object type or intersection of object types with statically known members.
    

    3. class和abstract class

    class和abstract class的區別主要是abstract class不能被實例化:

    abstract Human {
    	name: string;
        abstract lang(): void;
    	toString() {
        	return `<human:${this.name}>`
        }
    }
    new Human // Cannot create an instance of an abstract class.
    

    4. interface和abstract class

    兩者都不能被實例化,但是abstract class 也可以被賦值給變量。
    interface 裏面不能有方法的實現,abstract class 可以提供部分的方法實現,這些方法可以被子類調用。

    參考: https://www.typescriptlang.org/docs/handbook/declaration-merging.html

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 程序員的命名素養

    程序員的命名素養

    引言

    今天來聊聊命名相關內容。

    在日常工作中,項目、類、方法、表等等等等,都需要我們起名來標識區分。好的名字讓人賞心悅目,不好的名字讓人看的想吐。

    最近工作有幸寫了node、前端、php、sql、scala,也見識了公司各位前輩們的命名功底。其中不乏abc命名、拼音命名、蹩腳英文命名,更有不少從別的地方粘過來連名都不改的操作。

    命名沒有對錯,只是規範一點,可以提高可讀性、可維護性。

    命名原則

    拼寫正確

    拼寫正確是可讀的基礎。

    play shiftplay shit自己體會一下

    清新明了,見名知意

    根據要表達的內容命名,一針見血。

    getNameById 根據id獲取名稱
    ClassLoader 類加載器
    MYSQL_USERNAME mysql用戶名 
    

    如上幾個例子,我們一眼就知道要表達什麼,可讀性高。

    使用英文字母命名

    在編程中,英文還是較為主流的,最好使用單詞來命名,再不濟也是用拼音來命名。

    不論是拼音或單詞,清晰表意是首要。

    保持一致

    在一個項目中,應該使用統一的規範來命名。

    無規矩不成方圓。

    合理使用動詞名詞

    類名、變量名通常應使用名詞。如ClassLoaderuserId

    對於方法名、函數名,應包含動詞。如handleClickgenerateUniqueId

    命名方法

    常見的命名方法有駝峰命名法、匈牙利命名法、帕斯卡命名法、中/下劃線命名法

    駝峰命名法Camel-Case

    駝峰命名法,又叫小駝峰命名法,如名稱所表達的意思,指混合使用大小寫字母老表示名字。

    userIdgetCompanyNameById

    應用很廣泛。

    匈牙利命名法

    基本原則是:變量名=屬性+類型+對象描述。通過名稱可以直觀的了解他的所屬、類型等信息。

    是早期的命名方式,早期IDE沒有很智能的時候,這種命名是很有必要的。

    iNum,表示int類型的num

    現在依舊很少有人用了。

    帕斯卡命名法

    又叫大駝峰命名法,就是把駝峰命名的首字母大寫了。

    ClassLoader

    中/下劃線命名法

    單詞全部小寫,單詞和單詞間用中劃線或下劃線分割。

    user_idpython-flask-demo

    下劃線命名在數據庫中較為常見。

    常量命名法

    這個不是官方的方法,但是常量一般是由固定規範的。

    格式:所有單詞的所有字母都是大寫,單詞之間用下戶線連接。

    APOLLO_NAMESPAC

    總結

    好的命名習慣是每個程序員必備的基本素養。

    寫代碼時,好的命名會讓思路更加清洗,代碼寫的更加絲滑。

    代碼就是程序員的形象,從命名的細節開始,讓自己更帥一些。

    個人公眾號:碼農峰,定時推送行業資訊,持續發布原創技術文章,歡迎大家關注。

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

    【其他文章推薦】

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

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

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

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

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

    網頁設計最專業,超強功能平台可客製化

  • Web前端兼容性指南

    Web前端兼容性指南

    一、Web前端兼容性問題

    一直以來,Web前端領域最大的問題就是兼容性問題,沒有之一。

     

    前端兼容性問題分三類:

    • 瀏覽器兼容性
    • 屏幕分辨率兼容性
    • 跨平台兼容性

     

    1、瀏覽器兼容性問題

    第一次瀏覽器大戰發生在上個世紀90年代,微軟發布了IE瀏覽器,和網景公司的Netscape Navigator大打出手,1998年網景不得不將公司賣給AOL。沒有了對手的IE不思進取,W3C標準支持發展緩慢,為以後的IE兼容性災難埋下了伏筆。到2004年,IE的市場份額達到95%,但在此之後IE的份額逐步遭其他瀏覽器蠶食,主要包括Firefox,Chrome,Safari和Opera。.

     

    2001年8月27日,微軟發布IE6,時隔五年直到2006年才發布了IE7。2009年3月19日,經歷了眾多測試版后,IE8最終發布,雖然IE8針對舊版IE在多方面做了很大改進,但在HTML5、CSS 3等標準支持方面仍落後於其他瀏覽器對手。這三個版本的IE是所有兼容性問題的最大根源,堪稱前端噩夢。

     

    IE6、7、8不支持HTML5、CSS3、SVG標準,可被判定為“極難兼容”

    IE9不支持Flex、Web Socket、WebGL,可被判定為“較難兼容”

    IE10部分支持Flex(-ms-flexbox)、Web Socket,可被判定為“較易兼容”

    IE11部分支持Flex、WebGL,可被判定為“較易兼容”

     

    IE6、7、8、9可視為“老式瀏覽器”

    IE10、11可視為“准現代瀏覽器”

    Chrome、Firefox、Safari、Opera 、Edge可視為“現代瀏覽器”

     

    瀏覽器與Windows版本份額

    Statcounter的各項數據以2020年6月為基準。

    http://gsa.statcounter.com/

     

     

     2、屏幕分辨率兼容性問題

    在不同的屏幕分辨率,瀏覽器頁面展示差異很大。特別是屏幕分辨率較小時,容易發生布局錯亂。為了解決這個問題,響應式UI框架應運而生。

     

    主流桌面屏幕分辨率寬度集中在1280~1920,高度集中在720~1080;

    主流平板屏幕分辨率寬度集中在962~1280,高度集中在601~800。

    主流移動屏幕分辨率寬度集中在360~414,高度集中在640~896。

     

    典型的桌面屏幕分辨率:1920×1080

    典型的便攜屏幕分辨率:1366×768

    典型的平板屏幕分辨率:768×1024

    典型的移動屏幕分辨率:360×640

     

    Bootstrap定義(參考系是邏輯分辨率):

    分辨率

    設備名

    典型屏幕

    >=1400px

    xxl 超超大屏設備

    桌面屏幕

    >=1200px

    xl 超大屏設備

    便攜屏幕

    >=992px

    lg 大屏設備

    豎屏桌面屏幕、橫屏平板屏幕

    >=768px

    md 中屏設備

    豎屏平板屏幕

    >=576px

    sm 小屏設備

    橫屏移動屏幕

    <576px

    xs 超小屏(自動)設備

    豎屏移動屏幕

    注:Bootstrap5新增xxl,Bootstrap3中的lg>=1200px,無576px檔。

     

    手機屏幕分辨率說明

    由於手機屏幕尺寸過小,使用原始分辨率會使得頁面显示過小,因此使用了邏輯分辨率,用倍數放大的方法來保證兼容性。比如iOS app的UI資源區分@1x、@2x和@3x,這就是指原始分辨率對邏輯分辨率的倍數,被稱為設備像素比DPR。所以大部分人的手機分辨率都是1080×1920,在分類中卻被歸為了360×640。這個分辨率和CSS中的PX是一致的。

     

    桌面屏幕分辨率說明

    移動設備一開始就考慮了DPR,而Windwos桌面的分辨率由於歷史原因卻沒有這一概念,於是Windwos引入了DPI,最初是設置DPI,後來是設置DPI比例。比如設置DPI比例=125%,你可以查詢Chrome的window.devicePixelRatio,這時輸出1.25,這說明DPI比例=DPR。但是大部分老程序並不支持DPI(Unaware),所以當你設置高DPI時,只能等比放大,字模糊到眼要瞎,最後落得空有大屏只能用超低分辨率。由於Chrome支持DPI,所以並不擔心Web有DPI問題。但需要注意的是與手機屏幕分辨率不同,桌面分辨率要除以DPI比例,才是邏輯分辨率。如1920×1080設置DPI比例=1.25,邏輯分辨率實際為1536×864。

      

      

    屏幕分辨率基礎概念說明

    縮寫

    全稱

    說明

    PX

    Device Pixels

    設備像素,指設備的物理像素

    PX

    CSS Pixels

    CSS像素,指CSS樣式代碼中使用的邏輯像素

    DOT

    Dot

    點,屏幕或打印紙上的點,等同物理像素

    PT

    Point

    磅(傳統長度單位)為1/72英寸=0.35mm

    PT

    iOS Point

    磅(iOS長度單位),為1/163英寸,等同於CSS邏輯像素

    DP

    Density independent Pixels

    設備無關像素(Android長度單位),為1/160英寸,等同於CSS邏輯像素

    SP

    Scale independent Pixels

    縮放無關像素(Android字體單位),等同於CSS邏輯像素,但文字尺寸可調(單獨縮放)

    DPR

    Device Pixel Ratio

    設備像素比,指CSS邏輯像素對於物理像素的倍數

    DPPX

    Dots Per Pixel

    等同於DPR

    PPI

    Pixel Per Inch

    屏幕上每英寸(2.54厘米)的像素點個數

    DPI

    Dots Per Inch

    屏幕或紙上每英寸(2.54厘米)的點個數,標準密度:傳統打印=72;Windows=96;Android=160;iOS=163。

    DPIR

    DPI Ratio

    DPI縮放比例,指DPI對於Windows標準DPI的倍數=DPI/96,等同於DPR

    注:各廠商概念有重名現象,請注意區分。

     

    各平台屏幕分辨率份額

      

    3、跨平台兼容性問題 

    隨着移動和平板市場的日益發展,Web在桌面、平板、移動平台上的兼容性問題日益突出。由於移動和平板是觸摸式操作,與桌面的鼠標操作方式有很大差異,因此在不同平台上要做相應修改。為了解決這個問題,誕生了跨平台框架,在不同平台上,外觀、布局、操作都有差異化修改。

     

    各平台份額

      

    二、前端里程碑框架

    在前端領域,隨着技術的不斷進步,逐步誕生了一些里程碑式的前端框架。這些前端框架,大致也是隨着兼容性問題的發生、發展而誕生、發展的。

     

    這些框架代表了前端應用當時先進、成熟、主流的開發方式與發展方向,兼容性問題也在這些框架的基礎之上不斷得到解決,大致也分為三個階段:

    一、DOM操作框架,代表框架:jQuery

    二、響應式框架,代表框架:Bootstrap

    三、前端MVC框架,代表框架:React、Angular、Vue

     

    1、JQuery

    2006年1月John Resig等人創建了jQuery;8月,jQuery的第一個穩定版本。jQuery是DOM操作時代前端框架最優秀,也幾乎是唯一代表;但是在以React為代表的新式前端框架崛起之後,迅速沒落。

     

    • JQuery 1.x兼容IE6+瀏覽器
    • JQuery 2.x兼容IE9+瀏覽器
    • JQuery 3.x兼容IE9+瀏覽器

     

    2、Bootstrap

    Bootstrap原名Twitter Blueprint,由Mark Otto和Jacob Thornton開發,最經典的響應式CSS框架,在2011年8月19日作為開源項目發布。其核心是16列布局柵格系統,使用媒體查詢設定閾值為超小屏幕,小屏幕,中等屏幕,大屏幕,超大屏幕創建不同的樣式。

     

    • Bootstrap 2兼容IE7+瀏覽器
    • Bootstrap 3兼容IE8+瀏覽器
    • Bootstrap 4兼容IE10+瀏覽器
    • Bootstrap 5不兼容IE瀏覽器

     

    3、React

    React 起源於 Facebook 的內部項目,在前端MVC框架大潮中誕生並走紅。2013年5月開源,憑藉Virtual Dom,JSX,Flux,Native等一大批創新特性,迅速吸引了大量開發人員,至今仍是最先進的前端JS框架。

     

    4、Angular

    AngularJS 誕生於2009年,由Misko Hevery 等人創建,後為Google所收購。由於Google不差錢,所以AngularJS經歷顛覆性升級為Angular。Angular最大的特點就是大而全。

     

    5、Vue

    2013年,在Google工作的尤雨溪,受到Angular的啟發,從中提取自己所喜歡的部分,開發出了一款輕量框架,最初命名為Seed,后更名為Vue。

     

    三、瀏覽器兼容框架

    在前端發展的初期,大多數開發最關注的問題就是瀏覽器兼容問題,迫切需要兼容所有瀏覽器的JS和CSS框架。這階段除了橫空出世的jQuery,還有一些其它方面的兼容框架。

     

    1、normalize.css

    讓不同的瀏覽器在渲染網頁元素的時候形式更統一。

     

    2、html5shiv.js

    IE6~IE8識別HTML5標籤,並且可以添加CSS樣式。

     

    3、respond.js

    使IE6~IE8瀏覽器支持媒體查詢。

     

    四、響應式框架

    有了jQuery等兼容框架的基礎,開發人員的關注點,逐漸轉移到越來越豐富的屏幕分辨率上,除開Bootstrap一家獨大,越來越多的響應式框架也在奮起直追。

     

    1、Semantic UI

    https://github.com/semantic-org/semantic-ui

    Semantic 是一個設計漂亮的響應式布局的語義化框架。

     

    2、Bulma

    https://github.com/jgthms/bulma

    基於 Flexbox 的現代 CSS 框架

     

    3、Tailwind

    https://github.com/tailwindcss/tailwindcss

    Tailwind是一個底層CSS 框架,快速 UI 開發的實用工具集,提供了高度可組合的應用程序類,可幫助開發者輕鬆構建複雜的用戶界面。另外Tailwind + Styled Component 簡直是絕配(摘自知乎https://www.zhihu.com/question/337939566)。

     

    4、Materialize

    https://github.com/Dogfalo/materialize

    A CSS Framework based on Material Design.

     

    5、Foundation

    https://github.com/foundation/foundation-sites

    The most advanced responsive front-end framework in the world.

     

    6、Pure.css

    https://github.com/pure-css/pure

    A set of small, responsive CSS modules

     

    7、YAMLCSS

    https://github.com/yamlcss/yaml

    YAML is a modular CSS framework for truly flexible, accessible and responsive websites.

     

    兼容IE6+瀏覽器(能兼容IE6的太稀少了)

     

    五、跨平台框架

    自2009年以來,由於Node.js生態的不斷髮展,前端開發的勢力大漲, AngularJS,BackboneJS,KnockoutJS等一批前端MVC框架開始出現。最終伴隨着React、Angular、Vue等框架的脫穎而出,用前端框架開發移動、桌面應用的野心開始暴漲,開始關注不同平台的差異化,越來越多的跨平台框架開始出現。

     

    1、Framework7

    https://github.com/framework7io/framework7

    Build iOS, Android & Desktop apps

     

     從上圖可以看出,桌面版本比移動版本更緊湊,控件風格跟所在平台近似。支持三種主題:ios、 md、 aurora對應不同平台。

     

    2、Ionic

    https://github.com/ionic-team/ionic

    build mobile and desktop apps

     

     從上圖可以看出,主要針對移動平台優化,但通過API支持多種平台。

     

    3、Onsen UI

    https://github.com/OnsenUI/OnsenUI

    develop HTML5 hybrid and mobile web apps

     

     從上圖可以看出,主要針對移動平台優化,但通過API支持多種平台。

     

    4、Quasar Framework

    https://github.com/quasarframework/quasar

    基於Vue構建響應式網站、PWA、SSR、移動和桌面應用

     

     Quasar將一些輔助CSS類附加到document.body:如desktop、mobile、touch、platform-[ios]、within-iframe等


    5、UNI-APP
     

    https://github.com/dcloudio/uni-app

    使用 Vue.js 開發所有前端應用的框架

     

     從上圖可以看出,三種平台比較一致,但移動版本還比桌面版本還緊湊是什麼意思?

     

    6、橫向對比

    框架

    桌面優化

    移動優化

    移動一致

    支持框架

    Framework7

    優秀

    優秀

    優秀

    最多

    Ionic

    一般

    優秀

    一般

    較多

    Onsen UI

    一般

    優秀

    一般

    較多

    Quasar

    良好

    優秀

    良好

    Vue

    UNI-APP

    一般

    優秀

    優秀

    Vue

     

    六、總結

    兼容性問題總是伴隨着平台的擴張而產生的,Web開發面臨的終極問題就是多平台兼容性問題,根據不同產品,不同階段做部分取捨,應用不同的框架而已。需要支持的平台,決定了你的選擇。

     

    新的框架或舊框架的新版本基本都不再支持IE,但國內還有5.65% 的IE用戶,而且3.29%的WinXP,46.79%的Win7都是潛在的IE用戶,所以可將其做為一個平台看待。

    • IE Web
    • Desktop Web
    • Mobile Web
    • Tablet Web
    • Desktop Hybrid
    • Mobile Hybrid
    • Tablet Hybrid

    注:React Native代表的Native技術不在本次討論之列

     

    1、瀏覽器兼容策略

    國內XP用戶還有3.29%,XP用戶既升級不了IE9,也無法安裝新版本Chrome和Firefox 。而IE用戶還有 5.65%,考慮到Windows用戶為87%,所以IE9+的份額應該要少於5.65%-3.29%*87%=2.79%。也就是說IE8以下的用戶要多於IE8以上的用戶。所以支持單獨支持IE9+ 瀏覽器沒有實際意義,要麼支持IE6,要麼不支持IE,。

     

    看看知名網站對IE8的兼容性,

    • 京東會提示“溫馨提示:您當前的瀏覽器版本過低,存在安全風險,建議升級瀏覽器”,但是頁面完全可以正確显示,幾乎沒有什麼異常發生,看來兼容工作很到位。
    • 淘寶會出現很多頁面異常,說明IE兼容工作要求不高,基本正常即可,只是象徵性的加了幾條兼容性內容。
    • 去哪兒網也會出現很多頁面異常,但頁面布局還是正常的,看來也是儘力而為,不做要求。
    • 騰訊的頁面只有一個立即更新按鈕,一貫地友好。
    • 知乎直接404,好吧,強大。

     

    兼容IE的建議:

    一、建議不做任何兼容,IE6~11直接显示升級瀏覽器按鈕。

    二、如果一定要兼容,後端返回IE專用頁面,至少兼容IE8。

     

    2、屏幕分辨率兼容策略

    屏幕分辨率最少要考慮兼容便攜屏幕和移動屏幕兩種。可以參考去哪兒網的做法,把內容分成三類:移動端主菜單與導航欄;主要內容;擴展內容。屏幕分辨率高於480,显示主要內容、擴展內容。屏幕分辨率低於480,显示移動端主菜單與導航欄、主要內容。

     

    如果你的應用是管理軟件,則最好考慮兼容桌面屏幕、便攜屏幕和移動屏幕三種。Bootstrap5新增了超超大屏幕,則就是基於這種考慮。這時候,可以加入側邊欄自動隱藏/打開,主要內容用Flex方式組織,可以在頁面中並排显示多頁(類似於Word的頁面視圖)。

     

    3、跨平台兼容策略

    大型網站,手機網站與桌面網站是不同的入口,因此不存在兼容,是兩個單獨的應用程序。對於流量較小的網站,平台的兼容策略主要是應用響應式框架,加上移動端主菜單與導航欄即可,其次可以選用跨平台框架來實現在不同平台的差異化體驗。沒有這些框架對於Web網站來說不造成大的體驗下降。而如果需要開發混合移動、桌面應用,則需要認真考慮這些框架,畢竟用戶對本地應用的體驗期待要高很多。

     

     (全文完)

     

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

    【其他文章推薦】

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

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

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

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

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

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

  • EM(最大期望)算法推導、GMM的應用與代碼實現

    EM(最大期望)算法推導、GMM的應用與代碼實現

      EM算法是一種迭代算法,用於含有隱變量的概率模型參數的極大似然估計。

    使用EM算法的原因

      首先舉李航老師《統計學習方法》中的例子來說明為什麼要用EM算法估計含有隱變量的概率模型參數。

      假設有三枚硬幣,分別記作A, B, C。這些硬幣正面出現的概率分別是$\pi,p,q$。進行如下擲硬幣試驗:先擲硬幣A,根據其結果選出硬幣B或C,正面選硬幣B,反面邊硬幣C;然後擲選出的硬幣,擲硬幣的結果出現正面記作1,反面記作0;獨立地重複$n$次試驗,觀測結果為$\{y_1,y_2,…,y_n\}$。問三硬幣出現正面的概率。

      三硬幣模型(也就是第二枚硬幣正反面的概率)可以寫作

    $ \begin{aligned} &P(y|\pi,p,q) \\ =&\sum\limits_z P(y,z|\pi,p,q)\\ =&\sum\limits_z P(y|z,\pi,p,q)P(z|\pi,p,q)\\ =&\pi p^y(1-p)^{1-y}+(1-\pi)q^y(1-q)^{1-y} \end{aligned} $

      其中$z$表示硬幣A的結果,也就是前面說的隱變量。通常我們直接使用極大似然估計,即最大化似然函數

    $ \begin{aligned} &\max\limits_{\pi,p,q}\prod\limits_{i=1}^n P(y_i|\pi,p,q) \\ =&\max\limits_{\pi,p,q}\prod\limits_{i=1}^n[\pi p^{y_i}(1-p)^{1-y_i}+(1-\pi)q^{y_i}(1-q)^{1-y_i}]\\ =&\max\limits_{\pi,p,q}\sum\limits_{i=1}^n\log[\pi p^{y_i}(1-p)^{1-y_i}+(1-\pi)q^{y_i}(1-q)^{1-y_i}]\\ =&\max\limits_{\pi,p,q}L(\pi,p,q) \end{aligned} $

      分別對$\pi,p,q$求偏導並等於0,求解線性方程組來估計這三個參數。但是,由於它是帶有隱變量的,在獲取最終的隨機變量之前有一個分支選擇的過程,導致這個$\log$的內部是加和的形式,計算導數十分困難,而待求解的方程組不是線性方程組。當複雜度一高,解這種方程組幾乎成為不可能的事。以下推導EM算法,它以迭代的方式來求解這些參數,應該也算一種貪心吧。

    算法導出與理解

      對於參數為$\theta$且含有隱變量$Z$的概率模型,進行$n$次抽樣。假設隨機變量$Y$的觀察值為$\mathcal{Y} = \{y_1,y_2,…,y_n\}$,隱變量$Z$的$m$個可能的取值為$\mathcal{Z}=\{z_1,z_2,…,z_m\}$。

      寫出似然函數:

    $ \begin{aligned} L(\theta) &= \sum\limits_{Y\in\mathcal{Y}}\log P(Y|\theta)\\ &=\sum\limits_{Y\in\mathcal{Y}}\log \sum\limits_{Z\in \mathcal{Z}} P(Y,Z|\theta)\\ \end{aligned} $

      EM算法首先初始化參數$\theta = \theta^0$,然後每一步迭代都會使似然函數增大,即$L(\theta^{k+1})\ge L(\theta^k)$。如何做到不斷變大呢?考慮迭代前的似然函數(為了方便不用$\theta^{k+1}$):

    $ \begin{gather} \begin{aligned} L(\theta)=&\sum\limits_{Y\in \mathcal{Y}} \log\sum\limits_{Z\in \mathcal{Z}} P(Y,Z|\theta)\\ =&\sum\limits_{Y\in \mathcal{Y}} \log\sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)\frac{P(Y,Z|\theta)}{P(Z|Y,\theta^k)}\\ \end{aligned} \label{} \end{gather} $

      至於上式的第二個等式為什麼取出$P(Z|Y,\theta^k)$而不是別的,正向的原因我想不出來,馬後炮原因在後面記錄。

      考慮其中的求和

    $ \sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)=1$

      且由於$\log$函數是凹函數,因此由Jenson不等式得

    $ \begin{gather} \begin{aligned} L(\theta) \ge&\sum\limits_{Y\in \mathcal{Y}}\sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)\log\frac{P(Y,Z|\theta)}{P(Z|Y,\theta^k)}\\ =&B(\theta,\theta^k) \end{aligned}\label{} \end{gather} $

      當$\theta = \theta^k$時,有

    $ \begin{gather} \begin{aligned} L(\theta^k) \ge& B(\theta^k,\theta^k)\\ =&\sum\limits_{Y\in \mathcal{Y}}\sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)\log\frac{P(Y,Z|\theta^k)}{P(Z|Y,\theta^k)}\\ =&\sum\limits_{Y\in \mathcal{Y}}\sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)\log P(Y|\theta^k)\\ =&\sum\limits_{Y\in \mathcal{Y}}\log P(Y|\theta^k)\\ =&L(\theta^k)\\ \end{aligned} \label{} \end{gather} $

      也就是在這時,$(2)$式取等,即$L(\theta^k) = B(\theta^k,\theta^k)$。取

    $ \begin{gather} \theta^*=\text{arg}\max\limits_{\theta}B(\theta,\theta^k)\label{} \end{gather} $

      可得不等式

    $L(\theta^*)\ge B(\theta^*,\theta^k)\ge B(\theta^k,\theta^k) = L(\theta^k)$

      所以,我們只要優化$(4)$式,讓$\theta^{k+1} = \theta^*$,即可保證每次迭代的非遞減勢頭,有$L(\theta^{k+1})\ge L(\theta^k)$。而由於似然函數是概率乘積的對數,一定有$L(\theta) < 0$,所以迭代有上界並且會收斂。以下是《統計學習方法》中EM算法一次迭代的示意圖:

      進一步簡化$(4)$式,去掉優化無關項:

    $ \begin{aligned} \theta^*=&\text{arg}\max\limits_{\theta}B(\theta,\theta^k) \\ =&\text{arg}\max\limits_{\theta}\sum\limits_{Y\in \mathcal{Y}}\sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)\log\frac{P(Y,Z|\theta)}{P(Z|Y,\theta^k)} \\ =&\text{arg}\max\limits_{\theta}\sum\limits_{Y\in \mathcal{Y}}\sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)\log P(Y,Z|\theta) \\ =&\text{arg}\max\limits_{\theta}Q(\theta,\theta^k) \\ \end{aligned} $

      $Q$函數使用導數求極值的方程與沒有隱變量的方程類似,容易求解。

      綜上,EM算法的流程為:

      1. 設置$\theta^0$的初值。EM算法對初值是敏感的,不同初值迭代出來的結果可能不同。

      2. 更新$\theta^k = \text{arg}\max\limits_{\theta}Q(\theta,\theta^{k-1})$。理解上來說,通常將這一步分為計算$Q$與極大化$Q$兩步,即求期望E與求極大M,但在代碼中並不會將它們分出來,因此這裏濃縮為一步。另外,如果這個優化很難計算的話,因為有不等式的保證,直接取$\theta^k$為某個$\hat{\theta}$,只要有$Q(\hat{\theta},\theta^{k-1})\ge Q(\theta^{k-1},\theta^{k-1})$即可。

      3. 比較$\theta^k$與$\theta^{k-1}$的差異,比如求它們的差的二范數,若小於一定閾值就結束迭代,否則重複步驟2。

      下面記錄一下我對$(1)$式取出$P(Z|Y,\theta^k)$而不取別的$P$的理解:

      經過以上的推導,我認為這是為了給不等式取等創造條件。如果不能確定$L(\theta^k)$與$Q(\theta^k,\theta^k)$能否取等,那麼取$Q$的最大值$Q(\theta^*,\theta^k)$時,儘管有$Q(\theta^*,\theta^k)\ge Q(\theta^k,\theta^k)$,但並不能保證$L(\theta^*)\ge L(\theta^k)$,迭代的不減性質就就沒了。

      我這裏暫且把它看做一種巧合,是研究EM算法的大佬,碰巧想用Jenson不等式來迭代而構造出來的一種做法。本人段位還太弱,無法正向理解其中的緣故,只能以這種方式來揣度大佬的思路了。知乎大佬發的EM算法九層理解(點擊鏈接),我當前只能到第3層,有時間一定要拜讀一下深度學習之父的著作。

    高斯混合模型的應用

    迭代式推導

      假設高斯混合模型混合了$m$個高斯分佈,參數為$\theta = (\alpha_1,\theta_1,\alpha_2,\theta_2,…,\alpha_m,\theta_m),\theta_i=(\mu_i,\sigma_i)$則整個概率分佈為:

    $\displaystyle P(y|\theta) = \sum\limits_{i=1}^m\alpha_i \phi(y|\theta_i) =  \sum\limits_{i=1}^m\frac{\alpha_i }{\sqrt{2\pi}\sigma_i}\exp\left(-\frac{(y-\mu_i)^2}{2\sigma_i^2}\right),\;\text{where}\;\sum\limits_{j=1}^m\alpha_j = 1$

      對混合分佈抽樣$n$次得到$\{y_1,…,y_n\}$,則在第$k+1$次迭代,待優化式為:

    $\begin{gather}\begin{aligned} &\max\limits_{\theta}Q(\theta,\theta^k) \\ =&\max\limits_{\theta}\sum\limits_{Y\in \mathcal{Y}}\sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta^k)\log P(Y,Z|\theta) \\ =&\max\limits_{\theta}\sum\limits_{Y\in \mathcal{Y}}\sum\limits_{Z\in \mathcal{Z}} \frac{P(Z,Y|\theta^k)}{P(Y|\theta^k)}\log P(Y,Z|\theta) \\ =&\max\limits_{\theta}\sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)} \log \left[\alpha_j\phi(y_i|\theta_j)\right] \\ =&\max\limits_{\theta}\sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)} \log \left[ \frac{\alpha_j}{\sqrt{2\pi}\sigma_j}\exp\left(-\frac{(y_i-\mu_j)^2}{2\sigma_j^2}\right) \right]\\ =&\max\limits_{\theta}\sum\limits_{j=1}^m \sum\limits_{i=1}^n \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)} \left[ \log \alpha_j – \log \sigma_j-\frac{(y_i-\mu_j)^2}{2\sigma_j^2} \right]\\  \end{aligned} \label{}\end{gather}$

    計算α

      定義

    $\displaystyle n_j = \sum\limits_{i=1}^n \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)}$

      則對於$\alpha$,優化式為

    $\begin{gather} \begin{aligned} \max\limits_{\alpha}\sum\limits_{j=1}^m n_j \log \alpha_j \end{aligned} \label{}\end{gather}$

      又因為$\sum\limits_{j=1}^m \alpha_j=1$,所以只需優化$m-1$個參數,上式變為:

    $ \max\limits_\alpha \left[ \begin{matrix} n_1&n_2&\cdots &n_{m-1}&n_{m}\\ \end{matrix} \right] \cdot \left[ \begin{matrix} \log\alpha_1\\ \log\alpha_2\\ \vdots\\ \log\alpha_{m-1}\\ \log(1-\alpha_1-\cdots-\alpha_{m-1})\\ \end{matrix} \right] $

      對每個$\alpha_j$求導並等於0,得到線性方程組:

    $\left[\begin{matrix}n_1+n_m&n_1&n_1&\cdots&n_1\\n_2&n_2+n_m&n_2&\cdots&n_2\\n_3&n_3&n_3+n_m&\cdots&n_3\\&&&\vdots&\\n_{m-1}&n_{m-1}&n_{m-1}&\cdots&n_{m-1}+n_m\\\end{matrix}\right]\cdot\left[\begin{matrix}\alpha_1\\\alpha_2\\\alpha_3\\\vdots\\\alpha_{m-1}\\\end{matrix}\right]=\left[\begin{matrix}n_1\\n_2\\n_3\\\vdots\\n_{m-1}\\\end{matrix}\right]$

      求解這個爪形線性方程組,得到

    $\left[\begin{matrix}\sum_{j=1}^mn_j/n_1&0&0&\cdots&0\\-n_2/n_1&1&0&\cdots&0\\-n_3/n_1&0&1&\cdots&0\\&&&\vdots&\\-n_{m-1}/n_1&0&0&\cdots&1\\\end{matrix}\right]\cdot\left[\begin{matrix}\alpha_1\\\alpha_2\\\alpha_3\\\vdots\\\alpha_{m-1}\\\end{matrix}\right]=\left[\begin{matrix}1\\0\\0\\\vdots\\0\\\end{matrix}\right]$

      因為

    $\displaystyle \sum\limits_{j=1}^m n_j =   \sum\limits_{j=1}^m\sum\limits_{i=1}^n \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)}=\sum\limits_{i=1}^n \sum\limits_{j=1}^m \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)} =\sum\limits_{i=1}^n 1 =  n$

      解得

    $\displaystyle\alpha_j = \frac{n_j}{n} = \frac{1}{n}\sum\limits_{i=1}^n \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)}$

    計算σ與μ

      與$\alpha$不同,它的方程組是所有$\alpha_j$之間聯立的;而$\sigma,\mu$的方程組則是$\sigma_j$與$\mu_j$之間聯立的。定義

    $\displaystyle p_{ji} = \frac{\alpha_j^k\phi(y_i|\theta_j^k)} {\sum\limits_{l=1}^m \alpha_l^k\phi(y_i|\theta_l^k)}$

      則對於$\sigma_j,\mu_j$,優化式為(比較$(6),(7)$式的區別)

    $\begin{gather}\displaystyle\min\limits_{\sigma_j,\mu_j}\sum\limits_{i=1}^n p_{ji} \left(\log \sigma_j+\frac{(y_i-\mu_j)^2}{2\sigma_j^2} \right)\label{}\end{gather}$

      對上式求導等於0,解得

    $ \begin{aligned} &\mu_j = \frac{\sum\limits_{i=1}^np_{ji}y_i}{\sum\limits_{i=1}^np_{ji}} = \frac{\sum\limits_{i=1}^np_{ji}y_i}{n_j} = \frac{\sum\limits_{i=1}^np_{ji}y_i}{n\alpha_j}\\ &\sigma^2_j = \frac{\sum\limits_{i=1}^np_{ji}(y_i-\mu_j)^2}{\sum\limits_{i=1}^np_{ji}} = \frac{\sum\limits_{i=1}^np_{ji}(y_i-\mu_j)^2}{n_j} = \frac{\sum\limits_{i=1}^np_{ji}(y_i-\mu_j)^2}{n\alpha_j} \end{aligned} $

    代碼實現

      對於概率密度為$P(x) = −2x+2,x\in (0,1)$的隨機變量,以下代碼實現GMM對這一概率密度的的擬合。共10000個抽樣,GMM混合了100個高斯分佈。

    #%%定義參數、函數、抽樣
    import numpy as np
    import matplotlib.pyplot as plt
    
    dis_num = 100 #用於擬合的分佈數量
    sample_num = 10000 #用於擬合的分佈數量
    alphas = np.random.rand(dis_num) 
    alphas /= np.sum(alphas)  
    mus = np.random.rand(dis_num)
    sigmas = np.random.rand(dis_num)**2#方差,不是標準差
    samples = 1-(1-np.random.rand(sample_num))**0.5 #樣本
    C_pi = (2*np.pi)**0.5
    
    dis_val = np.zeros([sample_num,dis_num])    #每個樣本在每個分佈成員上都有值,形成一個sample_num*dis_num的矩陣
    pij = np.zeros([sample_num,dis_num])        #pij矩陣
    def calc_dis_val(sample,alpha,mu,sigma,c_pi):
        return alpha*np.exp(-(sample[:,np.newaxis]-mu)**2/(2*sigma))/(c_pi*sigma**0.5) 
    def calc_pij(dis_v):  
        return dis_v / dis_v.sum(axis = 1)[:,np.newaxis]      
    #%%優化 
    for i in range(1000):
        print(i)
        dis_val = calc_dis_val(samples,alphas,mus,sigmas,C_pi)
        pij = calc_pij(dis_val)  
        nj = pij.sum(axis = 0)
        alphas_before = alphas
        alphas = nj / sample_num
        mus = (pij*samples[:,np.newaxis]).sum(axis=0)/nj
        sigmas = (pij*(samples[:,np.newaxis] - mus)**2 ).sum(axis=0)/nj
        a = np.linalg.norm(alphas_before - alphas)
        print(a)
        if  a< 0.001:
            break
    
    #%%繪圖 
    plt.rcParams['font.sans-serif']=['SimHei'] #用來正常显示中文標籤
    plt.rcParams['axes.unicode_minus']=False #用來正常显示負號
    def get_dis_val(x,alpha,sigma,mu,c_pi):
        y = np.zeros([len(x)]) 
        for a,s,m in zip(alpha,sigma,mu):   
            y += a*np.exp(-(x-m)**2/(2*s))/(c_pi*s**0.5)   
        return y
    def paint(alpha,sigma,mu,c_pi,samples):
        x = np.linspace(-1,2,500)
        y = get_dis_val(x,alpha,sigma,mu,c_pi) 
        fig = plt.figure()
        ax = fig.add_subplot(111)
        ax.hist(samples,density = True,label = '抽樣分佈') 
        ax.plot(x,y,label = "擬合的概率密度")
        ax.legend(loc = 'best')
        plt.show()
    paint(alphas,sigmas,mus,C_pi,samples)

      以下是擬合結果圖,有點像是核函數估計,但是完全不同:

    EM算法的推廣

      EM算法的推廣是對EM算法的另一種解釋,最終的結論是一樣的,它可以使我們對EM算法的理解更加深入。它也解釋了我在$(1)$式下方提出的疑問:為什麼取出$P(Z|Y,\theta^k)$而不是別的。

      定義$F$函數,即所謂Free energy自由能(自由能具體是啥先不研究了):

    $ \begin{aligned} F(\tilde{P},\theta) &= E_{\tilde{P}}(\log P(Y,Z|\theta)) + H(\tilde{P})\\ &= \sum\limits_{Z\in \mathcal{Z}} \tilde{P}(Z)\log P(Y,Z|\theta) – \sum\limits_{Z\in \mathcal{Z}} \tilde{P}(Z)\log \tilde{P}(Z)\\ \end{aligned} $

      其中$\tilde{P}$是$Z$的某個概率分佈(不一定是單獨的分佈,可能是在某個條件下的分佈),$E_{\tilde{P}}$表示分佈$\tilde{P}$下的期望,$H$表示信息熵。

      我們計算一下,對於固定的$\theta$,什麼樣的$\tilde{P}$會使$F(\tilde{P},\theta) $最大。也就是找到一個函數$\tilde{P}_{\theta}$,使$F$極大,寫成優化的形式就是(這裡是找函數而不是找參數哦,理解上可能要用到泛函分析的內容):

    $ \begin{aligned} &\max\limits_{\tilde{P}} \sum\limits_{Z\in \mathcal{Z}} \tilde{P}(Z)\log P(Y,Z|\theta) – \sum\limits_{Z\in \mathcal{Z}} \tilde{P}(Z)\log \tilde{P}(Z)\\ &\;\text{s.t.}\; \sum\limits_{Z\in \mathcal{Z}}\tilde{P}(Z) = 1 \end{aligned} $

      拉格朗日函數(拉格朗日對偶性,點擊鏈接)為:

    $ \begin{aligned} L =  \sum\limits_{Z\in \mathcal{Z}} \tilde{P}(Z)\log P(Y,Z|\theta) – \sum\limits_{Z\in \mathcal{Z}} \tilde{P}(Z)\log \tilde{P}(Z)+ \lambda\left(1-\sum\limits_{Z\in \mathcal{Z}}\tilde{P}(Z)\right) \end{aligned} $

      因為每個$\tilde{P}(Z)$之間都是求和,沒有其它其它諸如乘積的操作,所以可以直接令$L$對某個$\tilde{P}(Z)$求導等於$0$來計算極值:

    $ \begin{aligned} \frac{\partial L}{\partial \tilde{P}(Z)} = \log P(Y,Z|\theta) – \log \tilde{P}(Z) -1 -\lambda = 0 \end{aligned} $

      於是可以推出:

    $ \begin{aligned} P(Y,Z|\theta) = e^{1+\lambda}\tilde{P}(Z) \end{aligned} $

      又由約束$\sum\limits_{Z\in \mathcal{Z}}\tilde{P}(Z) = 1$:

    $P(Y|\theta) = e^{1+\lambda}$

      於是得到

    $\begin{gather}\tilde{P}_{\theta}(Z) = P(Z|Y,\theta)\label{}\end{gather}$

      代回$F(\tilde{P},\theta)$,得到

    $ \begin{aligned} F(\tilde{P}_\theta,\theta) &= \sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta)\log P(Y,Z|\theta) – \sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta)\log P(Z|Y,\theta)\\ &= \sum\limits_{Z\in \mathcal{Z}} P(Z|Y,\theta)\log \frac{P(Y,Z|\theta)}{P(Z|Y,\theta)}\\ &= \log P(Y|\theta)\\ \end{aligned} $

      也就是說,對$F$關於$\tilde{P}$進行最大化后,$F$就是待求分佈的對數似然;然後再關於$\theta$最大化,也就算得了最終要估計的參數$\hat{\theta}$。所以,EM算法也可以解釋為$F$的極大-極大算法。優化結果$(8)$式也解釋了我之前在$(1)$式下方的提問。

      那麼,怎麼使用$F$函數進行估計呢?還是要用迭代來算,迭代方式是和前面介紹的一樣的(懶得記錄了,統計學習方法上直接看吧)。實際上,$F$函數的方法只是提供了EM算法的另一種解釋,具體方法上並沒有提升之處。

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 重識Java8函數式編程

    重識Java8函數式編程

    前言

    最近真的是太忙忙忙忙忙了,很久沒有更新文章了。最近工作中看到了幾段關於函數式編程的代碼,但是有點費解,於是就準備總結一下函數式編程。很多東西很簡單,但是如果不總結,可能會被它的各種變體所困擾。接觸Lambda表達式已經很久了,但是也一直是處於照葫蘆畫瓢的階段,所以想自己去編寫相關代碼,也有些捉襟見肘。

    1. Lambda表達式的不同形式

    // 基本形式
    參數 -> 主體
    

    1.1 形式一

    Runnable noArguments = () -> System.out.println("Hello World");
    

    該形式的Lambda表達式不包含參數,使用空括號()表示沒有參數。它實現了Runnable接口,該接口也只有一個run方法,沒有桉樹,且返回類型為void。

    1.2 形式二

    ActionListener oneArgument = event -> System.out.println("button clicked");
    

    該形式的Lambda表達式包含且只包含一個參數,可省略參數的符號。

    1.3 形式三

    Runnable multiStatement = () -> {
    	System.out.print("Hello"); 
        System.out.println(" World"); 
    };
    

    Lambda表達式的主體不僅可以使一個表達式,而且也可以是一段代碼塊,使用大括號{}將代碼塊括起來。該代碼塊和普通方法遵循的規則別無二致,可以用返回或拋出異常來退出。只有以行代碼的Lambda表達式也可以使用大括號,用以明確Lambda表達式從何處開始,到哪裡結束。

    1.4 形式四

    BinaryOperator<Long> add = (x, y) -> x + y;
    

    Lambda表達式也可以表示包含多個參數的方法,上面的Lambda表達式並不是將兩個数字相加,而是創建了一個函數,用來計算兩個数字相加的結果。變量add的類型時BinaryOperator ,它不是兩個数字的和,而是將兩個数字相加的那行代碼。

    1.5 形式五

    BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
    

    到目前為止,所有Lambda表達式中的參數類型都是由編譯器推斷得出的。但有時最好也可以显示聲明參數類型,此時就需要使用小括號將參數括起來,多個參數的情況也是如此。

    2. 引用值,而不是變量

    如果你曾使用過匿名內部類,也許遇到過這樣的情況:需要引用它所在方法里的變量。這是,需要將變量聲明為final。

    final String name = getUserName(); 
    button.addActionListener(new ActionListener() {
    	public void actionPerformed(ActionEvent event) { 
            System.out.println("hi " + name); 
        } 
    });
    

    將變量聲明為 final,意味着不能為其重複賦 值。同時也意味着在使用 final 變量時,實際上是在使用賦給該變量的一個特定的值。

    Java 8 雖然放鬆了這一限制,可以引用非 final 變量,但是該變量在既成事實上必須是 final(意思就是你不能再次對該變量賦值)。雖然無需將變量聲明為 final,但在 Lambda 表達式中,也無法用作非終態變量。如 果堅持用作非終態變量,編譯器就會報錯。 既成事實上的 final 是指只能給該變量賦值一次。換句話說,Lambda 表達式引用的是值, 而不是變量。

    例如:

    String name = getUserName(); 
    button.addActionListener(event -> System.out.println("hi " + name));
    

    3. 函數接口

    在 Java 里,所有方法參數都有固定的類型。假設將数字 3 作為參數傳給一個方法,則參數 的類型是 int。那麼,Lambda 表達式的類型又是什麼呢?

    使用只有一個方法的接口來表示某特定方法並反覆使用,是很早就有的習慣。使用 Swing 編寫過用戶界面的人對這種方式都不陌生,這裏無需再標新立異,Lambda 表達式也使用同樣的技巧,並將這種接口稱為函數接口。

    接口中單一方法的命名並不重要,只要方法簽名和 Lambda 表達式的類型匹配即可。可在函數接口中為參數起一個有意義的名字,增加代碼易讀性,便於更透徹 地理解參數的用途。

    3.1 Java中重要的函數接口

    接口 參數 返回類型 示例
    Predicate T boolean 判斷是否
    Consumer T void 輸出一個值
    Function<T,R> T T 獲得對象的名字
    Supplier None T 工廠方法
    UnaryOperator T T 邏輯非(!)
    BinaryOperator (T, T) T 求兩個數的乘積(*)

    3.2 函數接口定義

    定義函數接口需要使用到註解@FunctionalInterface

    例如:

    @FunctionalInterface
    public interface MyFuncInterface {
    	void print();
    }
    

    使用:

    public class MyFunctionalInterfaceTest {
        public static void main(String[] args) {
            doPrint(() -> System.out.println("java"));
        }
    
        public static void doPrint(MyFuncInterface my) {
            System.out.println("請問你喜歡什麼編程語言?");
            my.print();
        }
    }
    

    說明:

    這隻是一個很簡單的例子,有人覺得為什麼要搞這麼複雜,去定義一個接口?這個問題還是讀者在平時的工作中去感悟吧,總之,先學會怎麼用它。不至於看了別人寫的代碼都看不懂。

    至於我個人的理解,可以簡單聊聊。以前寫過JavaScript,裏面有一種語法就是將自定義函數B作為參數傳遞到另外一個函數A裏面,在函數A裏面會執行你自定義的函數B邏輯,我當時就非常喜歡這種特性,因為每個人關於函數B的實現可能不一樣,亦或者場景不一樣也會導致函數B的實現不一樣。我覺得Java8的這個函數式編程就是對這一特性的補充。

    4. 流

    流的常用操作有很多,例如collect(toList())mapfiltermaxmin等,下面介紹一下flatMapreduce

    4.1 flatMap

    flatMap 方法可用 Stream 替換值,然後將多個 Stream 連接成一個 Stream。

    List<Integer> together = Stream.of(asList(1, 2), asList(3, 4)) 				 
        .flatMap(numbers -> numbers.stream())
        .collect(toList()); 
    assertEquals(asList(1, 2, 3, 4), together);
    

    調用 stream 方法,將每個列錶轉換成 Stream 對象,其餘部分由 flatMap 方法處理。 flatMap 方法的相關函數接口和 map 方法的一樣,都是 Function 接口,只是方法的返回值 限定為 Stream 類型罷了。

    4.2 reduce

    reduce 操作可以實現從一組值中生成一個值。對於 count、min 和 max 方 法,因為常用而被納入標準庫中。事實上,這些方法都是 reduce 操作。

    如何通過 reduce 操作對 Stream 中的数字求和。以 0 作起點——一個空Stream 的求和結果,每一步都將 Stream 中的元素累加至 accumulator,遍歷至 Stream 中的 最後一個元素時,accumulator 的值就是所有元素的和。

    int count = Stream.of(1, 2, 3)
        .reduce(0, (acc, element) -> acc + element); 
    assertEquals(6, count);
    

    Lambda 表達式的返回值是最新的 acc,是上一輪 acc 的值和當前元素相加的結果。reducer 的類型是前面已介紹過的 BinaryOperator。

    5. Optional

    reduce 方法的一個重點尚未提及:reduce 方法有兩種形式,一種如前面出現的需要有一 個初始值,另一種變式則不需要有初始值。沒有初始值的情況下,reduce 的第一步使用 Stream 中的前兩個元素。有時,reduce 操作不存在有意義的初始值,這樣做就是有意義的,此時,reduce 方法返回一個 Optional 對象。

    Optional 是為核心類庫新設計的一個數據類型,用來替換 null 值。人們對原有的 null 值有很多抱怨。人們常常使用 null 值表示值不存在,Optional 對象能更好地表達這個概念。使用 null 代 表值不存在的最大問題在於 NullPointerException。一旦引用一個存儲 null 值的變量,程 序會立即崩潰。使用 Optional 對象有兩個目的:首先,Optional 對象鼓勵程序員適時檢查變量是否為空,以避免代碼缺陷;其次,它將一個類的 API 中可能為空的值文檔化,這比閱讀實現代碼要簡單很多。

    下面我們舉例說明 Optional 對象的 API,從而切身體會一下它的使用方法。使用工廠方法 of,可以從某個值創建出一個 Optional 對象。Optional 對象相當於值的容器,而該值可以 通過 get 方法提取。

    Optional<String> a = Optional.of("a"); 
    assertEquals("a", a.get());
    

    Optional 對象也可能為空,因此還有一個對應的工廠方法 empty,另外一個工廠方法 ofNullable 則可將一個空值轉換成 Optional 對象。下面的代碼同時展示 了第三個方法 isPresent 的用法(該方法表示一個 Optional 對象里是否有值)。

    Optional emptyOptional = Optional.empty(); 
    Optional alsoEmpty = Optional.ofNullable(null); assertFalse(emptyOptional.isPresent());
    

    使用 Optional 對象的方式之一是在調用 get() 方法前,先使用 isPresent 檢查 Optional 對象是否有值。使用 orElse 方法則更簡潔,當 Optional 對象為空時,該方法提供了一個 備選值。如果計算備選值在計算上太過繁瑣,即可使用 orElseGet 方法。該方法接受一個 Supplier 對象,只有在 Optional 對象真正為空時才會調用。

    assertEquals("b", emptyOptional.orElse("b")); 
    assertEquals("c", emptyOptional.orElseGet(() -> "c"));
    

    最後

    實踐是檢驗真理的唯一標準,多寫代碼,多思考,你的代碼才會越來越好。

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

    【其他文章推薦】

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

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

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

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

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

    網頁設計最專業,超強功能平台可客製化

  • RabbitMQ入門,我是動了心的

    RabbitMQ入門,我是動了心的

    人一輩子最值得炫耀的不應該是你的財富有多少(雖然這話說得有點違心,呵呵),而是你的學習能力。技術更新迭代的速度非常快,那作為程序員,我們就應該擁有一顆擁抱變化的心,积極地跟進。

    在 RabbitMQ 入門之前,我已經入門了 Redis、Elasticsearch 和 MongoDB,這讓我感覺自己富有極客精神,非常良好。

    小夥伴們在繼續閱讀之前,我必須要聲明一點,我對 RabbitMQ 並沒有進行很深入的研究,僅僅是因為要用,就學一下。但作為一名負責任的技術博主,我是動了心的,這篇入門教程,小夥伴們讀完后絕對會感到滿意,忍不住無情地點贊,以及赤裸裸地轉發。

    當然了,小夥伴們遇到文章中有錯誤的地方,不要手下留情,可以組團過來捶我,但要保證一點,不要打臉,我怕毀容。

    01、RabbitMQ 是什麼

    首先,我知道,Rabbit 是一隻兔子(哎呀媽呀,忍不住秀了一波自己的英語功底),可愛的形象已經躍然於我的腦海中了。那 MQ 又是什麼呢?是 Message Queue 的首字母縮寫,也就是說 RabbitMQ 是一款開源的消息隊列系統。

    RabbitMQ 的主要特點在於健壯性好、易於使用、高性能、高併發、集群易擴展,以及強大的開源社區支持。反正就是很牛逼的樣子。

    九年前我做大宗期貨交易的時候,也需要消息推送,那時候還不知道去找這種現成的中間件,就用自定義的隊列實現,結果搞了不少 bug,有些到現在還沒有解決,真的是不堪回首的往事啊。

    下圖是 RabbitMQ 的消息模型圖(來源於網絡,侵刪),小夥伴們來感受下。

    1)P 是 Producer,代表生產者,也就是消息的發送者,可以將消息發送到 X

    2)X 是 Exchange(為啥不是 E,我也很好奇),代表交換機,可以接受生產者發送的消息,並根據路由將消息發送給指定的隊列

    3)Q 是 Queue,也就是隊列,存放交換機發送來的消息

    4)C 是 Consumer,代表消費者,也就是消息的接受者,從隊列中獲取消息

    聽我這樣一解釋,是不是對 RabbitMQ 的印象就很具象化了?小夥伴們,學起來吧!

    02、安裝 Erlang

    咦,怎麼不是安裝 RabbitMQ 啊?先來看看官方的解釋。

    英文看不太懂,沒關係,我來補充兩人話。RabbitMQ 服務器是用 Erlang 語言編寫的,它的安裝包里並沒有集成 Erlang 的環境,因此需要先安裝 Erlang。小夥伴們不要擔心,Erlang 安裝起來沒有任何難度。

    Erlang 下載地址如下:

    https://erlang.org/download/otp_versions_tree.html

    最新的版本是 23.0.1,我選擇的是 64 位的版本,104M 左右。下載完就可以雙擊運行安裝,傻瓜式的。

    需要注意的是,我安裝的過程中,電腦重啟了一次,好像要安裝一個什麼庫,重啟之前忘記保存圖片了(sorry)。重啟后,重新雙擊運行 otp_win64_23.0.1.exe 文件完成 Erlang 安裝。

    03、安裝 RabbitMQ

    Erlang 安裝成功后,就可以安裝 RabbitMQ 了。下載地址如下所示:

    https://www.rabbitmq.com/install-windows.html

    找到下圖中的位置,選擇紅色框中的文件進行下載。

    安裝包只有 16.5M 大小,還是非常輕量級的。下載完后直接雙擊運行 exe 文件就可以傻瓜式地安裝了。

    安裝成功后,就可以將 RabbitMQ 作為 Windows 服務啟動,可以從“開始”菜單管理 RabbitMQ Windows 服務。

    點擊「RabbitMQ Command Prompt (sbin dir)」,進入命令行,輸入 rabbitmqctl.bat status 可確認 RabbitMQ 的啟動狀態。

    可以看到 RabbitMQ 一些狀態信息:

    • 進程 ID,也就是 PID 為 2816
    • 操作系統為 Windows
    • 當前的版本號為 3.8.4
    • Erlang 的配置信息

    命令行界面看起來不夠優雅,因此我們可以輸入以下命令來啟用客戶端管理 UI 插件:

    rabbitmq-plugins enable rabbitmq_management

    看到以下信息就可以確認插件啟用成功了。

    在瀏覽器地址欄輸入 http://localhost:15672/ 可以進入管理端界面,如下圖所示:

    04、在 Java 中使用 RabbitMQ

    有些小夥伴可能會問,“二哥,我是一名 Java 程序員,我該如何在 Java 中使用 RabbitMQ 呢?”這個問題問得好,這就來,這就來。

    第一步,在項目中添加 RabbitMQ 客戶端依賴:

    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.9.0</version>
    </dependency>

    第二步,我們來模擬一個最簡單的場景,一個生產者發送消息到隊列中,一個消費者從隊列中讀取消息並打印。

    官方對 RabbitMQ 有一個很好的解釋,我就“拿來主義”的用一下。在我上高中的年代,同學們之間最流行的交流方式不是 QQ、微信,甚至短信這些,而是書信。因為那時候還沒有智能手機,況且上學期間學校也是命令禁用手機的,所以書信是情感表達的最好方式。好懷念啊。

    假如我向女朋友小巷寫了一封情書,內容如下所示:

    致小巷
    你好呀,小巷。
    你走了以後我每天都感到很悶,就像堂吉訶德一樣,每天想念托波索的達辛妮亞。我現在已經養成了一種習慣,就是每兩三天就要找你說幾句不想對別人說的話。
    。。。。。。
    王二,5月20日

    那這封情書要寄給小巷,我就需要跑到郵局,買上郵票,投遞到郵箱當中。女朋友要收到這封情書,就需要郵遞員盡心儘力,不要弄丟了。

    RabbitMQ 就像郵局一樣,只不過處理的不是郵件,而是消息。之前解釋過了,P 就是生產者,C 就是消費者。

    新建生產者類 Wanger :

    public class Wanger {
        private final static String QUEUE_NAME = "love";
        public static void main(String[] args) throws IOException, TimeoutException {
            ConnectionFactory factory = new ConnectionFactory();

            try (Connection connection = factory.newConnection();
                 Channel channel = connection.createChannel()) {
                channel.queueDeclare(QUEUE_NAME, falsefalsefalsenull);
                String message = "小巷,我喜歡你。";
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
                System.out.println(" [王二] 發送 '" + message + "'");
            }
        }
    }

    1)QUEUE_NAME 為隊列名,也就是說,生產者發送的消息會放到 love 隊列中。

    2)通過以下方式創建服務器連接:

    ConnectionFactory factory = new ConnectionFactory();
    try (Connection connection = factory.newConnection();
                 Channel channel = connection.createChannel()) {

    ConnectionFactory 是一個非常方便的工廠類,可用來創建到 RabbitMQ 的默認連接(主機名為“localhost”)。然後,創建一個通道( Channel)來發送消息。

    Connection 和 Channel 類都實現了 Closeable 接口,所以可以使用 try-with-resource 語句,如果有小夥伴對 try-with-resource 語句不太熟悉,可以查看我之前寫的我去文章。

    3)在發送消息的時候,必須設置隊列名稱,通過 queueDeclare() 方法設置。

    4)basicPublish() 方法用於發布消息:

    • 第一個參數為交換機(exchange),當前場景不需要,因此設置為空字符串;
    • 第二個參數為路由關鍵字(routingKey),暫時使用隊列名填充;
    • 第三個參數為消息的其他參數(BasicProperties),暫時不配置;
    • 第四個參數為消息的主體,這裏為 UTF-8 格式的字節數組,可以有效地杜絕中文亂碼。

    生產者類有了,接下來新建消費者類 XiaoXiang:

    public class XiaoXiang {
        private final static String QUEUE_NAME = "love";
        public static void main(String[] args) throws IOException, TimeoutException {
            ConnectionFactory factory = new ConnectionFactory();
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            channel.queueDeclare(QUEUE_NAME, falsefalsefalsenull);
            System.out.println("等待接收消息");

            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [小巷] 接收到的消息 '" + message + "'");
            };
            channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
        }
    }

    1)創建通道的代碼和生產者差不多,只不過沒有使用 try-with-resource 語句來自動關閉連接和通道,因為我們希望消費者能夠一直保持連接,直到我們強制關閉它。

    2)在接收消息的時候,必須設置隊列名稱,通過 queueDeclare() 方法設置。

    3)由於 RabbitMQ 將會通過異步的方式向我們推送消息,因此我們需要提供了一個回調,該回調將對消息進行緩衝,直到我們做好準備接收它們為止。

    DeliverCallback deliverCallback = (consumerTag, delivery) -> {
        String message = new String(delivery.getBody(), "UTF-8");
        System.out.println(" [小巷] 接收到的消息 '" + message + "'");
    };

    basicConsume() 方法用於接收消息:

    • 第一個參數為隊列名(queue),和生產者相匹配(love)。

    • 第二個參數為 autoAck,如果為 true 的話,表明服務器要一次性交付消息。怎麼理解這個概念呢?小夥伴們可以在運行消費者類 XiaoXiang 類之前,先多次運行生產者類 Wanger,向隊列中發送多個消息,等到消費者類啟動后,你就會看到多條消息一次性接收到了,就像下面這樣。

    等待接收消息
     [小巷] 接收到的消息 '小巷,我喜歡你。'
     [小巷] 接收到的消息 '小巷,我喜歡你。'
     [小巷] 接收到的消息 '小巷,我喜歡你。'
    • 第三個參數為 DeliverCallback,也就是消息的回調函數。

    • 第四個參數為 CancelCallback,我暫時沒搞清楚是幹嘛的。

    在消息發送的過程中,也可以使用 RabbitMQ 的管理面板查看到消息的走勢圖,如下所示。

    05、鳴謝

    好了,我親愛的小夥伴們,以上就是本文的全部內容了,是不是看完后很想實操一把 RabbitMQ,趕快行動吧!如果你在學習的過程中遇到了問題,歡迎隨時和我交流,雖然我也是個菜鳥,但我有熱情啊。

    另外,如果你想寫入門級別的文章,這篇就是最好的範例。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 這篇文章,我們來談一談Spring中的屬性注入

    這篇文章,我們來談一談Spring中的屬性注入

    本系列文章:

    讀源碼,我們可以從第一行讀起

    你知道Spring是怎麼解析配置類的嗎?

    配置類為什麼要添加@Configuration註解?

    談談Spring中的對象跟Bean,你知道Spring怎麼創建對象的嗎?

    推薦閱讀:

    Spring官網閱讀 | 總結篇

    Spring雜談

    本系列文章將會帶你一行行的將Spring的源碼吃透,推薦閱讀的文章是閱讀源碼的基礎!

    前言

    在前面的文章中已經知道了Spring是如何將一個對象創建出來的,那麼緊接着,Spring就需要將這個對象變成一個真正的Bean了,這個過程主要分為兩步

    1. 屬性注入
    2. 初始化

    在這兩個過程中,Bean的後置處理器會穿插執行,其中有些後置處理器是為了幫助完成屬性注入或者初始化的,而有些後置處理器是Spring提供給程序員進行擴展的,當然,這二者並不衝突。整個Spring創建對象並將對象變成Bean的過程就是我們經常提到了Spring中Bean的生命周期。當然,本系列源碼分析的文章不會再對生命周期的概念做過多闡述了,如果大家有這方面的需求的話可以參考我之前的文章,或者關注我的公眾號:程序員DMZ

    Spring官網閱讀(九)Spring中Bean的生命周期(上)

    Spring官網閱讀(十)Spring中Bean的生命周期(下)

    源碼分析

    閑話不再多說,我們正式進入源碼分析階段,本文重點要分析的方法就是org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean,其源碼如下:

    doCreateBean

    	protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    			throws BeanCreationException {
    
    		// 創建對象的過程在上篇文章中我們已經介紹過了,這裏不再贅述
    		BeanWrapper instanceWrapper = null;
    		if (mbd.isSingleton()) {
    			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    		}
    		if (instanceWrapper == null) {
    			instanceWrapper = createBeanInstance(beanName, mbd, args);
    		}
            
            // 獲取到創建的這個對象
    		final Object bean = instanceWrapper.getWrappedInstance();
    		Class<?> beanType = instanceWrapper.getWrappedClass();
    		if (beanType != NullBean.class) {
    			mbd.resolvedTargetType = beanType;
    		}
    
    		// Allow post-processors to modify the merged bean definition.
            // 按照官方的註釋來說,這個地方是Spring提供的一個擴展點,對程序員而言,我們可以通過一個實現了MergedBeanDefinitionPostProcessor的後置處理器來修改bd中的屬性,從而影響到後續的Bean的生命周期
            // 不過官方自己實現的後置處理器並沒有去修改bd,而是調用了applyMergedBeanDefinitionPostProcessors方法
            // 這個方法名直譯過來就是-應用合併后的bd,也就是說它這裏只是對bd做了進一步的使用而沒有真正的修改
    		synchronized (mbd.postProcessingLock) {
               // bd只允許被處理一次
    			if (!mbd.postProcessed) {
    				try {
                        // 應用合併后的bd
    					applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
    				}
    				catch (Throwable ex) {
    					throw new BeanCreationException(mbd.getResourceDescription(), beanName,
    							"Post-processing of merged bean definition failed", ex);
    				}
                    // 標註這個bd已經被MergedBeanDefinitionPostProcessor的後置處理器處理過
                    // 那麼在第二次創建Bean的時候,不會再次調用applyMergedBeanDefinitionPostProcessors
    				mbd.postProcessed = true;
    			}
    		}
    
    		// 這裡是用來出來循環依賴的,關於循環以來,在介紹完正常的Bean的創建后,單獨用一篇文章說明
            // 這裏不做過多解釋
    		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
    				isSingletonCurrentlyInCreation(beanName));
    		if (earlySingletonExposure) {
    			if (logger.isTraceEnabled()) {
    				logger.trace("Eagerly caching bean '" + beanName +
    						"' to allow for resolving potential circular references");
    			}
    			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    		}
    
    
    		Object exposedObject = bean;
    		try {
                // 我們這篇文章重點要分析的就是populateBean方法,在這個方法中完成了屬性注入
    			populateBean(beanName, mbd, instanceWrapper);
                // 初始化
    			exposedObject = initializeBean(beanName, exposedObject, mbd);
    		}
    		catch (Throwable ex) {
    			// 省略異常代碼
    		}
    
    		// 後續代碼不在本文探討範圍內了,暫不考慮
    
    		return exposedObject;
    	}
    

    applyMergedBeanDefinitionPostProcessors

    源碼如下:

    // 可以看到這個方法的代碼還是很簡單的,就是調用了MergedBeanDefinitionPostProcessor的postProcessMergedBeanDefinition方法
    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);
            }
        }
    }
    

    這個時候我們就要思考一個問題,容器中現在有哪些後置處理器是MergedBeanDefinitionPostProcessor呢?

    查看這個方法的實現類我們會發現總共就這麼幾個類實現了MergedBeanDefinitionPostProcessor接口。實際上除了ApplicationListenerDetector之外,其餘的後置處理器的邏輯都差不多。我們在這裏我們主要就分析兩個後置處理

    1. ApplicationListenerDetector
    2. AutowiredAnnotationBeanPostProcessor

    ApplicationListenerDetector

    首先,我們來ApplicationListenerDetector,這個類在之前的文章中也多次提到過了,它的作用是用來處理嵌套Bean的情況,主要是保證能將嵌套在Bean標籤中的ApplicationListener也能添加到容器的監聽器集合中去。我們先通過一個例子來感受下這個後置處理器的作用吧

    配置文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    	<bean class="com.dmz.source.populate.service.DmzService" id="dmzService">
    		<constructor-arg name="orderService">
    			<bean class="com.dmz.source.populate.service.OrderService"/>
    		</constructor-arg>
    	</bean>
    </beans>
    

    示例代碼:

    // 事件
    public class DmzEvent extends ApplicationEvent {
    	public DmzEvent(Object source) {
    		super(source);
    	}
    }
    
    public class DmzService {
    
    	OrderService orderService;
    
    	public DmzService(OrderService orderService) {
    		this.orderService = orderService;
    	}
    }
    // 實現ApplicationListener接口
    public class OrderService implements ApplicationListener<DmzEvent> {
    	@Override
    	public void onApplicationEvent(DmzEvent event) {
    		System.out.println(event.getSource());
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext("application-populate.xml");
    		cc.publishEvent(new DmzEvent("my name is dmz"));
    	}
    }
    
    // 程序運行結果,控制台打印:my name is dmz
    

    說明OrderService已經被添加到了容器的監聽器集合中。但是請注意,在這種情況下,如果要使OrderService能夠執行監聽的邏輯,必須要滿足下面這兩個條件

    • 外部的Bean要是單例的,對於我們的例子而言就是dmzService
    • 內嵌的Bean也必須是單例的,在上面的例子中也就是orderService必須是單例

    另外需要注意的是,這種嵌套的Bean比較特殊,它雖然由Spring創建,但是確不存在於容器中,就是說我們不能將其作為依賴注入到別的Bean中。

    AutowiredAnnotationBeanPostProcessor

    對應源碼如下:

    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        // 找到注入的元數據,第一次是構建,後續可以直接從緩存中拿
        // 註解元數據其實就是當前這個類中的所有需要進行注入的“點”的集合,
        // 注入點(InjectedElement)包含兩種,字段/方法
        // 對應的就是AutowiredFieldElement/AutowiredMethodElement
        InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
        // 排除掉被外部管理的注入點
        metadata.checkConfigMembers(beanDefinition);
    }
    

    上面代碼的核心邏輯就是

    • 找到所有的注入點,其實就是被@Autowired註解修飾的方法以及字段,同時靜態的方法以及字段也會被排除
    • 排除掉被外部管理的注入點,在後續的源碼分析中我們再細說

    findAutowiringMetadata

    // 這個方法的核心邏輯就是先從緩存中獲取已經解析好的注入點信息,很明顯,在原型情況下才會使用緩存
    // 創建注入點的核心邏輯在buildAutowiringMetadata方法中
    private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
        String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
        InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
        // 可能我們會修改bd中的class屬性,那麼InjectionMetadata中的注入點信息也需要刷新
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized (this.injectionMetadataCache) {
                metadata = this.injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }
                    // 這裏真正創建注入點
                    metadata = buildAutowiringMetadata(clazz);
                    this.injectionMetadataCache.put(cacheKey, metadata);
                }
            }
        }
        return metadata;
    }
    

    buildAutowiringMetadata

    // 我們應用中使用@Autowired註解標註在字段上或者setter方法能夠完成屬性注入
    // 就是因為這個方法將@Autowired註解標註的方法以及字段封裝成InjectionMetadata
    // 在後續階段會調用InjectionMetadata的inject方法進行注入
    private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
        List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
        Class<?> targetClass = clazz;
    
        do {
            final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
    		// 處理所有的被@AutoWired/@Value註解標註的字段
            ReflectionUtils.doWithLocalFields(targetClass, field -> {
                AnnotationAttributes ann = findAutowiredAnnotation(field);
                if (ann != null) {
                    // 靜態字段會直接跳過
                    if (Modifier.isStatic(field.getModifiers())) {
                        // 省略日誌打印
                        return;
                    }
                    // 得到@AutoWired註解中的required屬性
                    boolean required = determineRequiredStatus(ann);
                    currElements.add(new AutowiredFieldElement(field, required));
                }
            });
    		// 處理所有的被@AutoWired註解標註的方法,相對於字段而言,這裏需要對橋接方法進行特殊處理
            ReflectionUtils.doWithLocalMethods(targetClass, method -> {
                // 只處理一種特殊的橋接場景,其餘的橋接方法都會被忽略
                Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                    return;
                }
                AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
                // 處理方法時需要注意,當父類中的方法被子類重寫時,如果子父類中的方法都加了@Autowired
                // 那麼此時父類方法不能被處理,即不能被封裝成一個AutowiredMethodElement
                if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                    if (Modifier.isStatic(method.getModifiers())) {
                        // 省略日誌打印
                        return;
                    }
                    if (method.getParameterCount() == 0) {
                        // 當方法的參數數量為0時,雖然不需要進行注入,但是還是會把這個方法作為注入點使用
                        // 這個方法最終還是會被調用
                        if (logger.isInfoEnabled()) {
                            logger.info("Autowired annotation should only be used on methods with parameters: " +
                                        method);
                        }
                    }
                    boolean required = determineRequiredStatus(ann);
                    // PropertyDescriptor: 屬性描述符
                    // 就是通過解析getter/setter方法,例如void getA()會解析得到一個屬性名稱為a
                    // readMethod為getA的PropertyDescriptor,
                    // 在《Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換》文中已經做過解釋
                    // 這裏不再贅述,這裏之所以來這麼一次查找是因為當XML中對這個屬性進行了配置后,
                    // 那麼就不會進行自動注入了,XML中显示指定的屬性優先級高於註解
                    PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);		   // 構造一個對應的AutowiredMethodElement,後續這個方法會被執行
                    // 方法的參數會被自動注入,這裏不限於setter方法
                    currElements.add(new AutowiredMethodElement(method, required, pd));
                }
            });
    		// 會處理父類中字段上及方法上的@AutoWired註解,並且父類的優先級比子類高
            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);
    
        return new InjectionMetadata(clazz, elements);
    }
    
    難點代碼分析

    上面的代碼整體來說應該很簡單,就如我們之前所說的,處理帶有@Autowired註解的字段及方法,同時會過濾掉所有的靜態字段及方法。上面複雜的地方在於對橋接方法的處理,可能大部分人都沒辦法理解這幾行代碼:

    // 第一行
    Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
    
    // 第二行
    if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
        return;
    }
    
    // 第三行
    if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
    
    }
    

    要理解這些代碼,首先你得知道什麼是橋接,為此我已經寫好了一篇文章:

    Spring雜談 | 從橋接方法到JVM方法調用

    除了在上面的文章中提到的橋接方法外,還有一種特殊的情況

    // A類跟B類在同一個包下,A不是public的
    class A {
    	public void test(){
    
    	}
    }
    
    // 在B中會生成一個跟A中的方法描述符(參數+返回值)一模一樣的橋接方法
    // 這個橋接方法實際上就是調用父類中的方法
    // 具體可以參考:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=63424113
    public class B extends A {
    }
    

    在理解了什麼是橋接之後,那麼上邊的第一行代碼你應該就能看懂了,就以上面的代碼為例,B中會生成一個橋接方法,對應的被橋接的方法就是A中的test方法。

    接着,我們看看第二行代碼

    public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) {
        // 說明這個方法本身就不是橋接方法,直接返回true
        if (bridgeMethod == bridgedMethod) {
            return true;
        }
        // 說明是橋接方法,並且方法描述符一致
        // 當且僅當是上面例子中描述的這種橋接的時候這個判斷才會滿足
        // 正常來說橋接方法跟被橋接方法的返回值+參數類型肯定不一致
        // 所以這個判斷會過濾掉其餘的所有類型的橋接方法
        // 只會保留本文提及這種特殊情況下產生的橋接方法
        return (bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()) &&
                Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes()));
    }
    

    最後,再來看看第三行代碼,核心就是這句 method.equals(ClassUtils.getMostSpecificMethod(method, clazz)。這句代碼的主要目的就是為了處理下面這種情況

    @Component
    public class D extends C {
    
    	@Autowired
    	@Override
    	public void setDmzService(DmzService dmzService) {
    		dmzService.init();
    		this.dmzService = dmzService;
    	}
    }
    
    // C不是Spring中的組件
    public class C {
    	DmzService dmzService;
        @Autowired
    	public void setDmzService(DmzService dmzService) {
    		this.dmzService = dmzService;
    	}
    }
    
    

    這種情況下,在處理D中的@Autowired註解時,雖然我們要處理父類中的@Autowired註解,但是因為子類中的方法已經複寫了父類中的方法,所以此時應該要跳過父類中的這個被複寫的方法,這就是第三行代碼的作用。

    小結

    到這裏我們主要分析了applyMergedBeanDefinitionPostProcessors這段代碼的作用,它的執行時機是在創建對象之後,屬性注入之前。按照官方的定義來說,到這裏我們仍然可以使用這個方法來修改bd的定義,那麼相對於通過BeanFactoryPostProcessor的方式修改bd,applyMergedBeanDefinitionPostProcessors這個方法影響的範圍更小,BeanFactoryPostProcessor影響的是整個Bean的生命周期,而applyMergedBeanDefinitionPostProcessors只會影響屬性注入之後的生命周期。

    其次,我們分析了Spring中內置的MergedBeanDefinitionPostProcessor,選取了其中兩個特殊的後置處理器進行分析,其中ApplicationListenerDetector主要處理內嵌的事件監聽器,而AutowiredAnnotationBeanPostProcessor主要用於處理@Autowired註解,實際上我們會發現,到這裏還只是完成了@Autowired註解的解析,還沒有真正開始進行注入,真正注入的邏輯在後面我們要分析的populateBean方法中,在這個方法中會使用解析好的注入元信息完成真正的屬性注入,那麼接下來我們就開始分析populateBean這個方法的源碼。

    populateBean

    循環依賴的代碼我們暫且跳過,後續出一篇專門文章解讀循環依賴,我們直接看看populateBean到底做了什麼。

    protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    
        // 處理空實例
        if (bw == null) {
            // 如果創建的對象為空,但是在XML中又配置了需要注入的屬性的話,那麼直接報錯
            if (mbd.hasPropertyValues()) {
                throw new BeanCreationException(
                    mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
            }
            else {
                // 空對象,不進行屬性注入
                return;
            }
        }
    
        // 滿足兩個條件,不是合成類 && 存在InstantiationAwareBeanPostProcessor
        // 其中InstantiationAwareBeanPostProcessor主要作用就是作為Bean的實例化前後的鈎子
        // 外加完成屬性注入,對於三個方法就是
        // postProcessBeforeInstantiation  創建對象前調用
        // postProcessAfterInstantiation   對象創建完成,@AutoWired註解解析后調用   
        // postProcessPropertyValues(已過期,被postProcessProperties替代) 進行屬性注入
        // 下面這段代碼的主要作用就是我們可以提供一個InstantiationAwareBeanPostProcessor
        // 提供的這個後置處理如果實現了postProcessAfterInstantiation方法並且返回false
        // 那麼可以跳過Spring默認的屬性注入,但是這也意味着我們要自己去實現屬性注入的邏輯
        // 所以一般情況下,我們也不會這麼去擴展
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
                if (bp instanceof InstantiationAwareBeanPostProcessor) {
                    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                    if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
                        return;
                    }
                }
            }
        }
    	
        // 這裏其實就是判斷XML是否提供了屬性相關配置
        PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
    	
        // 確認注入模型
        int resolvedAutowireMode = mbd.getResolvedAutowireMode();
        
        // 主要處理byName跟byType兩種注入模型,byConstructor這種注入模型在創建對象的時候已經處理過了
        // 這裏都是對自動注入進行處理,byName跟byType兩種注入模型均是依賴setter方法
        // byName,根據setter方法的名字來查找對應的依賴,例如setA,那麼就是去容器中查找名字為a的Bean
        // byType,根據setter方法的參數類型來查找對應的依賴,例如setXx(A a),就是去容器中查詢類型為A的bean
        if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
            MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
            if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
                autowireByName(beanName, mbd, bw, newPvs);
            }
            if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
                autowireByType(beanName, mbd, bw, newPvs);
            }
            // pvs是XML定義的屬性
            // 自動注入后,bean實際用到的屬性就應該要替換成自動注入后的屬性
            pvs = newPvs;
        }
    	// 檢查是否有InstantiationAwareBeanPostProcessor
        // 前面說過了,這個後置處理器就是來完成屬性注入的
        boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
        
        //  是否需要依賴檢查,默認是不會進行依賴檢查的
        boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
    	
        // 下面這段代碼有點麻煩了,因為涉及到版本問題
        // 其核心代碼就是調用了postProcessProperties完成了屬性注入
       
        PropertyDescriptor[] filteredPds = null;
        
        // 存在InstantiationAwareBeanPostProcessor,我們需要調用這類後置處理器的方法進行注入
    		if (hasInstAwareBpps) {
    			if (pvs == null) {
    				pvs = mbd.getPropertyValues();
    			}
    			for (BeanPostProcessor bp : getBeanPostProcessors()) {
    				if (bp instanceof InstantiationAwareBeanPostProcessor) {
    					InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                        // 這句就是核心
    					PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
    					if (pvsToUse == null) {
    						if (filteredPds == null) {
                                // 得到需要進行依賴檢查的屬性的集合
    							filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    						}
                            //  這個方法已經過時了,放到這裏就是為了兼容老版本
    						pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
    						if (pvsToUse == null) {
    							return;
    						}
    					}
    					pvs = pvsToUse;
    				}
    			}
    		}
        // 需要進行依賴檢查
    		if (needsDepCheck) {
    			if (filteredPds == null) {
                    // 得到需要進行依賴檢查的屬性的集合
    				filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    			}
                // 對需要進行依賴檢查的屬性進行依賴檢查
    			checkDependencies(beanName, mbd, filteredPds, pvs);
    		}
        // 將XML中的配置屬性應用到Bean上
    		if (pvs != null) {
    			applyPropertyValues(beanName, mbd, bw, pvs);
    		}
    }
    

    上面這段代碼主要可以拆分為三個部分

    1. 處理自動注入
    2. 處理屬性注入(主要指處理@Autowired註解),最重要
    3. 處理依賴檢查

    處理自動注入

    autowireByName

    對應源碼如下:

    protected void autowireByName(
        String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
        // 得到符合下麵條件的屬性名稱
        // 1.有setter方法
        // 2.需要進行依賴檢查
        // 3.不包含在XML配置中
        // 4.不是簡單類型(基本數據類型,枚舉,日期等)
        // 這裏可以看到XML配置優先級高於自動注入的優先級
        // 不進行依賴檢查的屬性,也不會進行屬性注入
        String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
        for (String propertyName : propertyNames) {
            if (containsBean(propertyName)) {
                Object bean = getBean(propertyName);
                // 將自動注入的屬性添加到pvs中去
                pvs.add(propertyName, bean);
                // 註冊bean之間的依賴關係
                registerDependentBean(propertyName, beanName);
                // 忽略日誌
            }
            // 忽略日誌
        }
    }
    

    看到了嗎?代碼就是這麼的簡單,不是要通過名稱注入嗎?直接通過beanName調用getBean,完事兒

    autowireByType

    	protected void autowireByType(
    			String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
    		// 這個類型轉換器,主要是在處理@Value時需要使用
    		TypeConverter converter = getCustomTypeConverter();
    		if (converter == null) {
    			converter = bw;
    		}
    
    		Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
    		// 得到符合下麵條件的屬性名稱
    		// 1.有setter方法
    		// 2.需要進行依賴檢查
    		// 3.不包含在XML配置中
    		// 4.不是簡單類型(基本數據類型,枚舉,日期等)
    		// 這裏可以看到XML配置優先級高於自動注入的優先級
    		String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
    		for (String propertyName : propertyNames) {
    			try {
    				PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
    				if (Object.class != pd.getPropertyType()) {
    					// 這裏獲取到的就是setter方法的參數,因為我們需要按照類型進行注入嘛
    					MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
    					
                        // 如果是PriorityOrdered在進行類型匹配時不會去匹配factoryBean
    					// 如果不是PriorityOrdered,那麼在查找對應類型的依賴的時候會會去匹factoryBean
    				 	// 這就是Spring的一種設計理念,實現了PriorityOrdered接口的Bean被認為是一種
                        // 最高優先級的Bean,這一類的Bean在進行為了完成裝配而去檢查類型時,
                        // 不去檢查factoryBean
                        // 具體可以參考PriorityOrdered接口上的註釋文檔
    					boolean eager = !(bw.getWrappedInstance() instanceof PriorityOrdered);
    					// 將參數封裝成為一個依賴描述符
    					// 依賴描述符會通過:依賴所在的類,字段名/方法名,依賴的具體類型等來描述這個依賴
    					DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
    					// 解析依賴,這裡會處理@Value註解
                        // 另外,通過指定的類型到容器中查找對應的bean
    					Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
    					if (autowiredArgument != null) {
    						// 將查找出來的依賴屬性添加到pvs中,後面會將這個pvs應用到bean上
    						pvs.add(propertyName, autowiredArgument);
    					}
    					// 註冊bean直接的依賴關係
    					for (String autowiredBeanName : autowiredBeanNames) {
    						registerDependentBean(autowiredBeanName, beanName);
    						if (logger.isDebugEnabled()) {
    							logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" +
    									propertyName + "' to bean named '" + autowiredBeanName + "'");
    						}
    					}
    					autowiredBeanNames.clear();
    				}
    			}
    			catch (BeansException ex) {
    				throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex);
    			}
    		}
    	}
    
    
    resolveDependency

    這個方法在Spring雜談 | 什麼是ObjectFactory?什麼是ObjectProvider?已經做過分析了,本文不再贅述。

    可以看到,真正做事的方法是doResolveDependency

    @Override
    public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName, Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    	// descriptor代表當前需要注入的那個字段,或者方法的參數,也就是注入點
        // ParameterNameDiscovery用於解析方法參數名稱
        descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
        // 1. Optional<T>
        if (Optional.class == descriptor.getDependencyType()) {
            return createOptionalDependency(descriptor, requestingBeanName);
        // 2. ObjectFactory<T>、ObjectProvider<T>
        } else if (ObjectFactory.class == descriptor.getDependencyType() ||
                 ObjectProvider.class == descriptor.getDependencyType()) {
            return new DependencyObjectProvider(descriptor, requestingBeanName);
        // 3. javax.inject.Provider<T>
        } else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
            return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
        } else {
            // 4. @Lazy
            Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
                descriptor, requestingBeanName);
            // 5. 正常情況
            if (result == null) {
                result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
            }
            return result;
        }
    }
    
    doResolveDependency
    	public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
    			@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    
    		InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
    		try {
    			Object shortcut = descriptor.resolveShortcut(this);
    			if (shortcut != null) {
    				return shortcut;
    			}
    			// 依賴的具體類型
    			Class<?> type = descriptor.getDependencyType();
    			// 處理@Value註解,這裏得到的時候@Value中的值
    			Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
    			if (value != null) {
    				if (value instanceof String) {
    					// 解析@Value中的佔位符
    					String strVal = resolveEmbeddedValue((String) value);
    					// 獲取到對應的bd
    					BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
    					// 處理EL表達式
    					value = evaluateBeanDefinitionString(strVal, bd);
    				}
    				// 通過解析el表達式可能還需要進行類型轉換
    				TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
    				return (descriptor.getField() != null ?
    						converter.convertIfNecessary(value, type, descriptor.getField()) :
    						converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
    			}
    			
                // 對map,collection,數組類型的依賴進行處理
    			// 最終會根據集合中的元素類型,調用findAutowireCandidates方法
    			Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
    			if (multipleBeans != null) {
    				return multipleBeans;
    			}
    			
                // 根據指定類型可能會找到多個bean
                // 這裏返回的既有可能是對象,也有可能是對象的類型
                // 這是因為到這裏還不能明確的確定當前bean到底依賴的是哪一個bean
                // 所以如果只會返回這個依賴的類型以及對應名稱,最後還需要調用getBean(beanName)
                // 去創建這個Bean
    			Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
    			// 一個都沒找到,直接拋出異常
    			if (matchingBeans.isEmpty()) {
    				if (isRequired(descriptor)) {
    					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
    				}
    				return null;
    			}
    
    			String autowiredBeanName;
    			Object instanceCandidate;
    			// 通過類型找到了多個
    			if (matchingBeans.size() > 1) {
    				// 根據是否是主Bean
    				// 是否是最高優先級的Bean
    				// 是否是名稱匹配的Bean
    				// 來確定具體的需要注入的Bean的名稱
                    // 到這裏可以知道,Spring在查找依賴的時候遵循先類型再名稱的原則(沒有@Qualifier註解情況下)
    				autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
    				if (autowiredBeanName == null) {
    					// 無法推斷出具體的名稱
    					// 如果依賴是必須的,直接拋出異常
    					// 如果依賴不是必須的,但是這個依賴類型不是集合或者數組,那麼也拋出異常
    					if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
    						return descriptor.resolveNotUnique(type, matchingBeans);
    					}
    					// 依賴不是必須的,但是依賴類型是集合或者數組,那麼返回一個null
    					else {
    						return null;
    					}
    				}
    				instanceCandidate = matchingBeans.get(autowiredBeanName);
    			}
    			else {
    				// 直接找到了一個對應的Bean
    				Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
    				autowiredBeanName = entry.getKey();
    				instanceCandidate = entry.getValue();
    			}
    			if (autowiredBeanNames != null) {
    				autowiredBeanNames.add(autowiredBeanName);
    			}
                
                // 前面已經說過了,這裏可能返回的是Bean的類型,所以需要進一步調用getBean
    			if (instanceCandidate instanceof Class) {
    				instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
    			}
                
                // 做一些檢查,如果依賴是必須的,查找出來的依賴是一個null,那麼報錯
                // 查詢處理的依賴類型不符合,也報錯
    			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());
    			}
    			return result;
    		}
    		finally {
    			ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
    		}
    	}
    
    findAutowireCandidates
    protected Map<String, Object> findAutowireCandidates(
        @Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {
    	
        // 簡單來說,這裏就是到容器中查詢requiredType類型的所有bean的名稱的集合
        // 這裡會根據descriptor.isEager()來決定是否要匹配factoryBean類型的Bean
        // 如果isEager()為true,那麼會匹配factoryBean,反之,不會
        String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
            this, requiredType, true, descriptor.isEager());
       
        Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);
       
        // 第一步會到resolvableDependencies這個集合中查詢是否已經存在了解析好的依賴
        // 像我們之所以能夠直接在Bean中注入applicationContext對象
        // 就是因為Spring之前就將這個對象放入了resolvableDependencies集合中
        for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {
            if (autowiringType.isAssignableFrom(requiredType)) {
                Object autowiringValue = this.resolvableDependencies.get(autowiringType);
                
                // 如果resolvableDependencies放入的是一個ObjectFactory類型的依賴
                // 那麼在這裡會生成一個代理對象
                // 例如,我們可以在controller中直接注入request對象
                // 就是因為,容器啟動時就在resolvableDependencies放入了一個鍵值對
                // 其中key為:Request.class,value為:ObjectFactory
                // 在實際注入時放入的是一個代理對象
                autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
                if (requiredType.isInstance(autowiringValue)) {
                    // 這裏放入的key不是Bean的名稱
                    // value是實際依賴的對象
                    result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
                    break;
                }
            }
        }
        
        // 接下來開始對之前查找出來的類型匹配的所有BeanName進行處理
        for (String candidate : candidateNames) {
            // 不是自引用,什麼是自引用?
            // 1.候選的Bean的名稱跟需要進行注入的Bean名稱相同,意味着,自己注入自己
            // 2.或者候選的Bean對應的factoryBean的名稱跟需要注入的Bean名稱相同,
            // 也就是說A依賴了B但是B的創建又需要依賴A
            // 要符合注入的條件
            if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
                // 調用addCandidateEntry,加入到返回集合中,後文有對這個方法的分析
                addCandidateEntry(result, candidate, descriptor, requiredType);
            }
        }
        
        // 排除自引用的情況下,沒有找到一個合適的依賴
        if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
            // 1.先走fallback邏輯,Spring提供的一個擴展吧,感覺沒什麼卵用
            // 默認情況下fallback的依賴描述符就是自身
            DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
            for (String candidate : candidateNames) {
                if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {
                    addCandidateEntry(result, candidate, descriptor, requiredType);
                }
            }
            // fallback還是失敗
            if (result.isEmpty()) {
                // 處理自引用
                // 從這裏可以看出,自引用的優先級是很低的,只有在容器中真正的只有這個Bean能作為
                // 候選者的時候,才會去處理,否則自引用是被排除掉的
                for (String candidate : candidateNames) {
                    if (isSelfReference(beanName, candidate) &&
                        // 不是一個集合或者
                        // 是一個集合,但是beanName跟candidate的factoryBeanName相同
                        (!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
                        isAutowireCandidate(candidate, fallbackDescriptor)) {
                        addCandidateEntry(result, candidate, descriptor, requiredType);
                    }
                }
            }
        }
        return result;
    }
    
    
    // candidates:就是findAutowireCandidates方法要返回的候選集合
    // candidateName:當前的這個候選Bean的名稱
    // descriptor:依賴描述符
    // requiredType:依賴的類型
    private void addCandidateEntry(Map<String, Object> candidates, String candidateName,
                                   DependencyDescriptor descriptor, Class<?> requiredType) {
    	
        // 如果依賴是一個集合,或者容器中已經包含這個單例了
        // 那麼直接調用getBean方法創建或者獲取這個Bean
        if (descriptor instanceof MultiElementDescriptor || containsSingleton(candidateName)) {
            Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this);
            candidates.put(candidateName, (beanInstance instanceof NullBean ? null : beanInstance));
        }
        // 如果依賴的類型不是一個集合,這個時候還不能確定到底要使用哪個依賴,
        // 所以不能將這些Bean創建出來,所以這個時候,放入candidates是Bean的名稱以及類型
        else {
            candidates.put(candidateName, getType(candidateName));
        }
    }
    

    處理屬性注入(@Autowired)

    postProcessProperties

    // 在applyMergedBeanDefinitionPostProcessors方法執行的時候,
    // 已經解析過了@Autowired註解(buildAutowiringMetadata方法)
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
        // 這裏獲取到的是解析過的緩存好的注入元數據
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {
            // 直接調用inject方法
            // 存在兩種InjectionMetadata
            // 1.AutowiredFieldElement
            // 2.AutowiredMethodElement
            // 分別對應字段的屬性注入以及方法的屬性注入
            metadata.inject(bean, beanName, pvs);
        }
        catch (BeanCreationException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
    }
    
    字段的屬性注入
    // 最終反射調用filed.set方法
    protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
        Field field = (Field) this.member;
        Object value;
        if (this.cached) {
            // 第一次注入的時候肯定沒有緩存
            // 這裏也是對原型情況的處理
            value = resolvedCachedArgument(beanName, this.cachedFieldValue);
        } else {
            DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
            desc.setContainingClass(bean.getClass());
            Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
            Assert.state(beanFactory != null, "No BeanFactory available");
            TypeConverter typeConverter = beanFactory.getTypeConverter();
            try {
                // 這裏可以看到,對@Autowired註解在字段上的處理
                // 跟byType下自動注入的處理是一樣的,就是調用resolveDependency方法
                value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
            } catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
            }
            synchronized (this) {
                // 沒有緩存過的話,這裏需要進行緩存
                if (!this.cached) {
                    if (value != null || this.required) {
                        this.cachedFieldValue = desc;
                        // 註冊Bean之間的依賴關係
                        registerDependentBeans(beanName, autowiredBeanNames);
                        // 如果這個類型的依賴只存在一個的話,我們就能確定這個Bean的名稱
                        // 那麼直接將這個名稱緩存到ShortcutDependencyDescriptor中
                        // 第二次進行注入的時候就可以直接調用getBean(beanName)得到這個依賴了
                        // 實際上正常也只有一個,多個就報錯了
                        // 另外這裡會過濾掉@Vlaue得到的依賴
                        if (autowiredBeanNames.size() == 1) {
                            String autowiredBeanName = autowiredBeanNames.iterator().next();
                            // 通過resolvableDependencies這個集合找的依賴不滿足containsBean條件
                            // 不會進行緩存,因為緩存實際還是要調用getBean,而resolvableDependencies
                            // 是沒法通過getBean獲取的
                            if (beanFactory.containsBean(autowiredBeanName) &&
                                beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {							 // 依賴描述符封裝成ShortcutDependencyDescriptor進行緩存
                                this.cachedFieldValue = new ShortcutDependencyDescriptor(
                                    desc, autowiredBeanName, field.getType());
                            }
                        }
                    } else {
                        this.cachedFieldValue = null;
                    }
                    this.cached = true;
                }
            }
        }
        if (value != null) {
            // 反射調用Field.set方法
            ReflectionUtils.makeAccessible(field);
            field.set(bean, value);
        }
    }
    
    方法的屬性注入
    // 代碼看着很長,實際上邏輯跟字段注入基本一樣
    protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
        // 判斷XML中是否配置了這個屬性,如果配置了直接跳過
        // 換而言之,XML配置的屬性優先級高於@Autowired註解
        if (checkPropertySkipping(pvs)) {
            return;
        }
        Method method = (Method) this.member;
        Object[] arguments;
        if (this.cached) {
            arguments = resolveCachedArguments(beanName);
        } else {
            // 通過方法參數類型構造依賴描述符
            // 邏輯基本一樣的,最終也是調用beanFactory.resolveDependency方法
            Class<?>[] paramTypes = method.getParameterTypes();
            arguments = new Object[paramTypes.length];
            DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length];
            Set<String> autowiredBeans = new LinkedHashSet<>(paramTypes.length);
            Assert.state(beanFactory != null, "No BeanFactory available");
            TypeConverter typeConverter = beanFactory.getTypeConverter();
            
            // 遍歷方法的每個參數
            for (int i = 0; i < arguments.length; i++) {
                MethodParameter methodParam = new MethodParameter(method, i);
                DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
                currDesc.setContainingClass(bean.getClass());
                descriptors[i] = currDesc;
                try {
                    // 還是要調用這個方法
                    Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
                    if (arg == null && !this.required) {
                        arguments = null;
                        break;
                    }
                    arguments[i] = arg;
                } catch (BeansException ex) {
                    throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
                }
            }
            synchronized (this) {
                if (!this.cached) {
                    if (arguments != null) {
                        Object[] cachedMethodArguments = new Object[paramTypes.length];
                        System.arraycopy(descriptors, 0, cachedMethodArguments, 0, arguments.length);  
                        // 註冊bean之間的依賴關係
                        registerDependentBeans(beanName, autowiredBeans);
                        
                        // 跟字段注入差不多,存在@Value註解,不進行緩存
                        if (autowiredBeans.size() == paramTypes.length) {
                            Iterator<String> it = autowiredBeans.iterator();
                            for (int i = 0; i < paramTypes.length; i++) {
                                String autowiredBeanName = it.next();
                                if (beanFactory.containsBean(autowiredBeanName) &&
                                    beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
                                    cachedMethodArguments[i] = new ShortcutDependencyDescriptor(
                                        descriptors[i], autowiredBeanName, paramTypes[i]);
                                }
                            }
                        }
                        this.cachedMethodArguments = cachedMethodArguments;
                    } else {
                        this.cachedMethodArguments = null;
                    }
                    this.cached = true;
                }
            }
        }
        if (arguments != null) {
            try {
                // 反射調用方法
                // 像我們的setter方法就是在這裏調用的
                ReflectionUtils.makeAccessible(method);
                method.invoke(bean, arguments);
            } catch (InvocationTargetException ex) {
                throw ex.getTargetException();
            }
        }
    }
    

    處理依賴檢查

    protected void checkDependencies(
        String beanName, AbstractBeanDefinition mbd, PropertyDescriptor[] pds, PropertyValues pvs)
        throws UnsatisfiedDependencyException {
    
        int dependencyCheck = mbd.getDependencyCheck();
        for (PropertyDescriptor pd : pds) {
            
            // 有set方法但是在pvs中沒有對應屬性,那麼需要判斷這個屬性是否要進行依賴檢查
            // 如果需要進行依賴檢查的話,就需要報錯了
            // pvs中保存的是自動注入以及XML配置的屬性
            if (pd.getWriteMethod() != null && !pvs.contains(pd.getName())) {
               
                // 是否是基本屬性,枚舉/日期等也包括在內
                boolean isSimple = BeanUtils.isSimpleProperty(pd.getPropertyType());
               	
                // 如果DEPENDENCY_CHECK_ALL,對任意屬性都開啟了依賴檢查,報錯
                // DEPENDENCY_CHECK_SIMPLE,對基本屬性開啟了依賴檢查並且是基本屬性,報錯
                // DEPENDENCY_CHECK_OBJECTS,對非基本屬性開啟了依賴檢查並且不是非基本屬性,報錯
                boolean unsatisfied = (dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_ALL) ||
                    (isSimple && dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_SIMPLE) ||
                    (!isSimple && dependencyCheck == AbstractBeanDefinition.DEPENDENCY_CHECK_OBJECTS);
                
                if (unsatisfied) {
                    throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, pd.getName(),
                                                             "Set this property value or disable dependency checking for this bean.");
                }
            }
        }
    }
    

    將解析出來的屬性應用到Bean上

    到這一步解析出來的屬性主要有三個來源

    1. XML中配置的
    2. 通過byName的方式自動注入的
    3. 通過byType的方式自動注入的

    但是在應用到Bean前還需要做一步類型轉換,這一部分代碼實際上跟我們之前在Spring官網閱讀(十四)Spring中的BeanWrapper及類型轉換介紹的差不多,而且因為XML跟自動注入的方式都不常見,正常@Autowired的方式進行注入的話,這個方法沒有什麼用,所以本文就不再贅述。

    總結

    本文我們主要分析了Spring在屬性注入過程中的相關代碼,整個屬性注入可以分為兩個部分

    1. @Autowired/@Vale的方式完成屬性注入
    2. 自動注入(byType/byName

    完成屬性注入的核心方法其實就是doResolveDependencydoResolveDependency這個方法的邏輯簡單來說分為兩步:

    1. 通過依賴類型查詢到所有的類型匹配的bean的名稱
    2. 如果找到了多個的話,再根據依賴的名稱匹配對應的Bean的名稱
    3. 調用getBean得到這個需要被注入的Bean
    4. 最後反射調用字段的set方法完成屬性注入

    從上面也可以知道,其實整個屬性注入的邏輯是很簡單的。

    如果本文對你有幫助的話,記得點個贊吧!也歡迎關注我的公眾號,微信搜索:程序員DMZ,或者掃描下方二維碼,跟着我一起認認真真學Java,踏踏實實做一個coder。

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

    【其他文章推薦】

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

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

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

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

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

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