人臉檢測和人臉識別原理,微調(Fine-tune)原理

一、MTCNN的原理

  搭建人臉識別系統的第一步是人臉檢測,也就是在圖片中找到人臉的位置。在這個過程中,系統的輸入是一張可能含有人臉的圖片,輸出是人臉位置的矩形框,如下圖所示。一般來說,人臉檢測應該可以正確檢測出圖片中存在的所有人臉,不能用遺漏,也不能有錯檢。  

   

  獲得包含人臉的矩形框后,第二步要做的就是人臉對齊(Face Alignment)。原始圖片中人臉的姿態、位置可能較大的區別,為了之後統一處理,要把人臉“擺正”。為此,需要檢測人臉中的關鍵點(Landmark),如眼睛的位置、鼻子的位置、嘴巴的位置、臉的輪廓點等。根據這些關鍵點可以使用仿射變換將人臉統一校準,以盡量消除姿勢不同帶來的誤差,人臉對齊的過程如下圖所示。

   

  這裏介紹一種基於深度卷積神經網絡的人臉檢測和人臉對齊方法—-MTCNN,它是基於卷積神經網絡的一種高精度的實時人臉檢測和對齊技術。MT是英文單詞Multi-task的縮寫,意思就是這種方法可以同時完成人臉檢測的人臉對齊兩項任務。相比於傳統方法,MTCNN的性能更好,可以更精確的定位人臉,此外,MTCNN也可以做到實時的檢測。

  MTCNN由三個神經網絡組成,分別是P-Net、R-Net、O-Net。在使用這些網絡之前,首先要將原始圖片縮放到不同尺度,形成一個“圖像金字塔”,如下圖所示。

   

  接着會對每個尺度的圖片通過神經網絡計算一遍。這樣做的原因在於:原始圖片中的人臉存在不同的尺度,如有的人臉比較大,有的人臉比較小。對於比較小的人臉,可以在放大后的圖片上檢測;對於比較大的人臉,可以在縮小后的圖片上進行檢測。這樣,就可以在統一的尺度下檢測人臉了。

  現在再來討論第一個網絡P-Net的結構,如下圖所示

   

  P-Net的輸入是一個寬和高皆為12像素,同時是3通道的RGB圖像,該網絡要判斷這個12×12的圖像中是否含有人臉,並且給出人臉框和關鍵點的位置。因此對應的輸出應該由3部分組成:

  (1)第一個部分要判斷該圖像是否是人臉(上圖中的face classification),輸出向量的形狀為1x1x2,也就是兩個值,分別為該圖像是人臉的概率,以及該圖像不是人臉的概率。這兩個值加起來應該嚴格等1。之所以使用兩個值來表示,是為了方便定義交叉熵損失。
  (2)第二個部分給出框的精確位置(上圖中的bounding box regression),一般稱之為框回歸。P-Net輸入的12×12的圖像塊可能並不是完美的人臉框的位置,如有的時候人臉並不正好為方形,有的時候12×12的圖像塊可能偏左或偏右,因此需要輸出當前框位置相對於完美的人臉框位置的偏移。這個偏移由四個變量組成。一般地,對於圖像中的框,可以用四個數來表示它的位置:框左上角的橫坐標、框左上角的縱坐標、框的寬度、框的高度。因此,框回歸輸出的值是:框左上角的橫坐標的相對偏移、框左上角的縱坐標的相對偏移、框的寬度的誤差、框的 高度的誤差。輸出向量的形狀就是上圖中的1x1x4。
  (3)第三個部分給出人臉的5個關鍵點的位置。5個關鍵點分別為:左眼的位置、右眼的位置、鼻子的位置、左嘴角的位置、右嘴角的位置。每個關鍵點又需要橫坐標和縱坐標來表示,因此輸出一共是10維(即1x1x10)

  上面的介紹大致就是P-Net的結構了。在實際計算中,通過P-Net中第一層卷積的移動,會對圖像中每一個12×12的區域做一次人臉檢測,得到的結構如下圖所示:

   

  圖中框的大小各有不同,除了框回歸的影響外,主要是因為將圖片金字塔的各個尺度都使用P-Net計算了一遍,因此形成了大小不同的人臉框。P-Net的結果還是比較粗糙的,所以接下來又使用R-Net進一步調優。R-Net的網絡結構如下圖所示。

   

  這個結構與之前的P-Net非常類似,P-Net的輸入是12x12x3的圖像,R-Net是24x24x3的圖像,也就是說,R-Net判斷24x24x3的圖像中是否含有人臉,以及預測關鍵點的位置。R-Net的輸出和P-Net完全一樣,同樣有人臉判別、框回歸、關鍵點位置預測三部分組成。

  在實際應用中,對每個P-Net輸出可能為人臉的區域都放縮到24×24的大小,在輸入到R-Net中,進行進一步的判定。得到的結果如下圖所示:

   

  顯然R-Net消除了P-Net中很多誤判的情況。

  進一步把所有得到的區域縮放成48×48的大小,輸入到最後的O-Net中,O-Net的結構同樣與P-Net類似,不同點在於它的輸入是48x48x3的圖像,網絡的通道數和層數也更多了。O-Net的網絡的結構如下圖所示:

   

  檢測結果如下圖所示:

   

  從P-Net到R-Net,最後再到O-Net,網絡輸入的圖片越來越大,卷積層的通道數越來越多,內部的層數也越來越多,因此它們識別人臉的準確率應該是越來越高的。同時,P-Net的運行速度是最快的,R-Net的速度其次,O-Net的運行速度最慢。之所以要使用三個網絡,是因為如果一開始直接對圖中的每個區域使用O-Net,速度會非常慢慢。實際上P-Net先做了一遍過濾,將過濾后的結果再交給R-Net進行過濾,最後將過濾后的結果交給效果最好但速度較慢的O-Net進行判別。這樣在每一步都提前減少了需要判別的數量,有效降低了處理時間。

  最後介紹MTCNN的損失定義和訓練過程。MTCNN中每個網絡都有三部分輸出,因此損失也由三部分組成。針對人臉判別部分,直接使用交叉熵損失,針對框回歸和關鍵點判定,直接使用L2損失。最後這三部分損失各自乘以自身的權重再加起來,就形成最後的總損失了。在訓練P-Net和R-Net時,更關心框位置的準確性,而較少關注關鍵點判定的損失,因此關鍵點判定損失的權重很小。對於O-Net,關鍵點判定損失的權重較大。

二、使用深度卷積網絡提取特徵

  經過人臉檢測和人臉對齊兩個步驟,就獲得了包含人臉的區域圖像,接下來就要進行人臉識別了。這一步一般是使用深度卷積網絡,將輸入的人臉圖像轉換為一個向量的表示,也就是所謂的“特徵”。

  如何針對人臉來提取特徵?可以先來回憶VGG16的網絡結構(見),輸入神經網絡的是圖像,經過一系列卷積計算后,全連接分類得到類別概率。

  在通常的圖像應用中,可以去掉全連接層,使用卷積層的最後一層當作圖像的“特徵”。但如果對人臉識別問題同樣採用這種方法,即使用卷積層最後一層做為人臉的“向量表示”,效果其實是不好的。這其中的原因和改進方法是什麼?在後面會談到,這裏先談談希望這種人臉的“向量表示”應該具有哪些性質。

  在理想的狀況下,希望“向量表示”之間的距離可以直接反映人臉的相似度

  對於同一個人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較小。對於不同人的兩張人臉圖像,對應的向量之間的歐幾里得距離應該比較大。

  例如,設人臉圖像為$x_{1}$,$x_{2}$,對應的特徵為$f(x_{1})$,$f(x_{2})$,當$x_{1}$,$x_{2}$對應是同一個人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很小,而當$x_{1}$,$x_{2}$是不同人的人臉時,$f(x_{1})$,$f(x_{2})$的距離$\left \| f(x_{1}),f(x_{2}) \right \|$2應該很大。

  在原始的CNN模型中,使用的是Softmax損失。Softmax是類別間的損失,對於人臉來說,每一類就是一個人。儘管使用Softmax損失可以區別出每個人,但其本質上沒有對每一類的向量表示之間的距離做出要求。

  舉個例子,使用CNN對MNIST進行分類,設計一個特殊的卷積網絡,讓其最後一層的向量變為2維,此時可以畫出每一類對應的2維向量(圖中一種顏色對應一種類別),如下圖所示:

   

  上圖是我們直接使用softmax訓練得到的結果,它就不符合我們希望特徵具有的特點:

  (1)我們希望同一類對應的向量表示盡可能接近。但這裏同一類(如紫色),可能具有很大的類間距離;
  (2)我們希望不同類對應的向量應該盡可能遠。但在圖中靠中心的位置,各個類別的距離都很近;

  對於人臉圖像同樣會出現類似的情況,對此,有很改進方法。這裏介紹其中兩種:一種是三元組損失函數(Triplet Loss),一種是中心損失函數。 

三、三元組損失的定義

  三元組損失函數的原理:既然目標是特徵之間的距離應該具備某些性質,那麼我們就圍繞這個距離來設計損失。具體的,我們每次都在訓練數據中抽出三張人臉圖像,第一張圖像記為$x_{i}^{a}$,第二張圖像記為$x_{i}^{p}$,第三張圖像記為$x_{i}^{n}$。在這樣的一個“三元組”中,$x_{i}^{a}$和$x_{i}^{p}$對應的是同一個人的圖像,而$x_{i}^{n}$是另外一個不同的人的人臉圖像。因此,距離$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}$應該較小,而距離$\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}$應該較大。嚴格來說,三元組損失要求下面的式子成立:

   $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$

  然後計算相同人臉之間與不同人臉之間距離的平方

   $\left [ \left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2} \right ]_{+}$

  上式表達相同人臉間的距離平方至少要比不同人臉間的距離平方小α(取平方主要是為了方便求導),據此,上式實際上就是相當於一個損失函數。這樣的話,當三元組的距離滿足 $\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}+\alpha <\left \| f(x_{i}^{a})- f(x_{i}^{p})\right \|_{2}^{2}$時,不產生任何損失,此時$L_{i}=0$。當距離不滿足上述等式時,就會有值為$\left \| f(x_{i}^{a})-f(x_{i}^{p}) \right \|_{2}^{2}+\alpha -\left \| f(x_{i}^{a})-f(x_{i}^{n}) \right \|_{2}^{2}$的損失。此外,在訓練時會固定$\left \| f(x) \right \|_{2}=1$,以保證特徵不會無限地“遠離”。

  三元組損失直接對距離進行優化,因此可以解決人臉的特徵表示問題。但是在訓練過程中,三元組的選擇非常地有技巧性。如果每次都是隨機選擇三元組,雖然模型可以正確的收斂,但是並不能達到最好的性能。如果加入”難例挖掘”,即每次都選擇最難分辨率的三元組進行訓練,模型又往往不能正確的收斂。對此,又提出每次都選擇那些“半難”(Semi-hard)的數據進行訓練,讓模型在可以收斂的同時也保持良好的性能。此外,使用三元組損失訓練人臉模型通常還需要非常大的人臉數據集,才能取得較好的效果。

四、中心損失的定義

  與三元組損失不同,中心損失(Center Loss)不直接對距離進行優化,它保留了原有的分類模型,但又為每個類(在人臉模型中,一個類就對應一個人)指定了一個類別中心。同一類的圖像對應的特徵都應該盡量靠近自己的類別中心,不同類的類別中心盡量遠離。與三元組損失函數相比,使用中心損失訓練人臉模型不需要使用特別的採樣方法,而且利用較少的圖像就可以達到與單元組損失相似的效果。下面我們一起來學習中心損失的定義:

   還是設輸入的人臉圖像為$x_{i}$,該人臉對應的類別為$y_{i}$,對每個類別都規定一個類別中心,記作$c_{yi}$。希望每個人臉圖像對應的特徵$f(x_{i})$都盡可能接近其中心$c_{yi}$。因此定義中心損失為:

    $L_{i}=\frac{1}{2}\left \| f(x_{i})-c_{yi}\right \|_{2}^{2}$

  多張圖像的中心損失就是將它們的值加在一起:

   $L_{center}=\sum\limits_{i}L_i$

  這是一個非常簡單的定義。不過還有一個問題沒有解決,那就是如何確定每個類別的中心$c_{yi}$呢?從理論上來說,類別$y_{i}$的最佳中心應該是它對應的所有圖片的特徵的平均值。但如果採取這樣的定義,那麼在每一次梯度下降時,都要對所有圖片計算一次$c_{yi}$,計算複雜度就太高了。針對這種情況,不妨近似一處理下,在初始階段,先隨機確定$c_{yi}$,接着在每個batch內,使用$L_i=\|f(x_i)-c_{yi}\|_2^2$對當前batch內的$c_{yi}$ 也計算梯度,並使用該梯度更新$c_{yi}$ 。此外,不能只使用中心損失來訓練分類模型,還需要加入Softmax損失,也就是說,最終的損失由兩部分構成,即$L = L_{softmax}+\lambda L_{center}$,其中$\lambda $是一個超參數。

  最後來總結使用中心損失來訓練人臉模型的過程。首先隨機初始化各个中心$c_{yi}$,接着不斷地取出batch進行訓練,在每個batch中,使用總的損失$L$,除了使用神經網絡模型的參數對模型進行更新外,也對$c_{yi}$進行計算梯度,並更新中心的位置。

  中心損失可以讓訓練處的特徵具有“內聚性”。還是以MNIST的例子來說,在未加入中心損失時,訓練的結果不具有內聚性。再加入中心損失后,得到的特徵如下圖所示。 

   

從圖中可以看出,當中心損失的權重λ越大時,生成的特徵就會具有越明顯的“內聚性” 。

五、使用特徵設計應用

當提取出特徵后,剩下的問題就非常簡單了。因為這種特徵已經具有了相同人對應的向量的距離小,不同人對應的向量距離大的特點,接下來,一般的應用有以下幾類:

  • 人臉驗證(Face Identification)。就是檢測A、B是否屬於同一個人。只需要計算向量之間的距離,設定合適的報警閾值(threshold)即可。
  • 人臉識別(Face Recognition)。這個應用是最多的,給定一張圖片,檢測數據庫中與之最相似的人臉。顯然可以被轉換為一個求距離的最近鄰問題。
  • 人臉聚類(Face Clustering)。在數據庫中對人臉進行聚類,直接用K-means即可。

 

 

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

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

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

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

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

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

Android DecorView 與 Activity 綁定原理分析

一年多以前,曾經以為自己對 View 的添加显示邏輯已經有所了解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的添加显示。

概論

Android 中 Activity 是作為應用程序的載體存在,代表着一個完整的用戶界面,提供了一個窗口來繪製各種視圖,當 Activity 啟動時,我們會通過 setContentView 方法來設置一個內容視圖,這個內容視圖就是用戶看到的界面。那麼 View 和 activity 是如何關聯在一起的呢 ?

 上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:

  • Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。

  • PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製窗口。並且,該類內部包含了一個 DecorView 對象,該 DectorView 對象是所有應用窗口的根 View。
  • DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(窗口內容的容器)。

  • ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設置它的子 View 。

  • WindowManager : 是一個接口,裏面常用的方法有:添加View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。
  • WindowManagerService (WMS) : 負責管理各 app 窗口的創建,更新,刪除, 显示順序。運行在 system_server 進程。

ViewRootImpl :擁有 DecorView 的實例,通過該實例來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通過調用 IWindow 一些方法,通過 Binder 通信的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來調用 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub,每一個應用進程都有一個唯一的 Session 對象與 WMS 通信。

DecorView 的創建 

先從 Mainactivity 中的代碼看起,首先是調用了 setContentView;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

該方法是父類 AppCompatActivity 的方法,最終會調用 AppCompatDelegateImpl 的 setContentView 方法:

// AppCompatDelegateImpl  
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }

ensureSubDecor 從字面理解就是創建 subDecorView,這個是根據主題來創建的,下文也會講到。創建完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 布局添加到裏面。不過大家注意的是,在添加之前先調用 removeAllViews() 方法,確保沒有其他子 View 的干擾。

    private void ensureSubDecor() {
        if (!this.mSubDecorInstalled) {
            this.mSubDecor = this.createSubDecor(); 
            ......
        }
        ......
    }        

 最終會調用 createSubDecor() ,來看看裏面的具體代碼邏輯:

 private ViewGroup createSubDecor() {
        // 1、獲取主題參數,進行一些設置,包括標題,actionbar 等 
        TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
        if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        } else {
            if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
                this.requestWindowFeature(1);
            } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
                this.requestWindowFeature(108);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                this.requestWindowFeature(109);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                this.requestWindowFeature(10);
            }

            this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
            a.recycle();
            // 2、確保優先初始化 DecorView
            this.mWindow.getDecorView();
            LayoutInflater inflater = LayoutInflater.from(this.mContext);
            ViewGroup subDecor = null;
            // 3、根據不同的設置來對 subDecor 進行初始化
            if (!this.mWindowNoTitle) {
                if (this.mIsFloating) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
                    this.mHasActionBar = this.mOverlayActionBar = false;
                } else if (this.mHasActionBar) {
                    TypedValue outValue = new TypedValue();
                    this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
                    Object themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
                    } else {
                        themedContext = this.mContext;
                    }

                    subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
                    this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                    this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
                    if (this.mOverlayActionBar) {
                        this.mDecorContentParent.initFeature(109);
                    }

                    if (this.mFeatureProgress) {
                        this.mDecorContentParent.initFeature(2);
                    }

                    if (this.mFeatureIndeterminateProgress) {
                        this.mDecorContentParent.initFeature(5);
                    }
                }
            } else {
                if (this.mOverlayActionMode) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
                } else {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
                }

                if (VERSION.SDK_INT >= 21) {
                    ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
                        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                            int top = insets.getSystemWindowInsetTop();
                            int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
                            if (top != newTop) {
                                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
                            }

                            return ViewCompat.onApplyWindowInsets(v, insets);
                        }
                    });
                } else {
                    ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
                        public void onFitSystemWindows(Rect insets) {
                            insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
                        }
                    });
                }
            }

            if (subDecor == null) {
                throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
            } else {
                if (this.mDecorContentParent == null) {
                    this.mTitleView = (TextView)subDecor.findViewById(id.title);
                }

                ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                if (windowContentView != null) {
                    while(windowContentView.getChildCount() > 0) {
                        View child = windowContentView.getChildAt(0);
                        windowContentView.removeViewAt(0);
                        contentView.addView(child);
                    }

                    windowContentView.setId(-1);
                    contentView.setId(16908290);
                    if (windowContentView instanceof FrameLayout) {
                        ((FrameLayout)windowContentView).setForeground((Drawable)null);
                    }
                }
                // 將 subDecor 添加到 DecorView 中
                this.mWindow.setContentView(subDecor);
                contentView.setAttachListener(new OnAttachListener() {
                    public void onAttachedFromWindow() {
                    }

                    public void onDetachedFromWindow() {
                        AppCompatDelegateImpl.this.dismissPopups();
                    }
                });
                return subDecor;
            }
        }
    }
                    

上面的代碼總結來說就是在做一件事,就是創建 subDecor。攤開來說具體如下:

1、根據用戶選擇的主題來設置一些显示特性,包括標題,actionbar 等。

2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。

3、最後添加到 DecorView中。

添加的具體代碼如下:此處是通過調用 

 // AppCompatDelegateImpl   this.mWindow.getDecorView();

 // phoneWindow    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
 

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
 // 生成 DecorView             mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
 // 這樣 DecorView 就持有了window             mDecor.setWindow(this);
        }
      ......
}


   protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
   }

到此,DecorView 的創建就講完了。可是我們似乎並沒有看到 DecorView 是被添加的,什麼時候對用戶可見的。

 WindowManager

View 創建完以後,那 Decorview 是怎麼添加到屏幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。

看 ActivityThread 中的 handleResumeActivity 方法:

// ActivityThread
public
void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) {           // 這裏也會調用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }

上面的代碼主要做了以下幾件事:

1、獲取到 DecorView,設置不可見,然後通過 wm.addView(decor, l) 將 view 添加到 WindowManager;

2、在某些情況下,比如此時點擊了輸入框調起了鍵盤,就會調用 wm.updateViewLayout(decor, l) 來更新 View 的布局。

3、這些做完以後,會調用 activity 的  makeVisible ,讓視圖可見。如果此時 DecorView 沒有添加到 WindowManager,那麼會添加。 

// Activity
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }

 接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:

// WindowManagerGlobal  
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
           // ......
    
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
           // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            } 
}

在這裏,實例化了 ViewRootImpl 。同時調用 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裏還保存了 DecorView ,Params,以及 ViewRootImpl 的實例。

現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。

 ViewRootImpl

實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程序窗口的 DecorView 都有一個與之關聯的 ViewRootImpl 對象,這種關聯關係是由 WindowManager 來維護的。

先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:

   /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
               
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.

                requestLayout();
                ......
            }
        }
    }

這裏先將 mView 保存了 DecorView 的實例,然後調用 requestLayout() 方法,以完成應用程序用戶界面的初次布局。

 public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

因為是 UI 繪製,所以一定要確保是在主線程進行的,checkThread 主要是做一個校驗。接着調用 scheduleTraversals 開始計劃繪製了。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這裏主要關注兩點:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步消息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理異步消息,如果沒有異步消息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設置為異步消息)。

mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。

mTraversalRunnable :TraversalRunnable 的實例,是一個Runnable,最終肯定會調用其 run 方法:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal,如其名,開始繪製了,該方法內部最終會調用 performTraversals 進行繪製。

  void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

到此,DecorView 與 activity 之間的綁定關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。 

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Java虛擬機詳解(十)——類加載過程

  在上一篇文章中,我們詳細的介紹了Java,那麼這些Class文件是如何被加載到內存,由虛擬機來直接使用的呢?這就是本篇博客將要介紹的——類加載過程。

1、類的生命周期

  類從被加載到虛擬機內存開始,到卸載出內存為止,其聲明周期流程如下:

  

  上圖中紅色的5個部分(加載、驗證、準備、初始化、卸載)順序是確定的,也就是說,類的加載過程必須按照這種順序按部就班的開始。這裏的“開始”不是按部就班的“進行”或者“完成”,因為這些階段通常是互相交叉混合的進行的,通常會在一個階段執行過程中調用另一個階段。

2、加載

  “加載”階段是“類加載”生命周期的第一個階段。在加載階段,虛擬機要完成下面三件事:

  ①、通過一個類的全限定名來獲取定義此類的二進制字節流。

  ②、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

  ③、在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。

  PS:類的全限定名可以理解為這個類存放的絕對路徑。方法區是JDK1.7以前定義的運行時數據區,而在JDK1.8以後改為元數據區(Metaspace),主要用於存放被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。詳情可以參考這邊該系列的第二篇文章——。

  另外,我們看第一點——通過類的權限定名來獲取定義此類的二進制流,這裏並沒有明確指明要從哪裡獲取以及怎樣獲取,也就是說並沒有明確規定一定要我們從一個 Class 文件中獲取。基於此,在Java的發展過程中,充滿創造力的開發人員在這個舞台上玩出了各種花樣:

  1、從 ZIP 包中讀取。這稱為後面的 JAR、EAR、WAR 格式的基礎。

  2、從網絡中獲取。比較典型的應用就是 Applet。

  3、運行時計算生成。這就是動態代理技術。

  4、由其它文件生成。比如 JSP 應用。

  5、從數據庫中讀取。

  加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,然後在Java堆中實例化一個 java.lang.Class 類的對象,這個對象將作為程序訪問方法區中這些類型數據的外部接口。

  注意,加載階段與連接階段的部分內容(如一部分字節碼文件的格式校驗)是交叉進行的,加載階段尚未完成,連接階段可能已經開始了。

3、驗證

  驗證是連接階段的第一步,作用是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  我們說Java語言本身是相對安全,因為編譯器的存在,純粹的Java代碼要訪問數組邊界外的數據、跳轉到不存在的代碼行之類的,是要被編譯器拒絕的。但是前面我們也說過,Class 文件不一定非要從Java源碼編譯過來,可以使用任何途徑,包括你很牛逼,直接用十六進制編輯器來編寫 Class 文件。

  所以,如果虛擬機不檢查輸入的字節流,將會載入有害的字節流而導致系統崩潰。但是虛擬機規範對於檢查哪些方面,何時檢查,怎麼檢查都沒有明確的規定,不同的虛擬機實現方式可能都會有所不同,但是大致都會完成下面四個方面的檢查。

①、文件格式驗證

  校驗字節流是否符合Class文件格式的規範,並且能夠被當前版本的虛擬機處理。

  一、是否以魔數 0xCAFEBABE 開頭。

  二、主、次版本號是否是當前虛擬機處理範圍之內。

  三、常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)

  四、指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。

  五、CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據。

  六、Class 文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

  以上是一部分校驗內容,當然遠不止這些。經過這些校驗后,字節流才會進入內存的方法區中存儲,接下來後面的三個階段校驗都是基於方法區的存儲結構進行的。

②、元數據驗證

  第二個階段主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範要求。

  一、這個類是否有父類(除了java.lang.Object 類之外,所有的類都應當有父類)。

  二、這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

  三、如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有普通方法。

  四、類中的字段、方法是否與父類產生了矛盾(例如覆蓋了父類的final字段、或者出現不符合規則的重載)

③、字節碼驗證

  第三個階段字節碼驗證是整個驗證階段中最複雜的,主要是進行數據流和控制流分析。該階段將對類的方法進行分析,保證被校驗的方法在運行時不會做出危害虛擬機安全的行為。

  一、保證任意時刻操作數棧中的數據類型與指令代碼序列都能配合工作。例如不會出現在操作數棧中放置了一個 int 類型的數據,使用時卻按照 long 類型來加載到本地變量表中。

  二、保證跳轉指令不會跳轉到方法體以外的字節碼指令中。

  三、保證方法體中的類型轉換是有效的。比如把一個子類對象賦值給父類數據類型,這是安全的。但是把父類對象賦值給子類數據類型,甚至賦值給完全不相干的類型,這就是不合法的。

④、符號引用驗證

  符號引用驗證主要是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,通常需要校驗如下內容:

  一、符號引用中通過字符串描述的全限定名是否能夠找到相應的類。

  二、在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。

  三、符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可以被當前類訪問。

4、準備

  準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存是在方法區中進行分配。

  注意:

  一、上面說的是類變量,也就是被 static 修飾的變量,不包括實例變量。實例變量會在對象實例化時隨着對象一起分配在堆中。

  二、初始值,指的是一些數據類型的默認值。基本的數據類型初始值如下(引用類型的初始值為null):

  

 

   比如,定義 public static int value = 123 。那麼在準備階段過後,value 的值是 0 而不是 123,把 value 賦值為123 是在程序被編譯后,存放在類的構造器方法之中,是在初始化階段才會被執行。但是有一種特殊情況,通過final 修飾的屬性,比如 定義 public final static int value = 123,那麼在準備階段過後,value 就被賦值為123了。

5、解析

  解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程。

  符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。

  直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在內存中存在。

  解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用,分別對應於常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANTS_InterfaceMethodref_info四種類型常量。

6、初始化

   初始化階段是類加載階段的最後一步,前面過程中,除第一個加載階段可以通過用戶自定義類加載器參与之外,其餘過程都是完全由虛擬機主導和控制。而到了初始化階段,則開始真正執行類中定義的Java程序代碼(或者說是字節碼)。

  在前面介紹的準備階段中,類變量已經被賦值過初始值了,而初始化階段,則根據程序員的編碼去初始化變量和資源。

  換句話來說,初始化階段是執行類構造器<clinit>() 方法的過程

  ①、<clinit>() 方法 是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。

  比如如下代碼會報錯:

  

 

   但是你把第 14 行代碼放到 static 靜態代碼塊的上面就不會報錯了。或者不改變代碼順序,將第 11 行代碼移除,也不會報錯。

  ②、<clinit>() 方法與類的構造函數(或者說是實例構造器<init>()方法)不同,它不需要显示的調用父類構造器,虛擬機會保證在子類的<init>()方法執行之前,父類的<init>()方法已經執行完畢。因此虛擬機中第一個被執行的<init>()方法的類肯定是 java.lang.Object。

  ③、由於父類的<clinit>() 方法先執行,所以父類中定義的靜態語句塊要優先於子類的變量賦值操作。

  ④、<clinit>() 方法對於接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成<clinit>() 方法。

  ⑤、接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>() 方法。但接口與類不同的是,執行接口中的<clinit>() 方法不需要先執行父接口的<clinit>() 方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。

  ⑥、接口的實現類在初始化時也一樣不會執行接口的<clinit>() 方法。

  ⑦、虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖和同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>() 方法,其他的線程都需要阻塞等待,直到活動線程執行<clinit>() 方法完畢。如果在一個類的<clinit>() 方法中有很耗時的操作,那麼可能造成多個進程的阻塞。

  比如對於如下代碼:

package com.yb.carton.controller;

/**
 * Create by YSOcean
 */
public class ClassLoadInitTest {


    static class Hello{
        static {
            if(true){
                System.out.println(Thread.currentThread().getName() + "init");
                while(true){}
            }
        }
    }

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h1 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();


        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h2 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();
    }

}

View Code

  運行結果如下:

  

 

   線程1搶到了執行<clinit>() 方法,但是該方法是一個死循環,線程2將一直阻塞等待。

  知道了類的初始化過程,那麼類的初始化何時被觸發呢?JVM大概規定了如下幾種情況:

  ①、當虛擬機啟動時,初始化用戶指定的類。

  ②、當遇到用以新建目標類實例的 new 指令時,初始化 new 指定的目標類。

  ③、當遇到調用靜態方法的指令時,初始化該靜態方法所在的類。

  ④、當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類。

  ⑤、子類的初始化會觸發父類的初始化。

  ⑥、如果一個接口定義了 default 方法,那麼直接實現或間接實現該接口的類的初始化,會觸發該接口的初始化。

  ⑦、使用反射 API 對某個類進行反射調用時,會初始化這個類。

  ⑧、當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

 

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

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Java——內部類詳解

說起內部類,大家肯定感覺熟悉又陌生,因為一定在很多框架源碼中有看到別人使用過,但又感覺自己使用的比較少,今天我就帶你具體來看看內部類。

內部類基礎

所謂內部類就是在類的內部繼續定義其他內部結構類。

在 Java 中,廣泛意義上的內部類一般來說包括這四種:成員內部類、局部內部類、匿名內部類和靜態內部類。下面就先來了解一下這四種內部類的用法。

成員內部類

成員內部類是最普通的內部類,它的定義為位於另一個類的內部,具體使用如下:

class Circle {
    double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}

這樣看起來,類 Draw 像是類 Circle 的一個成員, Circle 稱為外部類。成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括 private 成員和靜態成員),例如:

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
            // 外部類的靜態成員
            System.out.println(count);
        }
    }
}

不過要注意的是,當成員內部類擁有和外部類同名的成員變量或者方法時,會發生隱藏現象,即默認情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要採取以下形式進行訪問:

外部類.this.成員變量
外部類.this.成員方法

雖然成員內部類可以無條件地訪問外部類的成員,而外部類想訪問成員內部類的成員卻不是這麼隨心所欲了。在外部類中如果要訪問成員內部類的成員,必須先創建一個成員內部類的對象,再通過指向這個對象的引用來訪問,其具體形式為:

class Circle {
    private double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
        // 必須先創建成員內部類的對象,再進行訪問
        getDrawInstance().drawSahpe();
    }

    private Draw getDrawInstance() {
        return new Draw();
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
        }
    }
}

成員內部類是依附外部類而存在的,也就是說,如果要創建成員內部類的對象,前提是必須存在一個外部類的對象。創建成員內部類對象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        // 第一種方式
        Outter outter = new Outter();
        // 必須通過Outter對象來創建
        Outter.Inner inner = outter.new Inner();

        // 第二種方式
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}

class Outter {
    private Inner inner = null;
    public Outter() {
    }

    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }

    class Inner {
        public Inner() {
        }
    }
}

內部類可以擁有 private 訪問權限、 protected 訪問權限、 public 訪問權限及包訪問權限。

比如上面的例子,如果成員內部類 Inner 用 private 修飾,則只能在外部類的內部訪問;如果用 public 修飾,則任何地方都能訪問;如果用 protected 修飾,則只能在同一個包下或者繼承外部類的情況下訪問;如果是默認訪問權限,則只能在同一個包下訪問。

這一點和外部類有一點不一樣,外部類只能被 public 和包訪問兩種權限修飾。

我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種權限修飾。

局部內部類

局部內部類是定義在一個方法或者一個作用域裏面的類,它和成員內部類的區別在於局部內部類的訪問僅限於方法內或者該作用域內。

class People{
    public People() {
    }
}

class Man{
    public Man(){
    }

    public People getWoman(){
        /**
         * 局部內部類
         */
        class Woman extends People{
            int age =0;
        }
        return new Woman();
    }
}

注意,局部內部類就像是方法裏面的一個局部變量一樣,是不能用 public 、 protected 、 private 以及 static 修飾的。

匿名內部類

匿名內部類應該是平時我們編寫代碼時用得最多的,比如創建一個線程的時候:

class Test {

    public static void main(String[] args) {
        Thread thread = new Thread(
                // 匿名內部類
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Thread run");
                    }
                }
        );
    }
}

同樣的,匿名內部類也是不能有訪問修飾符和 static 修飾符的。

匿名內部類是唯一一種沒有構造器的類。正因為其沒有構造器,所以匿名內部類的使用範圍非常有限,大部分匿名內部類用於接口回調。

匿名內部類在編譯的時候由系統自動起名為Outter$1.class。一般來說,匿名內部類用於繼承其他類或是實現接口,並不需要增加額外的方法,只是對繼承方法的實現或是重寫。

靜態內部類

靜態內部類也是定義在另一個類裏面的類,只不過在類的前面多了一個關鍵字 static 。

靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非 static 成員變量或者方法,這點很好理解,因為在沒有外部類的對象的情況下,可以創建靜態內部類的對象,如果允許訪問外部類的非 static 成員就會產生矛盾,因為外部類的非 static 成員必須依附於具體的對象。

例如:

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {
    }

    /**
     * 靜態
     */
    static class Inner {
        public Inner() {
        }
    }
}

深入理解內部類

通過上面的介紹,相比你已經大致了解的內部類的使用,那麼你的心裏想必會有一個疑惑:

為什麼成員內部類可以無條件訪問外部類的成員?

首先我們先定義一個內部類:

public class Outter {
    private Inner inner = null;

    public Outter() {
    }

    public Inner getInnerInstance() {
        if (inner == null)
            inner = new Inner();
        return inner;
    }

    protected class Inner {
        public Inner() {
        }
    }
}

先用 javac 進行編譯,你可以發現會生成兩個文件: Outter$Inner.class 和 Outter.class 。接下來利用javap -p反編譯 Outter$Inner.class ,其結果如下:

Classfile /D:/project/Test/src/test/java/test/Outter$Inner.class
  Last modified 2019-11-25; size 408 bytes
  MD5 checksum b936e37bc77059b83951429e28f3f225
  Compiled from "Outter.java"
public class Outter$Inner
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#13         // test/Outter$Inner.this$0:Ltest/Outter;
   #2 = Methodref          #4.#14         // java/lang/Object."<init>":()V
   #3 = Class              #16            // test/Outter$Inner
   #4 = Class              #19            // java/lang/Object
   #5 = Utf8               this$0
   #6 = Utf8               Ltest/Outter;
   #7 = Utf8               <init>
   #8 = Utf8               (Ltest/Outter;)V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               Outter.java
  #13 = NameAndType        #5:#6          // this$0:Ltest/Outter;
  #14 = NameAndType        #7:#20         // "<init>":()V
  #15 = Class              #21            // test/Outter
  #16 = Utf8               test/Outter$Inner
  #17 = Utf8               Inner
  #18 = Utf8               InnerClasses
  #19 = Utf8               java/lang/Object
  #20 = Utf8               ()V
  #21 = Utf8               test/Outter
{
  final Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public Outter$Inner(Outter);
    descriptor: (Ltest/Outter;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 16: 0
        line 17: 9
}
SourceFile: "Outter.java"
InnerClasses:
     protected #17= #3 of #15; //Inner=class test/Outter$Inner of class test/Outter

32行的內容為:final Outter this$0;

學過 C 的朋友應該能知道,這是一個指向外部類 Outter 對象的指針,也就是說編譯器會默認為成員內部類添加一個指向外部類對象的引用,這樣也就解釋了為什麼成員內部類能夠無條件訪問外部類了。

那麼這個引用是如何賦初值的呢?下面接着看內部類的構造器:public Outter$Inner(Outter);

從這裏可以看出,雖然我們在定義的內部類的構造器是無參構造器,但編譯器還是會默認添加一個參數,該參數的類型為指向外部類對象的一個引用,所以成員內部類中的 Outter this&0 指針便指向了外部類對象,因此可以在成員內部類中隨意訪問外部類的成員。

從這裏也間接說明了成員內部類是依賴於外部類的,如果沒有創建外部類的對象,則無法對 Outter this&0 引用進行初始化賦值,也就無法創建成員內部類的對象了。

為什麼局部內部類和匿名內部類只能訪問局部final變量?

我們還是採用和之前一樣的解答方式,先定義一個類:

public class Outter {

    public static void main(String[] args)  {
        Outter outter = new Outter();
        int b = 10;
        outter.test(b);
    }

    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

通過 javac 編譯 Outter,也會生成兩個文件: Outter.class 和 Outter1.class。默認情況下,編譯器會為匿名內部類和局部內部類起名為 Outter$x.class( x 為正整數)。

根據我提供的類,可以思考一個問題:

當 test 方法執行完畢之後,變量 a 的生命周期就結束了,而此時 Thread 對象的生命周期很可能還沒有結束,那麼在 Thread 的 run 方法中繼續訪問變量 a 就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?

Java 採用了複製的手段來解決這個問題。將 Outter$1.class 反編譯可以得到下面的內容:

Classfile /D:/project/Test/src/test/java/test/Outter$1.class
  Last modified 2019-11-25; size 653 bytes
  MD5 checksum 2e238dafbd73356eba22d473c6469082
  Compiled from "Outter.java"
class test.Outter$1 extends java.lang.Thread
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #6.#23         // test/Outter$1.this$0:Ltest/Outter;
   #2 = Fieldref           #6.#24         // test/Outter$1.val$b:I
   #3 = Methodref          #7.#25         // java/lang/Thread."<init>":()V
   #4 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(I)V
   #6 = Class              #30            // test/Outter$1
   #7 = Class              #32            // java/lang/Thread
   #8 = Utf8               val$b
   #9 = Utf8               I
  #10 = Utf8               this$0
  #11 = Utf8               Ltest/Outter;
  #12 = Utf8               <init>
  #13 = Utf8               (Ltest/Outter;I)V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               run
  #17 = Utf8               ()V
  #18 = Utf8               SourceFile
  #19 = Utf8               Outter.java
  #20 = Utf8               EnclosingMethod
  #21 = Class              #33            // test/Outter
  #22 = NameAndType        #34:#35        // test:(I)V
  #23 = NameAndType        #10:#11        // this$0:Ltest/Outter;
  #24 = NameAndType        #8:#9          // val$b:I
  #25 = NameAndType        #12:#17        // "<init>":()V
  #26 = Class              #36            // java/lang/System
  #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #28 = Class              #39            // java/io/PrintStream
  #29 = NameAndType        #40:#35        // println:(I)V
  #30 = Utf8               test/Outter$1
  #31 = Utf8               InnerClasses
  #32 = Utf8               java/lang/Thread
  #33 = Utf8               test/Outter
  #34 = Utf8               test
  #35 = Utf8               (I)V
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
{
  final int val$b;
    descriptor: I
    flags: ACC_FINAL, ACC_SYNTHETIC

  final test.Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  test.Outter$1(test.Outter, int);
    descriptor: (Ltest/Outter;I)V
    flags:
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: iload_2
         7: putfield      #2                  // Field val$b:I
        10: aload_0
        11: invokespecial #3                  // Method java/lang/Thread."<init>":()V
        14: return
      LineNumberTable:
        line 10: 0

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: getfield      #2                  // Field val$b:I
        15: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        18: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 18
}
SourceFile: "Outter.java"
EnclosingMethod: #21.#22                // test.Outter.test
InnerClasses:
     #6; //class test/Outter$1

我們看到在 run 方法中有一條指令:bipush 10

這條指令表示將操作數10壓棧,表示使用的是一個本地局部變量。

這個過程是在編譯期間由編譯器默認進行,如果這個變量的值在編譯期間可以確定,則編譯器默認會在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的字節碼嵌入到執行字節碼中。

這樣一來,匿名內部類使用的變量是另一個局部變量,只不過值和方法中局部變量的值相等,因此和方法中的局部變量完全獨立開。

接下來也來看一下 test.Outter$1 的構造方法:test.Outter$1(test.Outter, int);

我們看到匿名內部類 Outter$1 的構造器含有兩個參數,一個是指向外部類對象的引用,一個是 int 型變量,很顯然,這裡是將變量 test 方法中的形參 b 以參數的形式傳進來對匿名內部類中的拷貝(變量 b 的拷貝)進行賦值初始化。

也就說如果局部變量的值在編譯期間就可以確定,則直接在匿名內部裏面創建一個拷貝。如果局部變量的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。

從上面可以看出,在 run 方法中訪問的變量 b 根本就不是test方法中的局部變量 b 。這樣一來就解決了前面所說的 生命周期不一致的問題。但是新的問題又來了,既然在 run 方法中訪問的變量 b 和test方法中的變量 b 不是同一個變量,那麼當在 run 方法中改變變量 b 的值的話,會出現什麼情況?

會造成數據不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題, Java 編譯器就限定必須將變量 b 限製為 final ,不允許對變量 b 進行更改(對於引用類型的變量,是不允許指向新的對象),這樣數據不一致性的問題就得以解決了。

到這裏,想必大家應該清楚為何 方法中的局部變量和形參都必須用 final 進行限定了。

靜態內部類有特殊的地方嗎?

從前面可以知道,靜態內部類是不依賴於外部類的,也就說可以在不創建外部類對象的情況下創建內部類的對象。

另外,靜態內部類是不持有指向外部類對象的引用的,這個讀者可以自己嘗試反編譯 class 文件看一下就知道了,是沒有 Outter this&0 引用的。

總結

今天介紹了內部類相關的知識,包括其一般的用法以及內部類和外部類的依賴關係,通過對字節碼進行反編譯詳細了解了其實現模式,最後留給大家一個任務自己去實際探索一下靜態內部類的實現。希望通過這篇介紹可以幫大家更加深刻了解內部類。

有興趣的話可以訪問我的博客或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

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

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

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

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

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

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

08 決策樹與隨機森林

08 決策樹與隨機森林

決策樹之信息論基礎

認識決策樹

  1. 來源: 決策樹的思想來源非常樸素,程序設計中的條件分支結構就是if – then 結構,最早的決策樹就是利用這類結構分割數據的一種分類學習方法。

  2. 舉例:是否見相親對象

信息的度量和作用

  1. 克勞德 .艾爾伍德 .香農:信息論創始人,密西根大學學士,麻省理工學院博士。 1948年發表了划時代論文 – 通信的數學原理,奠定了現代信息論的基礎。
  2. 信息的單位: 比特 (bit)

  3. 舉例: 以32支球隊爭奪世界杯冠軍
  • 如果不知道任何球隊的信息,每支球隊得冠概率相等。
    以二分法預測,最少需要使用5次才能預測到準確結果。 5 = log32 (以2為底)
    5 = -(1/32log1/32 + 1/32log1/32 + ……)

  • 開放一些信息,則小於5bit, 如1/6 德國,1/6 巴西, 1/10 中國
    5 > -(1/6log1/4 + 1/6log1/4 + ….)

  1. 信息熵:
  • “誰是世界杯冠軍”的信息量應該比5 bit少, 它的準確信息量應該是:
  • H = -(p1logp1 + p2logp2 + p3logp3 +……p32logp32 ) Pi 為第i支球隊獲勝的概率
  • H 的專業術語就是信息熵,單位為比特

決策樹的劃分以及案例

信息增益

  1. 定義: 特徵A對訓練數據集D的信息增益g(D,A), 定義為集合D的信息熵H(D)與特徵A給定條件下D的信息條件熵H(D|A) 之差,即:
    g(D,A) = H(D) – H(D | A)
    注: 信息增益表示得知特徵 X 的信息而使得類 Y的信息的不確定性減少的程度。

  2. 以不同特徵下的信貸成功率為例

  • H(D) = -(9/15log(9/15) + 6/15log(6/15)) = 0.971 # 以類別進行判斷,只有是否兩種類別
  • gD,年紀) = H(D) – H(D’|年紀) = 0.971 – [1/3H(青年)+ 1/3H(中年)+ 1/3H(老年)] # 三種年紀對應的目標值均佔1/3
    – H(青年) = -(2/5log(2/5) + 3/5log(3/5)) # 青年類型中,類別的目標值特徵為(2/5, 3/5)
    – H(中年) = -(2/5log(2/5) + 3/5log(3/5))
    – H(老年) = -(4/5log(2/5) + 1/5log(3/5))

令A1, A2, A3, A4 分別表示年齡,有工作,有房子和信貸情況4個特徵,則對應的信息增益為:
g(D,A1) = H(D) – H(D|A1)
其中,g(D,A2) = 0.324 , g(D,A3) = 0.420 , g(D,A4) = 0.363
相比而言,A3特徵(有房子)的信息增益最大,為最有用特徵。
所以決策樹的實際劃分為:

常見決策樹使用的算法

  1. ID3
  • 信息增益,最大原則
  1. C4.5
  • 信息增益比最大原則 (信息增益占原始信息量的比值)
  1. CART
  • 回歸樹: 平方誤差最小
  • 分類樹: 基尼係數最小原則 (劃分的細緻),sklearn默認的劃分原則

Sklearn決策樹API

  1. sklearn.tree.DecisionTreeClassifier(criterion=’gini’, max_depth=None, random_state=None)
  • criterion (標準): 默認基尼係數,也可以選用信息增益的熵‘entropy’
  • max_depth: 樹的深度大小
  • random_state: 隨機數種子
  1. 決策樹結構
    sklearn.tree.export_graphviz() 導出DOT文件格式
  • estimator: 估算器
  • out_file = “tree.dot” 導出路徑
  • feature_name = [,] 決策樹特徵名

決策樹預測泰坦尼克號案例

import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_graphviz

"""
泰坦尼克數據描述事故后乘客的生存狀態,該數據集包括了很多自建旅客名單,提取的數據集中的特徵包括:
票的類別,存貨,等級,年齡,登錄,目的地,房間,票,船,性別。
乘坐等級(1,2,3)是社會經濟階層的代表,其中age數據存在缺失。
"""


def decision():
    """
    決策樹對泰坦尼克號進行預測生死
    :return: None
    """
    # 1.獲取數據
    titan = pd.read_csv('./titanic_train.csv')

    # 2.處理數據,找出特徵值和目標值
    x = titan[['Pclass', 'Age', 'Sex']]
    y = titan[['Survived']]
    # print(x)

    # 缺失值處理 (使用平均值填充)
    x['Age'].fillna(x['Age'].mean(), inplace=True)
    print(x)
    # 3.分割數據集到訓練集和測試集
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25)

    # 4. 進行處理(特徵工程) 特徵,類別 --> one_hot編碼
    dict = DictVectorizer(sparse=False)
    x_train = dict.fit_transform(x_train.to_dict(orient='records'))
    print(dict.get_feature_names())
    x_test = dict.transform(x_test.to_dict(orient='records'))  # 默認一行一行轉換成字典
    print(x_train)

    # 5. 用決策樹進行預測
    dec = DecisionTreeClassifier()
    dec.fit(x_train, y_train)

    # 預測準確率
    print("預測的準確率:", dec.score(x_test, y_test))

    # 導出決策樹
    export_graphviz(dec, out_file='./tree.dot', feature_names=['Pclass', 'Age', 'Sex'])
    return None


if __name__ == '__main__':
    decision()

隨機森林

集成學習方法

  1. 定義:集成學習通過建立幾個模型組合,來解決單一預測問題。其工作原理是生成多個分類器 / 模型,各組獨立地學習和作出預測。這些預測最後結合成單預測,因此優於任何一個單分類的租出預測。

隨機森林

  1. 定義:在機器學習中,隨機森林是一個包含多個決策樹的分類器,並且其輸出的類別是由個別樹輸出的類別的眾數而定。
    例如: 訓練了5棵樹,其中4棵樹的結果是True, 1棵樹為False, 那麼最終的結果就是True. (投票)

  2. 問題: 如果每棵樹使用相同的特徵,相同的分類器,參數也相同,建立的每棵樹不就是相同的么?

隨機森林建立多個決策樹的過程:

單個樹的建立:(N個樣本,M個特徵)

  1. 隨機在N個樣本中選擇一個樣本,重複N次, 樣本有可能重複
  2. 隨機在M個特徵當中選出m個特徵 m << M
  3. 建立10棵決策樹,樣本,特徵大多不一樣 隨機有放回的抽樣 (bootstrap抽樣)

為什麼要隨機抽樣訓練集?

如果不隨機抽樣,每棵樹的訓練集都一樣,那麼最終訓練處的樹分類結果也是完全一樣的

為什麼要有放回的抽樣?

如果不是有放回的抽樣,那麼每棵樹的訓練樣本都是不同的,都是沒有交集的,這樣的每棵樹都是“有偏的”,“片面的”。即,每棵樹訓練出來都是有很大的差異,而隨機森鈴最後分類取決於多棵樹(弱分類器)的投票表決。

隨機森林 API

  • 分類器:sklearn.ensemble.RandomForestClassifier
    • n_estimators:integer(整數),option, default=10 (森林里數目的數量)
    • criteria: string (default =’gini’) 分割特徵的測量方法
    • max_depth 樹的最大深度
    • max_feature = ‘auto’ 每個決策樹的最大特徵數量
    • bootstrap: default = True 是否放回抽樣

隨機森林的優點

  1. 在當前的所有算法中,具有極好的準確率
  2. 能有有效地運行在大數據集上 (樣本,特徵)
  3. 能夠處理具有高維特徵的輸入樣本,不需要降維
  4. 能夠評估各個特徵在分類問題上的重要性

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

羞,Spring Bean 初始化/銷毀竟然有這麼多姿勢

文章來源:

一、前言

日常開發過程有時需要在應用啟動之後加載某些資源,或者在應用關閉之前釋放資源。Spring 框架提供相關功能,圍繞 Spring Bean 生命周期,可以在 Bean 創建過程初始化資源,以及銷毀 Bean 過程釋放資源。Spring 提供多種不同的方式初始化/銷毀 Bean,如果同時使用這幾種方式,Spring 如何處理這幾者之間的順序?

有沒有覺得標題很熟悉,沒錯標題模仿二哥 「@沉默王二」 文章。

二、姿勢剖析

首先我們先來回顧一下 Spring 初始化/銷毀 Bean 幾種方式,分別為:

  • init-method/destroy-method
  • InitializingBean/DisposableBean
  • @PostConstruct/@PreDestroy
  • ContextStartedEvent/ContextClosedEvent

PS: 其實還有一種方式,就是繼承 Spring Lifecycle 接口。不過這種方式比較繁瑣,這裏就不再分析。

2.1、init-method/destroy-method

這種方式在配置文件文件指定初始化/銷毀方法。XML 配置如下

<bean id="demoService" class="com.dubbo.example.provider.DemoServiceImpl"  destroy-method="close"  init-method="initMethod"/>

或者也可以使用註解方式配置:

@Configurable
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public HelloService hello() {
        return new HelloService();
    }
}

還記得剛開始接觸學習 Spring 框架,使用就是這種方式。

2.2、InitializingBean/DisposableBean

這種方式需要繼承 Spring 接口 InitializingBean/DisposableBean,其中 InitializingBean 用於初始化動作,而 DisposableBean 用於銷毀之前清理動作。使用方式如下:

@Service
public class HelloService implements InitializingBean, DisposableBean {
    
    @Override
    public void destroy() throws Exception {
        System.out.println("hello destroy...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello init....");
    }
}

2.3、@PostConstruct/@PreDestroy

這種方式相對於上面兩種方式來說,使用方式最簡單,只需要在相應的方法上使用註解即可。使用方式如下:

@Service
public class HelloService {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }
}

這裏踩過一個坑,如果使用 JDK9 之後版本 ,@PostConstruct/@PreDestroy 需要使用 maven 單獨引入 javax.annotation-api,否者註解不會生效。

2.4、ContextStartedEvent/ContextClosedEvent

這種方式使用 Spring 事件機制,日常業務開發比較少見,常用與框架集成中。Spring 啟動之後將會發送 ContextStartedEvent 事件,而關閉之前將會發送 ContextClosedEvent 事件。我們需要繼承 Spring ApplicationListener 才能監聽以上兩種事件。

@Service
public class HelloListener implements ApplicationListener {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof ContextClosedEvent){
            System.out.println("hello ContextClosedEvent");
        }else if(event instanceof ContextStartedEvent){
            System.out.println("hello ContextStartedEvent");
        }

    }
}

也可以使用 @EventListener註解,使用方式如下:

public class HelloListenerV2 {
    
    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("hello ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }
}

PS:只有調用 ApplicationContext#start 才會發送 ContextStartedEvent。若不想這麼麻煩,可以監聽 ContextRefreshedEvent 事件代替。一旦 Spring 容器初始化完成,就會發送 ContextRefreshedEvent

三、綜合使用

回顧完上面幾種方式,這裏我們綜合使用上面的四種方式,來看下 Spring 內部的處理順序。在看結果之前,各位讀者大人可以猜測下這幾種方式的執行順序。

public class HelloService implements InitializingBean, DisposableBean {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("bye DisposableBean...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello InitializingBean....");
    }

    public void xmlinit(){
        System.out.println("hello xml-init...");
    }

    public void xmlDestory(){
        System.out.println("bye xmlDestory...");
    }

    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("bye ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }

}

xml 配置方式如下:

    <context:annotation-config />
    <context:component-scan base-package="com.dubbo.example.demo"/>
    
    <bean class="com.dubbo.example.demo.HelloService" init-method="xmlinit" destroy-method="xmlDestory"/>

應用啟動方法如下:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
context.close();

程序輸出結果如下所示:

最後採用圖示說明總結以上結果:

四、源碼解析

不知道各位讀者有沒有猜對這幾種方式的執行順序,下面我們就從源碼角度解析 Spring 內部處理的順序。

4.1、初始化過程

使用 ClassPathXmlApplicationContext 啟動 Spring 容器,將會調用 refresh 方法初始化容器。初始化過程將會創建 Bean 。最後當一切準備完畢,將會發送 ContextRefreshedEvent。當容器初始化完畢,調用 context.start() 就發送 ContextStartedEvent 事件。

refresh 方法源碼如下:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
            //... 忽略無關代碼

            // 初始化所有非延遲初始化的 Bean
            finishBeanFactoryInitialization(beanFactory);

            // 發送 ContextRefreshedEvent
            finishRefresh();

            //... 忽略無關代碼
    }
}

一路跟蹤 finishBeanFactoryInitialization 源碼,直到 AbstractAutowireCapableBeanFactory#initializeBean,源碼如下:

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 調用 BeanPostProcessor#postProcessBeforeInitialization 方法
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
        // 初始化 Bean
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
}

BeanPostProcessor 將會起着攔截器的作用,一旦 Bean 符合條件,將會執行一些處理。這裏帶有 @PostConstruct 註解的 Bean 都將會被 CommonAnnotationBeanPostProcessor 類攔截,內部將會觸發 @PostConstruct 標註的方法。

接着執行 invokeInitMethods ,方法如下:

protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
        throws Throwable {

    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        // 省略無關代碼
        // 如果是 Bean 繼承 InitializingBean,將會執行  afterPropertiesSet 方法
        ((InitializingBean) bean).afterPropertiesSet();
    }

    if (mbd != null) {
        String initMethodName = mbd.getInitMethodName();
        if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 執行 XML 定義 init-method
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

如果 Bean 繼承 InitializingBean 接口,將會執行 afterPropertiesSet 方法,另外如果在 XML 中指定了 init-method ,也將會觸發。

上面源碼其實都是圍繞着 Bean 創建的過程,當所有 Bean 創建完成之後,調用 context#start 將會發送 ContextStartedEvent 。這裏源碼比較簡單,如下:

public void start() {
    getLifecycleProcessor().start();
    publishEvent(new ContextStartedEvent(this));
}

4.2、銷毀過程

調用 ClassPathXmlApplicationContext#close 方法將會關閉容器,具體邏輯將會在 doClose 方法執行。

doClose 這個方法首先發送 ContextClosedEvent,然再后開始銷毀 Bean

靈魂拷問:如果我們顛倒上面兩者順序,結果會一樣嗎?

doClose 源碼如下:

protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        // 省略無關代碼

        try {
            // Publish shutdown event.
            publishEvent(new ContextClosedEvent(this));
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }


        // 銷毀 Bean
        destroyBeans();

        // 省略無關代碼
    }
}

destroyBeans 最終將會執行 DisposableBeanAdapter#destroy@PreDestroyDisposableBeandestroy-method 三者定義的方法都將會在內部被執行。

首先執行 DestructionAwareBeanPostProcessor#postProcessBeforeDestruction,這裏方法類似與上面 BeanPostProcessor

@PreDestroy 註解將會被 CommonAnnotationBeanPostProcessor 攔截,這裏類同時也繼承了 DestructionAwareBeanPostProcessor

最後如果 BeanDisposableBean 的子類,將會執行 destroy 方法,如果在 xml 定義了 destroy-method 方法,該方法也會被執行。

public void destroy() {
    if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
        for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
            processor.postProcessBeforeDestruction(this.bean, this.beanName);
        }
    }

    if (this.invokeDisposableBean) {
        // 省略無關代碼
        // 如果 Bean 繼承 DisposableBean,執行 destroy 方法
        ((DisposableBean) bean).destroy();
        
    }

    if (this.destroyMethod != null) {
        // 執行 xml 指定的  destroy-method 方法
        invokeCustomDestroyMethod(this.destroyMethod);
    }
    else if (this.destroyMethodName != null) {
        Method methodToCall = determineDestroyMethod();
        if (methodToCall != null) {
            invokeCustomDestroyMethod(methodToCall);
        }
    }
}

五、總結

init-method/destroy-method 這種方式需要使用 XML 配置文件或單獨註解配置類,相對來說比較繁瑣。而InitializingBean/DisposableBean 這種方式需要單獨繼承 Spring 的接口實現相關方法。@PostConstruct/@PreDestroy 這種註解方式使用方式簡單,代碼清晰,比較推薦使用這種方式。

另外 ContextStartedEvent/ContextClosedEvent 這種方式比較適合在一些集成框架使用,比如 Dubbo 2.6.X 優雅停機就是用改機制。

六、Spring 歷史文章推薦

歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:

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

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Elasticsearch從入門到放棄:文檔CRUD要牢記

在Elasticsearch中,文檔(document)是所有可搜索數據的最小單位。它被序列化成JSON存儲在Elasticsearch中。每個文檔都會有一個唯一ID,這個ID你可以自己指定或者交給Elasticsearch自動生成。

如果延續我們之前不恰當的對比RDMS的話,我認為文檔可以類比成關係型數據庫中的表。

元數據

前面我們提到,每個文檔都有一個唯一ID來標識,獲取文檔時,“_id”字段記錄的就是文檔的唯一ID,它是元數據之一。當然,文檔還有一些其他的元數據,下面我們來一一介紹

  • _index:文檔所屬的索引名
  • _type:文檔所屬的type
  • _id:文檔的唯一ID

有了這三個,我們就可以唯一確定一個document了,當然,7.0版本以後我們已經不需要_type了。接下來我們再來看看其他的一些元數據

  • _source:文檔的原始JSON數據
  • _field_names:該字段用於索引文檔中值不為null的字段名,主要用於exists請求查找指定字段是否為空
  • _ignore:這個字段用於索引和存儲文檔中每個由於異常(開啟了ignore_malformed)而被忽略的字段的名稱
  • _meta:該字段用於存儲一些自定義的元數據信息
  • _routing:用來指定數據落在哪個分片上,默認值是Id
  • _version:文檔的版本信息
  • _score:相關性打分

創建文檔

創建文檔有以下4種方法:

  • PUT /<index>/_doc/<_id>
  • POST /<index>/_doc/
  • PUT /<index>/_create/<_id>
  • POST /<index>/_create/<_id>

這四種方法的區別是,如果不指定id,則Elasticsearch會自動生成一個id。如果使用_create的方法,則必須保證文檔不存在,而使用_doc方法的話,既可以創建新的文檔,也可以更新已存在的文檔。

在創建文檔時,還可以選擇一些參數。

請求參數

  • if_seq_no:當文檔的序列號是指定值時才更新
  • if_primary_term:當文檔的primary term是指定值時才更新
  • op_type:如果設置為create則指定id的文檔必須不存在,否則操作失敗。有效值為index或create,默認為index
  • op_type:指定預處理的管道id
  • refresh:如果設置為true,則立即刷新受影響的分片。如果是wait_for,則會等到刷新分片后,此次操作才對搜索可見。如果是false,則不會刷新分片。默認值為false
  • routing:指定路由到的主分片
  • timeout:指定響應時間,默認是30秒
  • master_timeout:連接主節點的響應時長,默認是30秒
  • version:顯式的指定版本號
  • version_type:指定版本號類型:internal、 external、external_gte、force
  • wait_for_active_shards:處理操作之前,必須保持活躍的分片副本數量,可以設置為all或者任意正整數。默認是1,即只需要主分片活躍。

響應包體

  • **_shards**:提供分片的信息
  • **_shards.total**:創建了文檔的總分片數量
  • **_shards.successful**:成功創建文檔分片的數量
  • **_shards.failed**:創建文檔失敗的分片數量
  • **_index**:文檔所屬索引
  • **_type**:文檔所屬type,目前只支持_doc
  • **_id**:文檔的id
  • **_version**:文檔的版本號
  • **_seq_no**:文檔的序列號
  • **_primary_term**:文檔的主要術語
  • result:索引的結果,created或者updated

我們在創建文檔時,如果指定的索引不存在,則ES會自動為我們創建索引。這一操作是可以通過設置中的action.auto_create_index字段來控制的,默認是true。你可以修改這個字段,實現指定某些索引可以自動創建或者所有索引都不能自動創建的目的。

更新文檔

了解了如何創建文檔之後,我們再來看看應該如何更新一個已經存在的文檔。其實在創建文檔時我們就提到過,使用PUT /<index>/_doc/<id>的方法就可以更新一個已存在的文檔。除此之外,我們還有另一種更新文檔的方法:

POST /<index>/_update/<_id>

這兩種更新有所不同。_doc方法是先刪除原有的文檔,再創建新的。而_update方法則是增量更新,它的更新過程是先檢索到文檔,然後運行指定腳本,最後重新索引。

還有一個區別就是_update方法支持使用腳本更新,默認的語言是painless,你可以通過參數lang來進行設置。在請求參數方面,_update相較於_doc多了以下幾個參數:

  • lang:指定腳本語言
  • retry_on_conflict:發生衝突時重試次數,默認是0
  • **_source**:設置為false,則不返回任何檢索字段
  • **_source_excludes**:指定要從檢索結果排除的source字段
  • **_source_includes**:指定要返回的檢索source字段

下面的一個例子是用腳本來更新文檔

curl -X POST "localhost:9200/test/_update/1?pretty" -H 'Content-Type: application/json' -d'
{
    "script" : {
        "source": "ctx._source.counter += params.count",
        "lang": "painless",
        "params" : {
            "count" : 4
        }
    }
}
'

Upsert

curl -X POST "localhost:9200/test/_update/1?pretty" -H 'Content-Type: application/json' -d'
{
    "script" : {
        "source": "ctx._source.counter += params.count",
        "lang": "painless",
        "params" : {
            "count" : 4
        }
    },
    "upsert" : {
        "counter" : 1
    }
}
'

當指定的文檔不存在時,可以使用upsert參數,創建一個新的文檔,而當指定的文檔存在時,該請求會執行script中的腳本。如果不想使用腳本,而只想新增/更新文檔的話,可以使用doc_as_upsert。

curl -X POST "localhost:9200/test/_update/1?pretty" -H 'Content-Type: application/json' -d'
{
    "doc" : {
        "name" : "new_name"
    },
    "doc_as_upsert" : true
}
'

update by query

這個API是用於批量更新檢索出的文檔的,具體可以通過一個例子來了解。

curl -X POST "localhost:9200/twitter/_update_by_query?pretty" -H 'Content-Type: application/json' -d'
{
  "script": {
    "source": "ctx._source.likes++",
    "lang": "painless"
  },
  "query": {
    "term": {
      "user": "kimchy"
    }
  }
}
'

獲取文檔

ES獲取文檔用的是GET API,請求的格式是:

GET /<index>/_doc/<_id>

它會返迴文檔的數據和一些元數據,如果你只想要文檔的內容而不需要元數據時,可以使用

GET /<index>/_source/<_id>

請求參數

獲取文檔的有幾個請求參數之前已經提到過,這裏不再贅述,它們分別是:

  • refresh
  • routing
  • **_source**
  • **_source_excludes**
  • **_source_includes**
  • version
  • version_type

而還有一些之前沒提到過的參數,我們來具體看一下

  • preference:用來 指定執行請求的node或shard,如果設置為_local,則會優先在本地的分片執行
  • realtime:如果設置為true,則請求是實時的而不是近實時。默認是true
  • stored_fields:返回指定的字段中,store為true的字段

mget

mget是批量獲取的方法之一,請求的格式有兩種:

  • GET /_mget
  • GET /<index>/_mget

第一種是在請求體中寫index。第二種是把index放到url中,不過這種方式可能會觸發ES的安全檢查。

mget的請求參數和get相同,只是需要在請求體中指定doc的相關檢索條件

request

GET /_mget
{
    "docs" : [
        {
            "_index" : "jackey",
            "_id" : "1"
        },
        {
            "_index" : "jackey",
            "_id" : "2"
        }
    ]
}

response

{
  "docs" : [
    {
      "_index" : "jackey",
      "_type" : "_doc",
      "_id" : "1",
      "_version" : 5,
      "_seq_no" : 6,
      "_primary_term" : 1,
      "found" : true,
      "_source" : {
        "user" : "ja",
        "tool" : "ES",
        "message" : "qwer"
      }
    },
    {
      "_index" : "jackey",
      "_type" : "_doc",
      "_id" : "2",
      "_version" : 1,
      "_seq_no" : 2,
      "_primary_term" : 1,
      "found" : true,
      "_source" : {
        "user" : "zhe",
        "post_date" : "2019-11-15T14:12:12",
        "message" : "learning Elasticsearch"
      }
    }
  ]
}

刪除文檔

CURD操作只剩下最後一個D了,下面我們就一起來看看ES中如何刪除一個文檔。

刪除指定id使用的請求是

DELETE /<index>/_doc/<_id>

在併發量比較大的情況下,我們在刪除時通常會指定版本,以確定刪除的文檔是我們真正想要刪除的文檔。刪除請求的參數我們在之前也都介紹過,想要具體了解的同學可以直接查看。

delete by query

類似於update,delete也有一個delete by query的API。

POST /<index>/_delete_by_query

它也是要先按照條件來查詢匹配的文檔,然後刪除這些文檔。在執行查詢之前,Elasticsearch會先為指定索引做一個快照,如果在執行刪除過程中,要索引發生改變,則會導致操作衝突,同時返回刪除失敗。

如果刪除的文檔比較多,也可以使這個請求異步執行,只需要設置wait_for_completion=false即可。

這個API的refresh與delete API的refresh參數有所不同,delete中的refresh參數是設置操作是否立即可見,即只刷新一個分片,而這個API中的refresh參數則是需要刷新受影響的所有分片。

Bulk API

最後,我們再來介紹一種特殊的API,批量操作的API。它支持兩種寫法,可以將索引名寫到url中,也可以寫到請求體中。

  • POST /_bulk

  • POST /<index>/_bulk

在這個請求中,你可以任意使用之前的CRUD請求的組合。

curl -X POST "localhost:9200/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
'

請求體中使用的語法是newline delimited JSON(NDJSON)。具體怎麼用呢?其實我們在上面的例子中已經有所展現了,對於index或create這樣的請求,如果請求本身是有包體的,那麼用換行符來表示下面的內容與子請求分隔,即為包體的開始。

例如上面例子中的index請求,它的包體就是{ “field1” : “value1” },所以它會在index請求的下一行出現。

對於批量執行操作來說,單條操作失敗並不會影響其他操作,而最終每條操作的結果也都會返回。

上面的例子執行完之後,我們得到的結果應該是

{
   "took": 30,
   "errors": false,
   "items": [
      {
         "index": {
            "_index": "test",
            "_type": "_doc",
            "_id": "1",
            "_version": 1,
            "result": "created",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "status": 201,
            "_seq_no" : 0,
            "_primary_term": 1
         }
      },
      {
         "delete": {
            "_index": "test",
            "_type": "_doc",
            "_id": "2",
            "_version": 1,
            "result": "not_found",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "status": 404,
            "_seq_no" : 1,
            "_primary_term" : 2
         }
      },
      {
         "create": {
            "_index": "test",
            "_type": "_doc",
            "_id": "3",
            "_version": 1,
            "result": "created",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "status": 201,
            "_seq_no" : 2,
            "_primary_term" : 3
         }
      },
      {
         "update": {
            "_index": "test",
            "_type": "_doc",
            "_id": "1",
            "_version": 2,
            "result": "updated",
            "_shards": {
                "total": 2,
                "successful": 1,
                "failed": 0
            },
            "status": 200,
            "_seq_no" : 3,
            "_primary_term" : 4
         }
      }
   ]
}

批量操作的執行過程相比多次單個操作而言,在性能上會有一定的提升。但同時也會有一定的風險,所以我們在使用的時候要非常的謹慎。

總結

本文我們先介紹了文檔的基本概念和文檔的元數據。接着又介紹了文檔的CRUD操作和Bulk API。相信看完文章你對Elasticsearch的文檔也會有一定的了解。那最後就請你啟動你的Elasticsearch,然後親自動手試一試這些操作,看看各種請求的參數究竟有什麼作用。相信親手實驗過一遍之後你會對這些API有更深的印象。

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

設計模式之代理模式

什麼是代理模式

代理模式就是為一個對象提供一個代理對象,由這個代理對象控制對該對象的訪問。

理解代理模式,可以對照生活中的一些具體例子,比如房產中介、二手車交易市場、經紀人等。

為什麼要用代理模式

通過使用代理模式,我們避免了直接訪問目標對象時可能帶來的一些問題,比如:遠程調用,需要使用遠程代理來幫我們處理一些網絡傳輸相關的細節邏輯;可能需要基於某種權限控制對目標資源的訪問,可以使用保護代理等。

總的來說,通過是用代理模式,我們可以控制對目標對象的訪問,可以在真實方法被調用前或調用后,通過代理對象加入額外的處理邏輯。

代理模式分類

代理模式分為靜態代理和動態代理。動態代理根據實現不同又可細分為JDK動態代理和cglib動態代理。

靜態代理是由程序員創建或工具生成代理類的源碼,再編譯代理類。所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和委託類的關係在運行前就確定了。

動態代理是在實現階段不用關心代理類,而在運行時動態生成代理類的。

靜態代理

以房哥買房子為例,用代碼實現靜態代理。

1、首先建立一個Seller接口

public interface Seller {
    void sell();
}

2、創建實現類,房哥,有一個方法,就是買房子

public class FangGe implements Seller{
    @Override
    public void sell() {
        System.out.println("房哥要出手一套四合院");
    }
}

3、買房子需要找到買家,達成交易后還要辦理過戶等其他手續,房哥只想賣房收錢就完了。因此,需要找一個代理來幫房哥處理這些雜事。

我們創建一個代理類FangGeProxy,代理類也需要實現Seller接口,行為上要保持和FangGe一樣,都是要賣房子。同時該代理類還需要持有房哥的引用。

public class FangGeProxy implements Seller{
    private FangGe fangGe;

    public FangGeProxy(FangGe fangGe){
        this.fangGe = fangGe;
    }
    @Override
    public void sell() {
        findBuyer();
        fangGe.sell();
        afterSell();
    }
    
    public void findBuyer(){
        System.out.println("代理幫助尋找買主");
    }
    
    public void afterSell(){
        System.out.println("達成交易后,辦理相關手續");
    }
}

可以看到,房哥的代理類通過findBuyer()和afterSell()兩個方法幫助房哥完成了其他一些雜事。

4、測試類

public class StaticProxyTest {
    public static void main(String[] args) {
        Seller seller = new FangGeProxy(new FangGe());
        seller.sell();
    }
}

輸出:

代理幫助尋找買主
房哥要出手一套四合院
達成交易后,辦理相關手續

最後,看下類圖

靜態代理的問題:

1、由於靜態代理類在編譯前已經確定了代理的對象,因此靜態代理只能代理一種類型的類,如果要給大量的類做代理,就需要編寫大量的代理類;

2、如果我們要給Seller,也就是目標對象要增加一些方法,則需要同步修改代理類,不符合開閉原則。

JDK動態代理

JDK的動態代理依賴於jdk給我們提供的類庫實現,是一種基於接口實現的動態代理,在編譯時並不知道要代理哪個類,而是在運行時動態生成代理類。同時也解決了靜態代理中存在的問題。

我們接上上面靜態代理的例子,繼續實現JDK的動態代理。

1、我們建一個方法轉發的處理器類,該類需要實現InvocationHandler接口。

public class SellerInvocationHandler implements InvocationHandler {

    // 要代理的真實對象
    private Object target;

    /**
     * 使用Proxy類靜態方法獲取代理類實例
     */
    public Object getProxyInstance(Object target){
        this.target = target;
        Class<?> clazz = target.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object obj = method.invoke(this.target, args);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法前");
    }
    
    private void after() {
        System.out.println("執行方法后");
    }
}

2、新建JDK動態代理測試類,首先代理房哥賣房子

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        
        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();

    }
}

輸出:

執行方法前
房哥要出手一套四合院
執行方法后

可以看到,完成了代理。

3、接下來我們新建另外一個類,User類,並使用JDK動態代理完成代理User類

public interface IUser {
    void sayHello();

    void work();
}

public class UserImpl implements IUser{
    @Override
    public void sayHello() {
        System.out.println("hello,我是小明");
    }

    @Override
    public void work() {
        System.out.println("我正在寫代碼");
    }
}

修改測試類,

public class JDKDynamicProxyTest {
    public static void main(String[] args) {

/*        // new一個房哥,下面幫房哥找個代理
        FangGe fangGe = new FangGe();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();

        // 房哥的代理對象
        Seller seller = (Seller) sellerInvocationHandler.getProxyInstance(fangGe);
        seller.sell();*/

        // 代理user類
        IUser user = new UserImpl();
        SellerInvocationHandler sellerInvocationHandler = new SellerInvocationHandler();
        IUser userProxy = (IUser) sellerInvocationHandler.getProxyInstance(user);
        userProxy.sayHello();
        userProxy.work();

    }
}

輸出:

執行方法前
hello,我是小明
執行方法后
執行方法前
我正在寫代碼
執行方法后

可以看到,我們SellerInvocationHandler 並未做任何改動,它便能為UserImpl類生成代理,並在執行方法的前後增加額外的執行邏輯。

cglib動態代理

JDK動態代理有一個局限就是,被代理的類必須要實現接口。如果被代理的類沒有實現接口,則JDK動態代理就無能為力了。這個時候該cglib動態代理上場了。

CGLIB是一個功能強大,高性能的代碼生成包。它為沒有實現接口的類提供代理,為JDK的動態代理提供了很好的補充。通常可以使用Java的動態代理創建代理,但當要代理的類沒有實現接口或者為了更好的性能,CGLIB是一個好的選擇。

1、新建一個MyCglibInterceptor,實現MethodInterceptor接口。該類類似於JDK動態代理中的InvocationHandler實例,是實現cglib動態代理的主要類。

public class MyCglibInterceptor implements MethodInterceptor {

    public Object getCglibProxyInstance(Object object){
        // 相當於Proxy,創建代理的工具類
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(object.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o, objects);
        after();
        return obj;
    }

    private void before() {
        System.out.println("執行方法之前");
    }

    private void after() {
        System.out.println("執行方法之後");
    }
}

2、新建cglib動態代理的測試類,先代理上面例子中的User類。

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();
    }
}

輸出:

執行方法之前
hello,我是小明
執行方法之後
執行方法之前
我正在寫代碼
執行方法之後

3、新建一個類HelloWorld,不實現任何接口,為該類實現動態代理。

public class HelloWorld {
    public void hello(){
        System.out.println("世界這麼大,我想去看看");
    }
}

測試代理類

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
/*        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        IUser userCglibProxy = (IUser) myCglibInterceptor.getCglibProxyInstance(new UserImpl());
        userCglibProxy.sayHello();
        userCglibProxy.work();*/

        // 代理未實現任何接口的類
        MyCglibInterceptor myCglibInterceptor = new MyCglibInterceptor();
        HelloWorld helloWorldProxy = (HelloWorld) myCglibInterceptor.getCglibProxyInstance(new HelloWorld());
        helloWorldProxy.hello();
    }
}

輸出:

執行方法之前
世界這麼大,我想去看看
執行方法之後

使用cglib動態代理,我們實現了對普通類的代理。

(完)

設計模式系列文章

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

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

Java併發之volatile關鍵字

引言

說到多線程,我覺得我們最重要的是要理解一個臨界區概念。

舉個例子,一個班上1個女孩子(臨界區),49個男孩子(線程),男孩子的目標就是這一個女孩子,就是會有競爭關係(線程安全問題)。推廣到實際場景,例如對一個數相加或者相減等等情形,因為操作對象就只有一個,在多線程環境下,就會產生線程安全問題。理解臨界區概念,我們對多線程問題可以有一個好意識。

Jav內存模型(JMM)

談到多線程就應該了解一下Java內存模型(JMM)的抽象示意圖.下圖:

線程A和線程B執行的是時候,會去讀取共享變量(臨界區),然後各自拷貝一份回到自己的本地內存,執行後續操作。
JMM模型是一種規範,就像Java的接口一樣。JMM會涉及到三個問題:原子性,可見性,有序性。
所謂原子性。就是說一個線程的執行會不會被其他線程影響的。他是不可中斷的。舉個例子:

int i=1

這個語句在Jmm中就是原子性的。無論是一個線程執行還是多個線程執行這個語句,讀出來的i就是等於1。那什麼是非原子性呢,按道理如果Java的代碼都是原子性,應該就不會有線程問題了啊。其實JMM這是規定某些語句是原子性罷了。舉個非原子性例子:

i ++;

這個操作就不是原子性的了。因為他就是包含了三個操作:第一讀取i的值,第二將i加上1,第三將結果賦值回來給i,更新i的值。
所謂可見性。可見性表示如果一個值在線程A修改了,線程B就會馬上知道這個結果。
所謂有序性。所謂有序性值的是語意的有序性。就是說代碼順序可能會發生變化。因為有一個指令重排機制。所謂指令重排,他會改變代碼執行順序,為了讓cpu執行效率更高。為了防止重排序出錯,JMM有個happen-before規則,這個規則限制了那些語句執行在前,那些語句執行在後。
Happen-before:
程序順序原則:一個線程內保證語義的串行性
volatile原則:volatile變量的寫發生在讀之前
鎖規則:先加鎖再解鎖
傳遞性:a先於b,b先於c,則a必定先於c
線程的start方法先於他的每一個操作
線程所有的操作先於線程的終結
對象的構造函數執行、結束先於finalize()方法。

volatile

進入正題,volatile可以保證變量(臨界區)的可見性以及有序性,但是不能保證原子性。舉個例子:

public class VolatileTest implements Runnable{
    private static VolatileTest volatileTest = new VolatileTest();
    private  static volatile int i= 0;
    public static void main(String[] args) throws InterruptedException {
        for (int j = 0; j < 20; j++) {
            Thread a = new Thread(new VolatileTest());
            Thread b = new Thread(new VolatileTest());
            a.start();b.start();
            a.join();b.join();
            System.out.print(i+"&&");
        }

    }
    
    @Override
    public void run() {
        for (int j = 0; j < 1000; j++) {
            i++;
        }
    }

}

// 輸出結果
// 2000&&4000&&5852&&7852&&9852&&11852&&13655&&15655&&17655&&19655&&21306     
//&&22566&&24566&&26189&&28189&&30189&&32189&&34189&&36189&&38089&&

有結果看到有問題,雖然i已經添加了volatile關鍵字,說明volatile關鍵字不能保證i++的原子性。

那什麼場景適合使用volatile關鍵字

  1. 輕量級的“讀-寫鎖”策略
private volatile int value;
public int getValue(){ return value;}
public synchronized void doubleValue(){ value = value*value; }

2.單例模式(雙檢查鎖機制

private volatile static Singleton instace;   
public static Singleton getInstance(){  // 沒有使用同步方法,而是同步方法塊
    //第一次null檢查 ,利用volatile的線程間可見性,不需要加鎖,性能提高    
    if(instance == null){            
        synchronized(Singleton.class) {    //鎖住類對象,阻塞其他線程
            //第二次null檢查,以保證不會創建重複的實例       
            if(instance == null){       
                instance = new Singleton(); // 禁止重排序
            }  
        }           
    }  
    return instance;

參考

《現代操作系統(第三版)中文版》
《實戰Java高併發程序設計》
《Java併發編程的藝術》

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

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

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

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

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

EF Core For MySql查詢中使用DateTime.Now作為查詢條件的一個小問題

背景

最近一直忙於手上澳洲線上項目的整體遷移和升級的準備工作,導致博客和公眾號停更。本周終於艱難的完成了任務,藉此機會,總結一下項目中遇到的一些問題。

EF Core一直是我們團隊中中小型項目常用的ORM框架,在使用SQL Server作為持久化倉儲的場景一下,一直表現還中規中矩。但是在本次項目中,項目使用了MySql作為持久化倉儲。為了與EF Core集成,團隊使用了Pomelo.EntityFrameworkCore.MySql作為EF Core For MySql的擴展。在開發過程中,團隊遇到了各種各樣在SQL Server場景下沒有遇到過的問題,其中最奇怪的,也是隱藏最深的問題,就是將DateTime.Now作為查詢條件,產生了非預期的結果。

問題場景

本周在項目升級的過程中,客戶反饋了一個問題。

在當前系統的Dashboard頁面,有一個消息提醒功能,客戶可以自定義一些消息,並且指定提醒的日期。客戶遇到的問題是通常添加的消息提醒,在指定日期的上午時間段是不會显示,只有在下午時間段才能看到,比如說客戶指定2019年10月26號看到一個的消息提醒,但是在10月26日這天早上8:00-12:00這個時間段,系統總是看不到提醒,只有到了下午的時間段才能看到提醒。

PS:這裏客戶表達的只是個籠統的問題,但問題確實是上午的大部分時間是看不到消息提醒的,但並不是精確到中午12:00點這個時間, 所以此處不必過於糾結於具體的時間。

查看問題代碼

看到這個問題的時候,我自己也很奇怪,難道代碼或者數據庫使用了時區,導致查詢出現了偏差?

於是我就Review了一下此處的查詢, 代碼如下。

var query = DbContext.CRM_Note_Reminders
    .Include(x => x.CRM_Note)
    .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= DateTime.Now.Date)
     .ToList();

PS: 這裏可能有同學會有疑問,為啥不用DbFunctions.DiffDays? 原因是DbFunctions.DiffDays是 EF Core for SQLServer的擴展方法,針對MySql還沒有官方的實現方案。

從這個查詢中,我沒有看出任何問題,於是我直接藉助一些日誌工具,將EF Core生成的查詢語句的輸出了出來。

其中WHERE條件部分如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE))
AND (CONVERT(`x`.`Reminder_Date`, date) 
  <= CONVERT(CURRENT_TIMESTAMP(), date)))

這裏CURRENT_TIMESTAMP()是MySql的內置函數,與SQLServer的內置函數GETDATE()不同,CURRENT_TIMESTAMP()默認返回的是UTC時間。因此我們大概能知道,為什麼澳洲客戶會遇到上面的場景了。

PS: 根據7樓兄弟的反饋,我試了一下,改動Mysql的時區配置之後,果然CURRENT_TIMESTAMP()就改為了對應時區的時間。這裏使用UTC時間的原因應該是我在AWS RDS上創建Mysql實例的時候,忽略了時區配置。

由於澳洲處於東10區,與UTC時間有+10個小時的時差,所以當澳洲上午的10點之前,UTC時間都是在當前澳洲日期的前一天,所以系統中出現了當天的消息提醒在上午時間段不能正常显示的問題。

PS: 由於澳洲是分冬令時和夏令時的,夏令時時間要加一個小時,所以實際上客戶在每天的11點之前都無法看到正確的消息提醒。

深入思考

你這可能會非常奇怪,為什麼DateTime.Now會被轉化成內置函數CURRENT_TIMESTAMP(),而沒有使用我們傳入的值DateTime.Now.Date呢?

其實EF/EF Core在查詢是時候是分2個階段的,一個是組合查詢表達式樹的階段,一個是真正的查詢階段。

在組合查詢表達式樹的階段,EF/EF Core只會去組合表達式,而不會去嘗試計算表達式的值,所以這個階段DateTime.Now.Date的值並沒有被計算出來, 在進入正常查詢階段的時候, EF/EF Core會嘗試將查詢表達式樹翻譯成SQL腳本,這時候由於我們的EF ProviderMySql Provider, 恰巧DateTime.Now可以翻譯成Mysql的內置函數CURRENT_TIMESTAMP(), 所以這裏EF/EF Core就跳過了表達式值的計算,直接將其翻譯成了對應的內置函數,所以導致生成的SQL查詢和我們的預期有偏差。

那麼我們該如何解決這個問題呢?

解決方案

經過了以上的思考,其實解決這個問題也就很簡單了,我們可以將DateTime.Now.Date先計算出來,保存在一個變量中,然後將這個變量傳入查詢中。

var today = DateTime.Now.Date;

var query = DbContext.CRM_Note_Reminders
     .Include(x => x.CRM_Note)
     .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= today)
     .ToList();

由此生成的MySQL腳本如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE)) 
AND (CONVERT(`x`.`Reminder_Date`, date) <= @__date_0)) 

這樣我們就得到了一個正確的結果,澳洲客戶也就收到了正確的消息。

是不是有種差之毫厘,謬以千里的感覺呢?

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!