Appium+python自動化(九)- 定位元素工具(義結金蘭的兩位異性兄弟)(超詳解)

簡介

   環境搭建好了,其他方面的知識也準備的差不多了,那麼我們就開始下一步元素定位,元素定位宏哥主要介紹如何使用uiautomatorviewer,通過定位到頁面上的元素,然後進行相應的點擊等操作. 此外在介紹另一款工具:Inspector。

uiautomatorviewer是android-sdk自帶的一個元素定位工具,非常簡單好用,使用uiautomatorviewer,你可以檢查一個應用的UI來查看應用的布局和組件以及相關的屬性。

  那麼宏哥先來給各位小夥伴們介紹大哥–uiautomatorviewer,再來介紹小弟–Inspector。

一、大哥–uiautomatorviewer

  大哥人物簡介:

  大哥是皇族後裔,但是比劉皇叔強多了。人家有一個好爹–Google。

  Android 4.1發布的,uiautomator是用來做UI測試的。也就是普通的手工測試,點擊每個控件元素 看看輸出的結果是否符合預期。比如 登陸界面 分別輸入正確和錯誤的用戶名密碼然後點擊登陸按鈕看看是否能否登陸以及是否有錯誤提示等。

功能性或者黑盒UI測試不需要測試人員了解程序如何實現的,只需要驗證各種操作的結果是否符合預期即可。這樣的測試可以分離團隊的開發人員和測試人員。大家各干各的沒有太多的交集。

  常用的UI測試方式就是人工驗證啦,就是測試人員拿着各種手機分別安裝要測試的程序然後看看是否能正確完成各種預定的功能。但是這種驗證方式是非常耗時間的,每次回歸都要全部驗證一邊,並且還容易出現人為錯誤。比較高效和可靠的UI測試方式就是自動化測試。自動化UI測試創建測試代碼來執行測試任務,各種測試任務分別覆蓋不同的使用場景,然後使用測試框架來運行這些測試任務。而uiautomator 就是你的自動化UI測試工具。

Android SDK在4.1中提供了如下工具來支持UI自動化測試:

  • uiautomatorviewer – 一個圖形界面工具來掃描和分析應用的UI控件。
  • uiautomator – 一個測試的Java庫,包含了創建UI測試的各種API和執行自動化測試的引擎。

要大哥出山也就是使用該工具,需要滿足如下條件:

  • Android SDK Tools, Revision 21 or higher
  • Android SDK Platform, API 16 or higher

一、啟動uiautomatorviewer.bat

兩種啟動方法:

第一種方法:

1、打開目錄D:\software\android-sdk-windows\tools

 

2、雙擊啟動,啟動之後出現如下界面

 


 
第二種方法:

1、如果不喜歡雙擊啟動的話,也可以在cmd裏面通過指令啟動

先cd到tools目錄下,然後輸入uiautomatorviewer.bat回車后啟動服務

二、連接手機

1、cmd打開輸入adb devices,確認手機已連上(以夜神模擬器為例)

 

2、打開手機淘寶頁面,讓屏幕處於點亮狀態

 

 

3、點左上角安卓機器人按鈕Devices Screenshot按鈕刷新頁面

 

 

三、定位元素

1、移動鼠標到需要定位的元素上,如搜索輸入框

 
 

2、右下角可以看到元素對應的屬性

text:好沙發需要好坐墊

resource-id:com.taobao.taobao:id/home_searchedit

class:android.widget.EditText

四、點搜索框

1、前面一篇啟動app后,休眠五秒,等待頁面加載完成

2、通過id來定位到搜索框,然後點擊

 

 

五、參考代碼

# coding=utf-8
# 1.先設置編碼,utf-8可支持中英文,如上,一般放在第一行

# 2.註釋:包括記錄創建時間,創建人,項目名稱。
'''
Created on 2019-6-24
@author: 北京-宏哥   QQ交流群:707699217
Project:學習和使用定位元素工具
'''
# 3.導入模塊
from appium import webdriver
import time
desired_caps = {}
desired_caps['platformName'] = 'Android'   #android的apk還是IOS的ipa
desired_caps['platformVersion'] = '8.0'  #android系統的版本號
desired_caps['deviceName'] = '127.0.0.1:62001'    #手機設備名稱,通過adb devices  查看
desired_caps['appPackage'] = 'com.taobao.taobao'  #apk的包名
desired_caps['appActivity'] = 'com.taobao.tao.welcome.Welcome'  #apk的launcherActivity
#desired_caps['unicodeKeyboard'] = True   #使用unicodeKeyboard的編碼方式來發送字符串
#desired_caps['resetKeyboard'] = True   #將鍵盤給隱藏起來
driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps) #啟動服務器地址,後面跟的是手機信息
# 休眠五秒等待頁面加載完成
time.sleep(5)

driver.find_element_by_id("com.taobao.taobao:id/home_searchedit").click()

# driver.quit()

 

六、元素定位

1、appium的webdriver提供了11種元素定位方法,在selenium的基礎上擴展了三個,可以在pycharm裏面輸入driver.find_element_by然後會自動匹配出來

 

2、多的三種如下,在後面的會詳細介紹

driver.find_element_by_accessibility_id()

driver.find_element_by_android_uiautomator()

driver.find_element_by_ios_uiautomation()

(第三個是ios的可以暫時不用管)

二、小弟–Inspector

   小弟人物簡介:

  小弟不是很牛叉,但是小弟的存在必定有小弟存在的必要,之前看了好多文章都要放棄小弟,我覺地不要放棄,沒準小弟可以替大哥大哥擋刀呢,呵呵,說笑了,畢竟過河的卒子可以當車!!!

  Appium Inspector是appium自帶的一個元素定位工具,前邊介紹了如何使用uiautomatorviewer進行元素定位。這裏就學習Appium Inspector是如何定位的。

  Appium Inspector 對於測試的同學不得不說是一個神器,可以查詢到我們需要定位的元素的 id 等各種信息 ,還可以 錄製用戶行為,翻譯成腳本。逆天的是可以翻譯成多種語言的腳本。這並不意味着測試的同學可以不用學習語言和編程了,因為機器翻譯出的腳本一般很難一次執行成功,其中很可能還需要經過人工修改后才能順利執行,但是這已經可以為測試腳本的編寫帶來很大的遍歷,所以 Appium Inspector 還是值得我們去學習和使用。

一、設置appium

1、先不要啟動appium,因為啟動以後就不能操作appium設置界面,必須在未啟動的時候進行操作

2、點開android setting界面(機器人圖標)

3、勾選Application Path,添加被測app的路徑

4、Devices name處添加設備名稱(adb devices查看到的)

 

(注意:通過這種方法也可查看到apk的包名和Launch Activity)

 

 

二、開啟appium

1、手機確保連接電腦(adb devices查看)(仍然以夜神模擬器為例)

 

 

2、點appium右上角三角形圖標,會啟動服務

 

 

3、這時候可以看到手機上安裝淘寶應用,並會啟動淘寶

4、點appium右上角的搜索圖標

 

 

5、點Inspector Window界面的 Refresh 按鈕刷新界面

 

 

三、Ispector Window

1、手機上打開需要單位元素的界面,然後點Refresh按鈕刷新

2、左邊菜單樹,可以挨個點開

3、如果想單位界面上的“是”和“否”按鈕,從菜單樹就可以看到這兩個元素的結構

四、查看屬性

1、選中左側菜單樹對應的元素,在右下角查看對應屬性

 

 

五、常見異常

1、在使用過程中,你會發現經常會報以下這個錯誤

2、每次啟動都會給你手機上重新安裝一次應用

3、並且不能用鼠標指定某個元素,沒有uiautomatorviewer使用方便

(要是以上三種問題你都遇到了,怎麼辦???那就放棄吧!!!)​

相信很多人在使用過程中,都會遇到中文無法輸入問題,宏哥在後續文章里會手把手教大家如何輸入中文。

三、小結

Android SDK中的UIAutomator中本身是不支持網頁中的UI元素定位,下面介紹幾種常用的定位app內部的網頁的UI元素的方法。

一、使用chrome瀏覽器調試移動端網頁

       這是使用最多的一種方法。首頁確保自己的手機已經跟電腦連接且處於開發者模式。

       打開PC端的谷歌瀏覽器,輸入chrome://inspect/#devices,會看到下圖所示界面:

      

     點擊需要測試的網頁下方的inspect,且保證手機界面停留在那個頁面。

      

 

      這樣打開后是不是就熟悉了,點擊上方紅色標記的就可以操作進行定位了~~~

 

 二、Android手機的鋪助功能帶有TalkBack

        之前一直用的是谷歌的方法,但是最近發現使用talkback可以直接用UIAutomator定位元素。  

        Android系統一般自帶一個TalkeBack功能。但這裏千萬要注意:打開之後整個系統的操作都變得不同了!!滑動界面需要兩個手指,單擊變成雙擊。打開之後用UiAutomatorViewer獲取webview界面的內容,你就會發現原來webview裏面獲取不到的ui元素,現在已經可以可以獲取到了。即使現在你關掉TalkBack,也能獲取到,除非重啟手機才會回到不能獲取的狀態,所以開啟后我們可以立即關閉,以方便操作。有些手機沒有talkBack的可以去應用市場下載。

       功能路徑:
       原生系統:設置 – 輔助功能(Accessibility) – TalkBack
       華為EMUI系統:設置 – 高級設置 – 輔助功能 – TalkBack

三、宏哥在小弟哪裡遇到的問題

 

查看appium日誌的error發現沒有設備名字報錯

 

將夜神模擬器的名字加上就成功了

宏哥不能保證所整理都符合大家的口味,但我能保證每一篇都是用心去寫和用心去整理,我始終認同“分享的越多,你的價值增值越大”,歡迎大家關注我的博客和個人公眾號的技術分享。在分享中進步,越努力越幸運,期待我們都有美好的明天!

支持宏哥的朋友們和宏哥的宏粉記得點波推薦哦,您的肯定就是我進步的動力。鄙人先在這裏給您道謝了,謝您嘞~~

個人公眾號(因為許多文章都被搬到別人的公眾號里了,還是原創,所以宏哥果斷也開一個公眾號。打算與博客園文章同步,希望大家隨時隨地學習與進步):

微信群(因為有人給我發短消息說公司不讓用QQ,就幫忙建立一個微信群,歡迎加入討論和交流)

 

我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2k3h2stkvscgc

【精選推薦文章】

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

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

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

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

大量文件名記錄的樹形結構存儲

十多年來,NAS中已經存在的目錄和文件達到10億之多,在設計和開發備份系統的過程中碰到了很多挑戰,本文將分享大量文件名記錄的樹形結構存儲實踐。

一、引言

既然是定期備份,肯定會有1次以上的備份。對於一個特定目錄,每次備份時都要與上次備份時進行比較,以期找出哪些文件被刪除了,又新增了哪些文件,這就需要每次備份時把該目錄下的所有文件名進行保存。我們首先想到的是把所有文件名用特定字符進行拼接后保存。由於我們使用了MySQL保存這些信息,當目錄下文件很多時,這種拼接的方式很可能超出MySQL的Blob長度限制。根據經驗,當一個目錄有大量文件時,這些文件的名稱往往是程序生成的,有一定規律的,而且開頭一般是重複的,於是我們想到了使用一種樹形結構來進行存儲。

例如,一個有abc、abc1、ad、cde 4個文件的目錄對應的樹如圖1所示。

圖1 樹形結構示例

圖1中,R表示根節點,青色節點我們稱為結束節點,從R到每個結束節點的路徑都表示一個文件名。可以在樹中查找是否含有某個文件名、遍歷樹中所有的文件名、對樹序列化進行保存、由序列化結果反序列化重新生成樹。

二、涉及的數據結構

注意:我們使用java編寫,文中涉及語言特性相關的知識點都是指java。

2.1 Node的結構

包括根節點在內的每個節點都使用Node類來表示。代碼如下:

 class Node {
        private char value;
        private Node[]children = new Node[0];
        private byte end = 0;
    }

 

字段說明:

  • value:該節點表示的字符,當Node表示根節點時,value無值。
  • children:該節點的所有子節點,初始化為長度為0的數組。
  • end:標記節點是否是結束節點。0不是;1是。恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子節點肯定是結束節點。默認非結束節點。

2.2 Node的操作

   public Node(char v);
    public Node findChild(char v);
    public Node addChild(char v);

 

操作說明:

  • Node:構造方法。將參數v賦值給this.value。
  • findChild:查找children中是否含有value為v的子節點。有則返回子節點,沒有則返回null。
  • addChild:首先查找children中是否已經含有value為v的子節點,如果有則直接將查到的子節點返回;否則創建value為v的節點,將children的長度延長1,將新創建的節點作為children的最後一個元素,並返回新創建的節點。

2.3 Tree的結構

  class Tree {
        public Node root = new Node();
    }

 

字段說明:Tree只含有root Node。如前所述,root的value無值,end為0。初始時的children長度為0。

2.4 Tree的操作

  public void addName(String name) ;
    public boolean contain(String name);
    public Found next(Found found);
    public void writeTo(OutputStream out);
    public static Tree readFrom(InputStream in);

 

操作說明:

  • addName:向樹中增加一個新的文件名,即參數name。以root為起點,name中的每個字符作參數調用addChild,返回值又作為新的起點,直到name中的全部字符添加完畢,對最後一次調用addChild的返回值標記為結束節點。
  • contain:查詢樹中是否含有一個文件名。
  • next:對樹中包含的所有文件名進行遍歷,為了使遍歷能夠順利進行,我們引入了新的類Found,細節會在後文詳述。
  • writeTo:將樹寫入一個輸出流以進行持久化。
  • readFrom:此方法是靜態方法。從一個輸入流來重新構建樹。

三、樹的構建

在新建的Tree上調用addName方法,將所有文件名添加到樹中,樹構建完成。仍然以含有abc、abc1、ad、cde 四個文件的目錄為例,對樹的構建進行圖示。

圖2 樹的構建過程

圖2中,橙色節點表示需要在該節點上調用addChild方法增加子節點,同時addChild的返回值作為新的橙色節點。直到沒有子節點需要增加時,把最後的橙色節點標記為結束節點。

四、樹的查詢

查找樹中是否含有一個某個文件名,對應Tree的contain方法。在圖2中的結果上分別查找ef、ab和abc三個文件來演示查找的過程。如圖3所示。

圖3 樹的查詢示意圖

圖3中,橙色節點表示需要在該節點上調用findChild方法查找子節點。

五、樹的遍歷

此處的遍歷不同於一般樹的遍歷。一般遍歷是遍歷樹中的節點,而此處的遍歷是遍歷根節點到所有結束節點的路徑。

我們採用從左到右、由淺及深的順序進行遍歷。我們引入了Found類,並作為next方法的參數進行遍歷。

5.1 Found的結構

 class Found {    
        private String name;
        private int[] idx ;
    }

 

為了更加容易的說明問題,在圖1基礎上進行了小小的改造,每個節點的右下角增加了下標,如圖4。

圖4 帶下標的Tree

對於abc這個文件名,Found中的name值為“abc”,idx為{0,0,0}。

對於abc1這個文件名,Found中的name值為“abc1”,idx為{0,0,0,0}。

對於ad這個文件名,Found中的name值為“ad”,idx為{0,1}。

對於cde這個文件名,Found中的name值為“cde”,idx為{1,0,0}。

5.2 如何遍歷

對於圖4而言,第一次調用next方法應傳入null,則返回第一個結果,即abc代表的Found;繼續以這個Found作為參數進行第二次next的調用,則返回第二個結果,即abc1代表的Found;再繼續以這個Found作為參數進行第三次next的調用,則返回第三個結果,即ad所代表的Found;再繼續以這個Found作為參數進行第四次next的調用,則返回第四個結果,即cde所代表的Found;再繼續以這個Found作為參數進行第五次調用,則返回null,遍歷結束。

六、序列化與反序列化

6.1 序列化

首先應該明確每個節點序列化后應該包含3個信息:節點的value、節點的children數量和節點是否為結束節點。

6.1.1 節點的value

雖然之前所舉的例子中節點的value都是英文字符,但實際上文件名中可能含有漢字或者其他語言的字符。為了方便處理,我們沒有使用變長編碼。而是直接使用unicode碼。字節序採用大端編碼。

6.1.2 節點的children數量

由於節點的value使用了unicode碼,所以children的數量不會多於unicode能表示的字符的數量,即65536。children數量使用2個字節。字節序同樣採用大端編碼。

6.1.3 節點的end

0或1可以使用1位(1bit)來表示,但java中最小單位是字節。如果採用1個字節來表示end,有些浪費空間,其實任何一個節點children數量達到65536/2的可能性都是極小的,因此我們考慮借用children數量的最高位來表示end。

綜上所述,一個節點序列化后佔用4個字節,以圖4中的根節點、value為b的節點和value為e的節點為例:

表1 Node序列化示例

  value的unicode children數量 end children數量/(end<<15) 最終結果
根節點 0x0000 2 0 0x0002 0x00020000
b節點 0x0062 1 0 0x0001 0x00010062
e節點 0x0065 0 1 0x8000 0x80000065

6.1.4 樹的序列化過程

對樹進行廣度遍歷,在遍歷過程中需要藉助隊列,以圖4的序列化為例進行說明: 

圖5 對圖4的序列化過程

6.2 反序列化

反序列化是序列化的逆過程,由於篇幅原因不再進行闡述。值得一提的是,反序列化過程同樣需要隊列的協助。

七、討論

7.1 關於節省空間

為方便討論,假設目錄下的文件名是10個阿拉伯数字的全排列,當位數為1時,目錄下含有10個文件,即0、1、2……8、9,當位數為2時,目錄下含有100個文件,即00、01、02……97、98、99,以此類推。

比較2種方法,一種使用“/”分隔,另一種是本文介紹的方法。

表2 2種方法的存儲空間比較(單位:字節)

位數 方法 1 2 3 4 5 6
“/”分隔 19 299 3999 49999 599999 6999999
Tree 44 444 4444 44444 444444 4444444

由表2可見,當位數為4時,使用Tree的方式開始節省空間,位數越多節省的比例越高,這正是我們所需要的。

表中,使用“/”分隔時,字節數佔用是按照utf8編碼計算的。如果直接使用unicode進行存儲,佔用空間會加倍,那麼會在位數為2時就開始節省空間。同樣使用“/”分隔,看起來utf8比使用unicode會更省空間,但實際上,文件名中有時候會含有漢字,漢字的utf8編碼佔用3個字節。

7.2 關於時間

在樹的構建、序列化反序列化過程中,引入了額外的運算,根據我們的實踐,user CPU並沒有明顯變化。

7.3 關於理想化假設

最初我們就是使用了“/”分隔的方法對文件名進行存儲,並且數據庫的相應字段類型是Blob(Blob的最大值是65K)。在測試階段就發現,超出65K是一件很平常的事情。在不可能預先統計最大目錄里所有文件名拼接后的大小的情況下,我們採取了2種手段,一是使用LongBlob類型,另一種就是盡量減小拼接結果的大小,即本文介紹的方法。

即使使用樹形結構來存儲文件名,也不能夠保證最終結果不超出4G(LongBlob類型的最大值),至少在我們實踐的過程並未出現問題,如果真出現這種情況,只能做特殊處理了。

7.4 關於其他壓縮方法

把文件名使用“/”拼接后,使用gzip等壓縮算法對拼接結果進行壓縮后再存儲,在節省存儲空間方面會取得更好的效果。但是在壓縮之前,拼接結果存在於內存,這樣對JVM的堆內存有比較高的要求;另外,使用“/”拼接時,查找會比較麻煩。

作者:牛寧昌

來源:宜信技術學院

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

實際項目中,看 ECharts 和 HighCharts 渲染性能對比,表面看衣裝,本質看內功!!!

     最近做項目,使用的是echarts显示圖表數據,但是數據量比較多的時候,有卡頓的情況。後來同事拿echarts和HighCharts做了對比,僅供大家參考。同時感謝同事做的工作。

一、查詢1天的源數據,屬性1、屬性2、屬性3、屬性4

       Echarts查詢3.61s,渲染0.786s(約8.6M數據)

       HighCharts查詢3.10s,渲染0.768s(約8.7M數據)

二、查詢7天的源數據,屬性1、屬性2、屬性3、屬性4

      Echarts查詢21.67秒,渲染5.38秒。(約60.7M數據)。渲染完成后

      HighCharts查詢22.18s,渲染5.39s。(約60.5M數據)

三、查詢14天的源數據,屬性1、屬性2、屬性3、屬性4

      Echarts渲染不出來,瀏覽器崩潰。

      HighCharts查詢41.42s,渲染10.82s。(約121M數據)

四、內存佔用對比:7天數據

      Echarts渲染后瀏覽器佔用內存約3280M。

      HighCharts渲染后瀏覽器佔用內存約637M。

五、操作對比

圖表類型

1天數據

7天數據

14天數據

Echarts

卡頓

無法操作

瀏覽器崩潰

HighCharts

流暢

流暢

流暢

 

雲端操作系統:http://www.ineuos.net

【精選推薦文章】

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

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

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

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

SpringBoot啟動流程分析(二):SpringApplication的run方法

SpringBoot系列文章簡介

SpringBoot源碼閱讀輔助篇:

  Spring IoC容器與應用上下文的設計與實現

SpringBoot啟動流程源碼分析:

  1. SpringBoot啟動流程分析(一):SpringApplication類初始化過程
  2. SpringBoot啟動流程分析(二):SpringApplication的run方法
  3. SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
  4. SpringBoot啟動流程分析(四):IoC容器的初始化過程
  5. SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
  6. SpringBoot啟動流程分析(六):IoC容器依賴注入

筆者註釋版Spring Framework與SpringBoot源碼git傳送門:請不要吝嗇小星星

  1. spring-framework-5.0.8.RELEASE
  2. SpringBoot-2.0.4.RELEASE

一、前言

  前一篇博客介紹了 SpringApplication 類的實例化過程,本章總結SpringBoot啟動流程最重要的部分run方法。通過rrun方法梳理出SpringBoot啟動的流程,然後後面的博客再一步步的分析啟動流程中各個步驟所做的具體的工作。深入分析後會發現SpringBoot也就是給Spring包了一層皮,事先替我們準備好Spring所需要的環境及一些基礎,具體通過源碼一步步深入分析後會發現Spring是真的很偉大。當然跟代碼的時候越深入越容易陷進去進而發現有些東西沒法通過博客詳細的梳理出來。當然在這個過程中還是立足於我們對SpringBoot的使用來說明源碼所做的工作。知其然才能知其所以然。加油

 

二、SpringBoot啟動流程梳理

  首先擺上run方法的源碼

 1 /**
 2  * Run the Spring application, creating and refreshing a new
 3  * {@link ApplicationContext}.
 4  *
 5  * @param args the application arguments (usually passed from a Java main method)
 6  * @return a running {@link ApplicationContext}
 7  *
 8  * 運行spring應用,並刷新一個新的 ApplicationContext(Spring的上下文)
 9  * ConfigurableApplicationContext 是 ApplicationContext 接口的子接口。在 ApplicationContext
10  * 基礎上增加了配置上下文的工具。 ConfigurableApplicationContext是容器的高級接口
11  */
12 public ConfigurableApplicationContext run(String... args) {
13     //記錄程序運行時間
14     StopWatch stopWatch = new StopWatch();
15     stopWatch.start();
16     // ConfigurableApplicationContext Spring 的上下文
17     ConfigurableApplicationContext context = null;
18     Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
19     configureHeadlessProperty();
20     //從META-INF/spring.factories中獲取監聽器
21     //1、獲取並啟動監聽器
22     SpringApplicationRunListeners listeners = getRunListeners(args);
23     listeners.starting();
24     try {
25         ApplicationArguments applicationArguments = new DefaultApplicationArguments(
26                 args);
27         //2、構造應用上下文環境
28         ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
29         //處理需要忽略的Bean
30         configureIgnoreBeanInfo(environment);
31         //打印banner
32         Banner printedBanner = printBanner(environment);
33         ///3、初始化應用上下文
34         context = createApplicationContext();
35         //實例化SpringBootExceptionReporter.class,用來支持報告關於啟動的錯誤
36         exceptionReporters = getSpringFactoriesInstances(
37                 SpringBootExceptionReporter.class,
38                 new Class[]{ConfigurableApplicationContext.class}, context);
39         //4、刷新應用上下文前的準備階段
40         prepareContext(context, environment, listeners, applicationArguments, printedBanner);
41         //5、刷新應用上下文
42         refreshContext(context);
43         //刷新應用上下文後的擴展接口
44         afterRefresh(context, applicationArguments);
45         //時間記錄停止
46         stopWatch.stop();
47         if (this.logStartupInfo) {
48             new StartupInfoLogger(this.mainApplicationClass)
49                     .logStarted(getApplicationLog(), stopWatch);
50         }
51         //發布容器啟動完成事件
52         listeners.started(context);
53         callRunners(context, applicationArguments);
54     } catch (Throwable ex) {
55         handleRunFailure(context, ex, exceptionReporters, listeners);
56         throw new IllegalStateException(ex);
57     }
58 
59     try {
60         listeners.running(context);
61     } catch (Throwable ex) {
62         handleRunFailure(context, ex, exceptionReporters, null);
63         throw new IllegalStateException(ex);
64     }
65     return context;
66 }

 

  具體的每一行代碼的含義請看註釋,我們在這先總結一下啟動過程中的重要步驟:(筆者傾向於將應用上下文同容器區分開來)

第一步:獲取並啟動監聽器
第二步:構造應用上下文環境
第三步:初始化應用上下文
第四步:刷新應用上下文前的準備階段
第五步:刷新應用上下文
第六步:刷新應用上下文後的擴展接口

  OK,下面SpringBoot的啟動流程分析,我們就根據這6大步驟進行詳細解讀。最總要的是第四,五步。我們會着重的分析。

 

三、第一步:獲取並啟動監聽器

  事件機制在Spring是很重要的一部分內容,通過事件機制我們可以監聽Spring容器中正在發生的一些事件,同樣也可以自定義監聽事件。Spring的事件為Bean和Bean之間的消息傳遞提供支持。當一個對象處理完某種任務后,通知另外的對象進行某些處理,常用的場景有進行某些操作后發送通知,消息、郵件等情況。

1 private SpringApplicationRunListeners getRunListeners(String[] args) {
2     Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};
3     return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
4             SpringApplicationRunListener.class, types, this, args));
5 }

  在這裏面是不是看到一個熟悉的方法:getSpringFactoriesInstances(),可以看下下面的註釋,前面的博文我們已經詳細介紹過該方法是怎麼一步步的獲取到META-INF/spring.factories中的指定的key的value,獲取到以後怎麼實例化類的。

 1 /**
 2  * 通過指定的classloader 從META-INF/spring.factories獲取指定的Spring的工廠實例
 3  * @param type
 4  * @param parameterTypes
 5  * @param args
 6  * @param <T>
 7  * @return
 8  */
 9 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
10                                                       Class<?>[] parameterTypes, Object... args) {
11     ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
12     // Use names and ensure unique to protect against duplicates
13     //通過指定的classLoader從 META-INF/spring.factories 的資源文件中,
14     //讀取 key 為 type.getName() 的 value
15     Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
16     //創建Spring工廠實例
17     List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
18             classLoader, args, names);
19     //對Spring工廠實例排序(org.springframework.core.annotation.Order註解指定的順序)
20     AnnotationAwareOrderComparator.sort(instances);
21     return instances;
22 }

   回到refresh方法,debug這個代碼 SpringApplicationRunListeners listeners = getRunListeners(args); 看一下獲取的是哪個監聽器:

  EventPublishingRunListener監聽器是Spring容器的啟動監聽器。

   listeners.starting(); 開啟了監聽事件。

 

四、第二步:構造應用上下文環境

  應用上下文環境包括什麼呢?包括計算機的環境,Java環境,Spring的運行環境,Spring項目的配置(在SpringBoot中就是那個熟悉的application.properties/yml)等等。

  首先看一下prepareEnvironment()方法。

 1 private ConfigurableEnvironment prepareEnvironment(
 2         SpringApplicationRunListeners listeners,
 3         ApplicationArguments applicationArguments) {
 4     // Create and configure the environment
 5     //創建並配置相應的環境
 6     ConfigurableEnvironment environment = getOrCreateEnvironment();
 7     //根據用戶配置,配置 environment系統環境
 8     configureEnvironment(environment, applicationArguments.getSourceArgs());
 9     // 啟動相應的監聽器,其中一個重要的監聽器 ConfigFileApplicationListener 就是加載項目配置文件的監聽器。
10     listeners.environmentPrepared(environment);
11     bindToSpringApplication(environment);
12     if (this.webApplicationType == WebApplicationType.NONE) {
13         environment = new EnvironmentConverter(getClassLoader())
14                 .convertToStandardEnvironmentIfNecessary(environment);
15     }
16     ConfigurationPropertySources.attach(environment);
17     return environment;
18 }

   看上面的註釋,方法中主要完成的工作,首先是創建並按照相應的應用類型配置相應的環境,然後根據用戶的配置,配置系統環境,然後啟動監聽器,並加載系統配置文件。

 

4.1、 ConfigurableEnvironment environment = getOrCreateEnvironment(); 

  看看getOrCreateEnvironment()幹了些什麼。

 1 private ConfigurableEnvironment getOrCreateEnvironment() {
 2     if (this.environment != null) {
 3         return this.environment;
 4     }
 5     //如果應用類型是 SERVLET 則實例化 StandardServletEnvironment
 6     if (this.webApplicationType == WebApplicationType.SERVLET) {
 7         return new StandardServletEnvironment();
 8     }
 9     return new StandardEnvironment();
10 }

   通過代碼可以看到根據不同的應用類型初始化不同的系統環境實例。前面咱們已經說過應用類型是怎麼判斷的了,這裏就不在贅述了。

  

  從上面的繼承關係可以看出,StandardServletEnvironment是StandardEnvironment的子類。這兩個對象也沒什麼好講的,當是web項目的時候,環境上會多一些關於web環境的配置。

 

4.2、 configureEnvironment(environment, applicationArguments.getSourceArgs()); 

1 protected void configureEnvironment(ConfigurableEnvironment environment,
2                                     String[] args) {
3     // 將main 函數的args封裝成 SimpleCommandLinePropertySource 加入環境中。
4     configurePropertySources(environment, args);
5     // 激活相應的配置文件
6     configureProfiles(environment, args);
7 }

  在執行完方法中的兩行代碼后,debug的截圖如下

  如下圖所示,我在spring的啟動參數中指定了參數:–spring.profiles.active=prod(關於這個參數的用法,點我,其實就是啟動多個實例用的)

 

  在configurePropertySources(environment, args);中將args封裝成了SimpleCommandLinePropertySource並加入到了environment中。

  configureProfiles(environment, args);根據啟動參數激活了相應的配置文件。

  話不多說,debug一遍就明白了。

 

4.3、 listeners.environmentPrepared(environment); 

  進入到方法一路跟下去就到了SimpleApplicationEventMulticaster類的multicastEvent()方法。

  

  查看getApplicationListeners(event, type)執行結果,發現一個重要的監聽器ConfigFileApplicationListener。

  先看看這個類的註釋

 1 /**
 2  * {@link EnvironmentPostProcessor} that configures the context environment by loading
 3  * properties from well known file locations. By default properties will be loaded from
 4  * 'application.properties' and/or 'application.yml' files in the following locations:
 5  * <ul>
 6  * <li>classpath:</li>
 7  * <li>file:./</li>
 8  * <li>classpath:config/</li>
 9  * <li>file:./config/:</li>
10  * </ul>
11  * <p>
12  * Alternative search locations and names can be specified using
13  * {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
14  * <p>
15  * Additional files will also be loaded based on active profiles. For example if a 'web'
16  * profile is active 'application-web.properties' and 'application-web.yml' will be
17  * considered.
18  * <p>
19  * The 'spring.config.name' property can be used to specify an alternative name to load
20  * and the 'spring.config.location' property can be used to specify alternative search
21  * locations or specific files.
22  * <p>
23  * 從默認的位置加載配置文件,並將其加入 上下文的 environment變量中
24  */

  這個監聽器默認的從註釋中<ul>標籤所示的幾個位置加載配置文件,並將其加入 上下文的 environment變量中。當然也可以通過配置指定。

  debug跳過 listeners.environmentPrepared(environment); 這一行,查看environment屬性,果真如上面所說的,配置文件的配置信息已經添加上來了。

  

五、第三步:初始化應用上下文

  在SpringBoot工程中,應用類型分為三種,如下代碼所示。

 1 public enum WebApplicationType {
 2     /**
 3      * 應用程序不是web應用,也不應該用web服務器去啟動
 4      */
 5     NONE,
 6     /**
 7      * 應用程序應作為基於servlet的web應用程序運行,並應啟動嵌入式servlet web(tomcat)服務器。
 8      */
 9     SERVLET,
10     /**
11      * 應用程序應作為 reactive web應用程序運行,並應啟動嵌入式 reactive web服務器。
12      */
13     REACTIVE
14 }

  對應三種應用類型,SpringBoot項目有三種對應的應用上下文,我們以web工程為例,即其上下文為AnnotationConfigServletWebServerApplicationContext。

 1 public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework.boot."
 2         + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
 3 public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
 4         + "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
 5 public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
 6         + "annotation.AnnotationConfigApplicationContext";
 7         
 8 protected ConfigurableApplicationContext createApplicationContext() {
 9     Class<?> contextClass = this.applicationContextClass;
10     if (contextClass == null) {
11         try {
12             switch (this.webApplicationType) {
13                 case SERVLET:
14                     contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
15                     break;
16                 case REACTIVE:
17                     contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
18                     break;
19                 default:
20                     contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
21             }
22         } catch (ClassNotFoundException ex) {
23             throw new IllegalStateException(
24                     "Unable create a default ApplicationContext, "
25                             + "please specify an ApplicationContextClass",
26                     ex);
27         }
28     }
29     return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
30 }

  我們先看一下AnnotationConfigServletWebServerApplicationContext的設計。

   關於他的繼承體系,我們在前面的博客中<Spring IoC容器與應用上下文的設計與實現>已經詳細介紹了,在此不再贅述。

  應用上下文可以理解成IoC容器的高級表現形式,應用上下文確實是在IoC容器的基礎上豐富了一些高級功能。

  應用上下文對IoC容器是持有的關係。他的一個屬性beanFactory就是IoC容器(DefaultListableBeanFactory)。所以他們之間是持有,和擴展的關係。

  接下來看GenericApplicationContext類

1 public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
2     private final DefaultListableBeanFactory beanFactory;
3     ...
4     public GenericApplicationContext() {
5         this.beanFactory = new DefaultListableBeanFactory();
6     }
7     ...
8 }

   beanFactory正是在AnnotationConfigServletWebServerApplicationContext實現的接口GenericApplicationContext中定義的。在上面createApplicationContext()方法中的, BeanUtils.instantiateClass(contextClass) 這個方法中,不但初始化了AnnotationConfigServletWebServerApplicationContext類,也就是我們的上下文context,同樣也觸發了GenericApplicationContext類的構造函數,從而IoC容器也創建了。仔細看他的構造函數,有沒有發現一個很熟悉的類DefaultListableBeanFactory,沒錯,DefaultListableBeanFactory就是IoC容器真實面目了。在後面的refresh()方法分析中,DefaultListableBeanFactory是無處不在的存在感。

  debug跳過createApplicationContext()方法。

  如上圖所示,context就是我們熟悉的上下文(也有人稱之為容器,都可以,看個人愛好和理解),beanFactory就是我們所說的IoC容器的真實面孔了。細細感受下上下文和容器的聯繫和區別,對於我們理解源碼有很大的幫助。在系列文章中,我們也是將上下文和容器嚴格區分開來的。

 

  

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

【精選推薦文章】

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

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

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

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

前端Vue項目——初始化及導航欄

一、項目初始化

  創建webpack模板項目如下所示:

MacBook-Pro:PycharmProjects hqs$ vue init webpack luffy_project

? Project name luffy_project
? Project description A Vue.js project
? Author hqs
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

   vue-cli · Generated "luffy_project".

  根據提示啟動項目:

$ cd luffy_project/
$ npm run dev

  由於在初始化時選擇了vue-router,因此會自動創建/src/router/index.js文件。

  刪除Helloworld組件相關信息后,index.js文件內容如下所示:

import Vue from 'vue'
import Router from 'vue-router'
// @絕對路徑 檢索到 ...src/

// 如果Router當做局部模塊使用一定要Vue.use(Router)
// 以後在組件中,可以通過this.$router 獲取Router實例化對象
// 路由信息對象 this.$routes 獲取路由配置信息
Vue.use(Router)

// 配置路由規則
export default new Router({
  routes: [
    {
'path': '/'
} ] })

二、基於ElementUI框架實現導航欄

1、elementUI——適合Vue的UI框架

  elementUI是一個UI庫,它不依賴於vue,但確是當前和vue配合做項目開發的一個比較好的UI框架。

(1)npm安裝

  推薦使用 npm 的方式安裝,能更好地和 webpack 打包工具配合使用。

$ npm i element-ui -S

(2)CDN

  目前可以通過 unpkg.com/element-ui 獲取到最新版本的資源,在頁面上引入 js 和 css 文件即可開始使用。

<!-- 引入樣式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入組件庫 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

  使用CND引入 Element 需要在鏈接地址上鎖定版本,以免將來 Element 升級時受到非兼容性更新的影響。鎖定版本的方法請查看 unpkg.com。

2、引入 Element

  在項目中可以引入整個Element,或者是根據需要僅引入部分組件。

(1)完整引入

  在 main.js 中寫入如下內容:

import Vue from 'vue'
import App from './App'
import router from './router'
// elementUI導入
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'  // 注意樣式文件需要單獨引入
// 調用插件
Vue.use(ElementUI);

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
});

  以上代碼便完成了 Element 的完整引入。

  嘗試在App.vue使用elementui的Button按鈕:

<template>
  <div id="app">
    <!-- 導航區域 -->
    <el-button type="info">信息按鈕</el-button>

    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

  显示效果:

   

(2)按需引入

  藉助 babel-plugin-component,可以只引入需要的組件,以達到減小項目體積的目的。

  首先安裝babel-plugin-component:

$ npm install babel-plugin-component -D

  然後將.babelrc文件修改如下:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

  如果只希望引入部分組件,如Buttion何Select,那麼需要在 main.js 中寫如下內容:

import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';

Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或寫為
 * Vue.use(Button)
 * Vue.use(Select)
 */

new Vue({
  el: '#app',
  render: h => h(App)
});

3、導航欄實現

  首先創建/src/components/Common/LuffyHeader.vue文件:

<template>
  <!-- element-ui -->
  <el-container>
    <el-header height = '80px' >
      <div class="header">
        <div class="nav-left">
          <img src="https://www.luffycity.com/static/img/head-logo.a7cedf3.svg" alt="">
        </div>
        <div class="nav-center">
          <ul>
            <li>
              <a href="#">
                導航鏈接
              </a>
            </li>
          </ul>
        </div>
        <div class="nav-right">
          <span>登錄</span>
          &nbsp;| &nbsp;
          <span>註冊</span>
        </div>
      </div>
    </el-header>
  </el-container>
</template>

<script>
  export default {
    name: 'LuffyHeader',
    data(){
      return {
      }
    },
  };
</script>

  再創建/static/global/global.css文件:

* {
  padding: 0;
  margin: 0;
}

body {
  font-size: 14px;
  color: #4a4a4a;
  font-family: PingFangSC-Light; /*蘋果設計的一款全新的中文系統字體,該字體支持蘋果的動態字體調節技術*/
}

ul {
  list-style: none;
}

a {
  text-decoration: none;
}

  最後在App.vue中引入和使用組件:

<template>
  <div id="app">
    <!-- 導航區域 -->
    <LuffyHeader/>
    <router-view/>
  </div>
</template>

<script>
  import LuffyHeader from '@/components/Common/LuffyHeader'
  export default {
    name: 'App',
    components:{
      LuffyHeader
    }
  }
</script>

  显示效果如下所示:

  

三、導航欄路由跳轉

1、組件創建和路由配置編寫

  添加“首頁”、“免費課程”、“輕課”、“學位課”四大組件,因此創建如下文件:

src/components/Home/Home.vue
src/components/Course/Course.vue
src/components/LightCourse/LightCourse.vue
src/components/Micro/Micro.vue

  在src/router/index.js中引入組件,配置路由規則:

import Vue from 'vue'
import Router from 'vue-router'
// @絕對路徑 檢索到 ...src/

// 如果Router當做局部模塊使用一定要Vue.use(Router)
// 以後在組件中,可以通過this.$router 獲取Router實例化對象
// 路由信息對象 this.$routes 獲取路由配置信息
import Home from '@/components/Home/Home'
import Course from '@/components/Course/Course'
import LightCourse from '@/components/LightCourse/LightCourse'
import Micro from '@/components/Micro/Micro'

Vue.use(Router)

// 配置路由規則
export default new Router({
  routes: [
    {
      path: '/',
      redirect: '/home'   // 訪問/,直接跳轉到/home路徑
    },
    {
      path: '/home',
      name: 'Home',
      component: Home
    },
    {
      path: '/course',
      name: 'Course',
      component: Course
    },
    {
      path: '/home/light-course',
      name: 'LightCourse',
      component: LightCourse
    },
    {
      path: '/micro',
      name: 'Micro',
      component: Micro
    }
  ]
})

2、導航鏈接編寫

  修改 LuffyHeader.vue頁面,編寫導航鏈接:

<template>
  <!-- element-ui -->
  <el-container>
    <el-header height = '80px' >
      <div class="header">
        <div class="nav-left">
          <img src="https://www.luffycity.com/static/img/head-logo.a7cedf3.svg" alt="">
        </div>
        <div class="nav-center">
          <ul>
            <li v-for="(list, index) in headerList" :key="list.id">
              <a href="#">
                {{ list.title }}
              </a>
            </li>
          </ul>
        </div>
        <div class="nav-right">
          <span>登錄</span>
          &nbsp;| &nbsp;
          <span>註冊</span>
        </div>
      </div>
    </el-header>
  </el-container>
</template>

<script>
  export default {
    name: 'LuffyHeader',
    data() {
      return {
        headerList: [
          {id: '1', name: 'Home', title: '首頁'},
          {id: '2', name: 'Course', title: '免費課程'},
          {id: '3', name: 'LightCourse', title: '輕課'},
          {id: '4', name: 'Micro', title: '學位課程'}
        ],
        isShow: false
      }
    }
  }
</script>

  編寫headerList列表及列表中的導航對象,在 導航欄中遍歷對象獲取對應信息,显示在頁面效果如下所示:

  

3、router-link路由跳轉

  經過上面的編寫,雖然導航欄已經可以正常显示,但是a標籤是不會做自動跳轉的。 需要使用 router-link 進一步改寫LuffyHeader.vue,使得路由跳轉得以渲染對應組件:

<template>
  <!-- element-ui -->
  <el-container>
    <el-header height = '80px' >
      <div class="header">
        <div class="nav-left">
          <img src="https://www.luffycity.com/static/img/head-logo.a7cedf3.svg" alt="">
        </div>
        <div class="nav-center">
          <ul>
            <li v-for="(list, index) in headerList" :key="list.id">
              <router-link :to="{name:list.name}">
                {{ list.title }}
              </router-link>
            </li>
          </ul>
        </div>
        <div class="nav-right">
          <span>登錄</span>
          &nbsp;| &nbsp;
          <span>註冊</span>
        </div>
      </div>
    </el-header>
  </el-container>
</template>

<script>
  export default {
    name: 'LuffyHeader',
    data() {
      return {
        headerList: [
          {id: '1', name: 'Home', title: '首頁'},
          {id: '2', name: 'Course', title: '免費課程'},
          {id: '3', name: 'LightCourse', title: '輕課'},
          {id: '4', name: 'Micro', title: '學位課程'}
        ],
        isShow: false
      }
    }
  }
</script>

  使用to='{name:list.name}’設置命令路由,這樣點擊a標籤就可以跳轉了。显示效果如下所示:

  

  可以看到雖然點擊了輕課,但是和其他導航項樣式沒有任何分別,需要設置路由active樣式完成優化。

4、linkActiveClass設置路由的active樣式

  linkActiveClass 全局配置 <router-link> 的默認“激活 class 類名”。

  active-class 設置 鏈接激活時使用的 CSS 類名。默認值可以通過路由的構造選項 linkActiveClass 來全局配置。

(1)在路由配置linkActiveClass

  在 src/router/index.js 中做如下配置:

import Vue from 'vue'
import Router from 'vue-router'
// @絕對路徑 檢索到 ...src/

// 如果Router當做局部模塊使用一定要Vue.use(Router)
// 以後在組件中,可以通過this.$router 獲取Router實例化對象
// 路由信息對象 this.$routes 獲取路由配置信息
import Home from '@/components/Home/Home'
import Course from '@/components/Course/Course'
import LightCourse from '@/components/LightCourse/LightCourse'
import Micro from '@/components/Micro/Micro'

Vue.use(Router)

// 配置路由規則
export default new Router({
  linkActiveClass: 'is-active',
  routes: [
    {
      path: '/',
      redirect: '/home'   // 訪問/,直接跳轉到/home路徑
    },
    ......
    {
      path: '/micro',
      name: 'Micro',
      component: Micro
    }
  ]
})

(2)在LuffyHeader.vue中配置路由active樣式

<template>
  ......省略
</template>

<script>
  ......省略
</script>

<style lang="css" scoped>
  .nav-center ul li a.is-active{
    color: #4a4a4a;
    border-bottom: 4px solid #ffc210;
  }
</style>

(3)显示效果

  

5、hash模式切換為 history 模式

  vue-router 默認 hash 模式——使用URL的hash來模擬一個完整的URL,於是當URL改變時,頁面不會重新加載。比如http://www.abc.com/#/indexhash值為#/indexhash模式的特點在於hash出現在url中,但是不會被包括在HTTP請求中,對後端沒有影響,不會重新加載頁面。

  如果不想要這種显示比較丑的hash,可以用路由的 history模式,這種模式充分利用 history.pushState API來完成URL跳轉而無需重新加載頁面。

(1)路由修改為history模式

  修改 src/router/index.js 文件如下所示:

import Vue from 'vue'
import Router from 'vue-router'
// @絕對路徑 檢索到 ...src/

// 如果Router當做局部模塊使用一定要Vue.use(Router)
// 以後在組件中,可以通過this.$router 獲取Router實例化對象
// 路由信息對象 this.$routes 獲取路由配置信息
import Home from '@/components/Home/Home'
import Course from '@/components/Course/Course'
import LightCourse from '@/components/LightCourse/LightCourse'
import Micro from '@/components/Micro/Micro'

Vue.use(Router)

// 配置路由規則
export default new Router({
  linkActiveClass: 'is-active',
  mode: 'history',   // 改為history模式
  routes: [
    {
      path: '/',
      redirect: '/home'   // 訪問/,直接跳轉到/home路徑
    },
    .....
  ]
})

  使用history模式時,url就像正常url,例如http://yoursite.com/user/id,這樣比較美觀。

  显示效果如下所示:

  

(2)後端配置

   但是要用好這種模式,需要後台配置支持。vue的應用是單頁客戶端應用,如果後台沒有正確的配置,用戶在瀏覽器訪問http://yoursite.com/user/id 就會返回404,這樣就不好了。

  因此要在服務端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是app依賴的頁面。

  後端配置示例:https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90

 

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

kubernetes高級之pod安全策略

系列目錄

什麼是pod安全策略

pod安全策略是集群級別的用於控制pod安全相關選項的一種資源.PodSecurityPolicy定義了一系列pod相要進行在系統中必須滿足的約束條件,以衣一些默認的約束值.它允許管理員控制以下方面內容

Control Aspect Field Names
以特權運行容器 privileged
使用宿主名稱空間 hostPID, hostIPC
使用宿主網絡和端口 hostNetwork, hostPorts
使用存儲卷類型 volumes
使用宿主機文件系統 allowedHostPaths
flex存儲卷白名單 allowedFlexVolumes
分配擁有 Pod 數據卷的 FSGroup fsGroup
只讀root文件系統 readOnlyRootFilesystem
容器的用戶id和組id runAsUser, runAsGroup, supplementalGroups
禁止提升到root權限 allowPrivilegeEscalation, defaultAllowPrivilegeEscalation
Linux能力 defaultAddCapabilities, requiredDropCapabilities, allowedCapabilities
SELinux上下文 seLinux
允許容器加載的proc類型 allowedProcMountTypes
The AppArmor profile used by containers annotations
The seccomp profile used by containers annotations
The sysctl profile used by containers annotations

啟用pod安全策略

pod安全策略作為可選的(但強烈建議的)admission controller的實現.pod安全策略通過啟用admission controller來實現,但是僅僅啟用而沒有對策略授權則會導致整個集群無法創建pod!

由於pod安全策略api(policy/v1beta1/podsecuritypolicy)獨立於admission controller之外啟用,對於已經存在的集群建議在啟用admission controller之前添加並授權策略.

授權策略

當一個pod安全策略資源被創建(前面說過,psp(PodSecurityPolicy )pod安全策略是一種kubernetes資源),它什麼都不會做.為了使用它,請求操作的用戶或者目標pod的serviceaccount必須通過策略的use動詞來授權.

絕大部分kubernetes pod並不是直接由用戶直接創建的.相反,典型使用場景是它們通過Deployment或者ReplicaSet間接被創建,或者通過控制器管理器的其它模板控制器來創建.對控制器進行策略授權也將對它所創建的所有pod進行策略授權.因此首選的授權方法是對pod的serviceaccount進行策略授權(後面有示例).

通過RBAC授權

RBAC是kubernetes標準的授權模式,並且很容易用於授權安全策略使用.

首先,一個角色(role)或者集群角色(clusterRole)需要被授權使用(use動詞)它想要的策略.對角色的授權類似下面

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: <role name>
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - 一系列要進行授權的資源名稱

然後把集群角色(或角色)與授權的用戶綁定

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: 綁定名稱
roleRef:
  kind: ClusterRole
  name: 角色名稱
  apiGroup: rbac.authorization.k8s.io
subjects:
# Authorize specific service accounts:
- kind: ServiceAccount
  name: 授權的serviceaccount名稱
  namespace: <authorized pod namespace>
# Authorize specific users (not recommended):
- kind: User
  apiGroup: rbac.authorization.k8s.io
  name: 授權的用戶名

如果一個角色綁定(不是集群角色綁定)被使用,它僅對和它處於同一名稱空間下的pod才能進行有效策略授權,這樣同樣適用於用戶和用戶組

# Authorize all service accounts in a namespace:
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:serviceaccounts
# Or equivalently, all authenticated users in a namespace:
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:authenticated

故障排除

控制器管理器必須運行在安全的api端口上,並且不能有超級權限.不然請求就會繞過認證和授權模塊,將導致所有的策略均被允許,並且用戶可以創建特權pod

策略順序

除了限制pod的創建和更新,pod安全策略還用於提供它所控制的諸多字段的默認值.當有多個策略時,pod安全策略根據以下因素來選擇策略

  • 任何成功通過驗證沒有警告的策略將被使用

  • 如果是請求創建pod,則按通過驗證的策略按字母表順序被選用

  • 否則,如果是一個更新請求,將會返回錯誤.因為在更新操作過程中不允許pod變化

示例

以下示例假定你運行的集群開啟了pod安全策略admission controller並且你有集群管理員權限

初始設置

我們為示例創建一個名稱空間和一個serviceaccount.我們使用這個serviceaccount來模擬一個非管理員用戶

kubectl create namespace psp-example
kubectl create serviceaccount -n psp-example fake-user
kubectl create rolebinding -n psp-example fake-editor --clusterrole=edit --serviceaccount=psp-example:fake-user

為了方便辨認我們使用的賬戶,我們創建兩個別名

alias kubectl-admin='kubectl -n psp-example'
alias kubectl-user='kubectl --as=system:serviceaccount:psp-example:fake-user -n psp-example'

創建一個策略和一個pod

以下定義文件定義了一個簡單pod安全策略(PodSecurityPolicy),這個策略僅僅阻止創建特權pod

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: example
spec:
  privileged: false  # Don't allow privileged pods!
  # The rest fills in some required fields.
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  runAsUser:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  volumes:
  - '*'

我們使用kubectl命令來應用以上文件.

現在,做為一個非特權用戶,我們創建一個簡單pod

kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
  name:      pause
spec:
  containers:
    - name:  pause
      image: k8s.gcr.io/pause
EOF
Error from server (Forbidden): error when creating "STDIN": pods "pause" is forbidden: unable to validate against any pod security policy: []

發生了什麼?儘管pod安全策略已創建,不管是pod的serviceaccount還是fack-user都沒有權限使用這個策略.

kubectl-user auth can-i use podsecuritypolicy/example
no

創建一個rolebing來授權fake-user來使用example策略(example是前面創建的策略的名稱)

但是請注意這裏並不是首選方式!後面的示例將介紹首選的方式

kubectl-admin create role psp:unprivileged \
    --verb=use \
    --resource=podsecuritypolicy \
    --resource-name=example
role "psp:unprivileged" created

kubectl-admin create rolebinding fake-user:psp:unprivileged \
    --role=psp:unprivileged \
    --serviceaccount=psp-example:fake-user
rolebinding "fake-user:psp:unprivileged" created

kubectl-user auth can-i use podsecuritypolicy/example
yes

此時,再重新嘗試創建pod

kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
  name:      pause
spec:
  containers:
    - name:  pause
      image: k8s.gcr.io/pause
EOF
pod "pause" created

這次正如我們期待的一樣,可以正常工作.但是試圖創建特權pod仍然會被阻止(因此策略本身阻止創建特權pod)

kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
  name:      privileged
spec:
  containers:
    - name:  pause
      image: k8s.gcr.io/pause
      securityContext:
        privileged: true
EOF
Error from server (Forbidden): error when creating "STDIN": pods "privileged" is forbidden: unable to validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]

再運行一個其它pod

我們再嘗試創建一個pod,這次有一點不同

ubectl-user run pause --image=k8s.gcr.io/pause
deployment "pause" created

kubectl-user get pods
No resources found.

kubectl-user get events | head -n 2
LASTSEEN   FIRSTSEEN   COUNT     NAME              KIND         SUBOBJECT                TYPE      REASON                  SOURCE                                  MESSAGE
1m         2m          15        pause-7774d79b5   ReplicaSet                            Warning   FailedCreate            replicaset-controller                   Error creating: pods "pause-7774d79b5-" is forbidden: no providers available to validate pod request

從以上可以看到deployment已經成功創建(kubectl run 實際上會創建一個deployment).但是使用kubectl get pod命令卻沒有發現pod被創建.這是為什麼?問題的答案隱藏在replicaset控制器里.Fake-user成功創建的deployment(deployment又成功創建replicaset),但是當replicaset嘗試創建pod的時候,它並沒有被授權使用example定義的策略.

為了解決這個問題,需要把psp:unprivileged角色(前面創建的)綁定到pod的serviceaccount上(前面我們是綁定在了fake-user上).這裏serviceaccount是default(因為我們沒有指定其它用戶)

看到這裏如果你仍然覺得難以理解,可以回頭再看看,還是無法理解的話則需要補充關於角色,用戶和RBAC相關的知識.

kubectl-admin create rolebinding default:psp:unprivileged \
    --role=psp:unprivileged \
    --serviceaccount=psp-example:default
rolebinding "default:psp:unprivileged" created

這時候等待若干分鐘,replicaset的控制器最終會成功創建pod

kubectl-user get pods --watch
NAME                    READY     STATUS    RESTARTS   AGE
pause-7774d79b5-qrgcb   0/1       Pending   0         1s
pause-7774d79b5-qrgcb   0/1       Pending   0         1s
pause-7774d79b5-qrgcb   0/1       ContainerCreating   0         1s
pause-7774d79b5-qrgcb   1/1       Running   0         2s

清理工作

刪除名稱空間以刪除絕大部分示例中用到的資源

kubectl-admin delete ns psp-example
namespace "psp-example" deleted

注意現在剛剛創建的pod安全策略已經沒有了名稱空間,並且需要單獨被清除

kubectl-admin delete psp example
podsecuritypolicy "example" deleted

策略示例

以下是一個最小限制的策略,和不使用pod安生策略admission controller效果一樣

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: privileged
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
spec:
  privileged: true
  allowPrivilegeEscalation: true
  allowedCapabilities:
  - '*'
  volumes:
  - '*'
  hostNetwork: true
  hostPorts:
  - min: 0
    max: 65535
  hostIPC: true
  hostPID: true
  runAsUser:
    rule: 'RunAsAny'
  seLinux:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'

以下的一個示例有限制性策略,需要用戶是一個非特權用戶,阻止pod的權限提升

之所以要求是非特權用戶,因為特權用戶將會繞過限制

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default'
    apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
    seccomp.security.alpha.kubernetes.io/defaultProfileName:  'docker/default'
    apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
spec:
  privileged: false
  # Required to prevent escalations to root.
  allowPrivilegeEscalation: false
  # This is redundant with non-root + disallow privilege escalation,
  # but we can provide it for defense in depth.
  requiredDropCapabilities:
    - ALL
  # Allow core volume types.
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    # Assume that persistentVolumes set up by the cluster admin are safe to use.
    - 'persistentVolumeClaim'
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    # Require the container to run without root privileges.
    rule: 'MustRunAsNonRoot'
  seLinux:
    # This policy assumes the nodes are using AppArmor rather than SELinux.
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'MustRunAs'
    ranges:
      # Forbid adding the root group.
      - min: 1
        max: 65535
  fsGroup:
    rule: 'MustRunAs'
    ranges:
      # Forbid adding the root group.
      - min: 1
        max: 65535
  readOnlyRootFilesystem: false

策略參考

特權的

它決定了pod中的所有容器是否被允許以特權方式運行.默認情況下容器不允許訪問主機的設備,但是特權容器卻被允許訪問.這將允許容器有幾乎和它所在的進程一樣的訪問主機的權利.這將非常有用當容器想要使用主機的功能,比如訪問網絡的設備.

Host名稱空間

HostPID – 控制容器是否可以共享主機的進程id名稱空間

HostIPC – 控制容器是否可以共享主機的IPC名稱空間

HostNetwork – 控制容器是否可以使用所在節點的網絡名稱空間.這將允許pod訪問迴環設備,監聽localhost,並且可以窺探同一節點上其它pod的網絡活動狀況

AllowedHostPaths – 控制允許訪問的宿主機路徑

存儲卷和文件系統

Volumes – 提供了一系列的存儲卷類型白名單.這些允許的值和創建存儲卷時定義的資源類型相對應.想要獲取所有存儲卷類型,可以查看存儲卷類型列表.此外,*可以被用來允許所有的存儲卷類型

以下是推薦的最小化的允許存儲卷類型的安全策略配置

  • configMap
  • downwardAPI
  • emptyDir
  • persistentVolumeClaim
  • secret
  • projected

AllowedHostPaths– 它定義了一個hostPath類型的存儲卷可用的宿主機路徑的白名單.空集群意味着對宿主機的path無使用限制.它被定義為一個包含了一系列對象的單個pathPrefix字段,允許hostpath類型的存儲卷掛載以pathPrefix字段開頭的宿主機路徑.readonly字段意味着必須以readonly方式掛載(即不能寫入,只能讀)

allowedHostPaths:
  # This allows "/foo", "/foo/", "/foo/bar" etc., but
  # disallows "/fool", "/etc/foo" etc.
  # "/foo/../" is never valid.
  - pathPrefix: "/foo"
    readOnly: true # only allow read-only mounts

警告,一個可以無限制訪問宿主機文件系統的容器可以有很多方式提升權限,包括讀取其它容器內的數據,濫用系統服務的密鑰,比如kubecctl

可寫的hostpath目錄存儲卷允許容器寫入到宿主機文件系統,並且可以遍歷pathPrefix以外的文件系統,readOnly: true在kubernetes 1.11+版本以後才能使用,並且在allowedHostPaths必須使用以有效限制訪問特定的pathPrefix

ReadOnlyRootFilesystem – 限制容器必須以只讀的root文件系統運行(沒有可寫層)

特權提升

這個選項控制着容器的allowPrivilegeEscalation選項.這個布爾值直接控制着no_new_privs是否設置到容器運行的進程.它將阻止setuid來改變user ID,並且阻止文件有其它的能力(比如禁止使用ping工具).這個行為需要啟用MustRunAsNonRoot

AllowPrivilegeEscalation– 它決定着容器的安全上下文是否可以設置allowPrivilegeEscalation=true,為true是默認值.設置為false將使得容器所有的子進程沒有比父進程更高的特權

DefaultAllowPrivilegeEscalation,為allowPrivilegeEscalation設置默認值,從上面可以看到,默認的值為true.如果這個行為不是我們期待的,這個字段可以用於把它設置為不允許,但是仍然pod顯式請求allowPrivilegeEscalation

【精選推薦文章】

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

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

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

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

C#中await/async閑說

自從C#5.0增加異步編程之後,異步編程越來越簡單,async和await用的地方越來越多,越來越好用,只要用異步的地方都是一連串的異步,如果想要異步編程的時候,需要從底層開始編寫,這樣後邊使用的時候就是異步,那麼底層是如何實現??我們如何編寫高效率的異步方法??

#了解基於任務的異步模式(TAP)

基於任務的異步編程模型 (TAP) 提供了異步代碼的抽象化,你只需像往常一樣將代碼編寫為一連串語句即可,在開始調用的地方運行。例如:var task = method()①; await task②; 在①的時候開始運行可能還沒有運行完,在②程序掛起等待運行完,中間怎麼運行的你不需要知道,編譯器會做若干操作的。當開啟多個任務的時候,像要他們都執行完,在執行其他的時候,可以await Task.WhenAll(task1,task2 …..);

#了解async/await

await 運算符應用於異步方法,在方法的執行中插入掛起點,直到所等待任務完成。使用async 和await定義異步方法不一定會創建新線程,當編譯器看到await關鍵字時,線程會掛起等待運行結束。
await 僅可用於由 async 關鍵字修改的異步方法中,使用 async 修飾符定義的方法通常包含一個或多個 await 表達式,使用await運算符的任務通常是實現[基於任務的異步模式(TAP)]的方法調用返回,返回值包括 Task、Task<TResult>、ValueTask 和 ValueTask<TResult> 對象的方法。

# 調用 Task.Wait() 或者 Task.Result 立刻產生死鎖的充分條件

1. 調用 Wait() 或 Result 的代碼位於 UI 線程。
2. Task 的實際執行在其他線程,且需要返回 UI 線程。
死鎖的原因:UWP、WPF、Windows Forms 程序的 UI 線程都是單線程的。為了避免產生死鎖,你應該一條道走到黑, Async All the Way。或者.ConfigureAwait(false)

# ValueTask與Task的區別

7.0為async新增的ValueTask的作用(如果沒有在Nuget上下載System.Threading.Tasks.Extensions,ValueTask就在這個庫中),ValueTask用於值類型的異步;Task為引用類型的,每次需要分配空間。
例如:

public async Task<int> CalculateSum(int a, int b) {
    if (a == 0 && b == 0)
    {
        return 0;
    }

    return await Task.Run(() => a + b);
}

當a,b=0的時候不會運行到task里,這個時候返回task就造成了資源的浪費,修改為以下會效率更高

public async ValueTask<int> CalculateSum2(int a, int b)
{
    if (a == 0 && b == 0)
    {
        return 0;
    }

    return await Task.Run(() => a + b);
}

但是也不是說到處用ValueTask會好,當是引用類型的時候,用ValueTask,你需要關注更多的數據,這個時候用Task會更好。

# await/async原理分析

[AsyncStateMachine(typeof(Class1.<CalculateSum2>d__1))]
public ValueTask<int> CalculateSum2(int a, int b)
{
    Class1.<CalculateSum2>d__1 <CalculateSum2>d__;
    <CalculateSum2>d__.a = a;
    <CalculateSum2>d__.b = b;
    <CalculateSum2>d__.<>t__builder = AsyncValueTaskMethodBuilder<int>.Create();
    <CalculateSum2>d__.<>1__state = -1;
    AsyncValueTaskMethodBuilder<int> <>t__builder = <CalculateSum2>d__.<>t__builder;
    <>t__builder.Start<Class1.<CalculateSum2>d__1>(ref <CalculateSum2>d__);
    return <CalculateSum2>d__.<>t__builder.Task;
}

對CalculateSum2代碼解析,發現沒有await/async,原來又是編譯器提供的語法糖。

[__DynamicallyInvokable, DebuggerStepThrough, SecuritySafeCritical]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
    if (stateMachine == null)
    {
        throw new ArgumentNullException("stateMachine");
    }
    ExecutionContextSwitcher executionContextSwitcher = default(ExecutionContextSwitcher);
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(ref executionContextSwitcher);
        stateMachine.MoveNext();
    }
    finally
    {
        executionContextSwitcher.Undo();
    }
}

對Start方法進行分析,可以看出MoveNext,程序的運行其實還是一步一步進行的,那麼await/async會不會創建一個線程,這倒是不一定,這個由線程池決定,那麼異步了不創建一個線程,怎麼異步的,這裏的異步可能是運行在已經有的線程上。

【精選推薦文章】

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

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

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

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

DDD中的聚合和UML中的聚合以及組合的關係

UML:

聚合關係:成員對象是整體的一部分,但是成員對象可以脫離整體對象獨立存在。
如汽車(Car)與引擎(Engine)、輪胎(Wheel)、車燈(Light)之間的關係為聚合關係,引擎、輪胎、車燈可以脫離車而存在,比如把一個引擎換到另一個汽車上也可以。

組合關係:也表示的是一種整體和部分的關係,但是在組合關係中整體對象可以控製成員對象的生命周期,一旦整體對象不存在,成員對象也不存在。

所以,UML中聚合與組合的共同點:是兩者都是整體與部分的關係,差別點:是整體消亡后,成員對象是否可以脫離整體對象而單獨存在。

DDD聚合

也是一種整體和部分的關係,部分脫離整體會變得毫無意義,整體和部分之間強調一致的生命周期,也就是整體消亡的話部分也一起消亡,不能單獨存在。所以,從定義來看DDD中的聚合應該和UML中的組合關係是同一種關係。所以,Martin Flower也說,DDD中的聚合是限定在DDD這個方法論的上下文中,它不同於其他上下文(UML)中的聚合。

一個例子

按照上面的定義,我們再來分析一下一個典型的例子,就是公司和部門的關係。

UML的角度:
1、一個公司由多個部門組成,所以滿足整體和部分的關係;
2、一個部門不能脫離公司和加入到其他公司;

所以,我們推導出,在UML中公司和部門應該屬於組合關係。

DDD的角度:
雖然基於UML的角度,公司和部門屬於組合關係,那在DDD中是否應該把部門聚合在公司下面呢?我的看法是,雖然從生命周期上,確實部門不能脫離公司。但是DDD的聚合設計要考慮的因素會更加豐滿,比如:

  • DDD強調需求和Bounded Context,也就是會基於需求和上下文進行建模,我們建模前必須要先確定當前的需求和上下文是什麼;
  • 整體在當前上下文是否強關心部分的存在;
  • 整體和部分之間是否存在某些不變性規則;
  • 操作整體與操作部分的業務場景是否一致;
  • 性能問題,如果整體聚合的部分的數量過大,那也不會考慮聚合,即小聚合原則;
  • 一致性問題,我們在設計系統時,即便把本該是聚合在一起的對象分開設計為多個聚合,也可以從技術上去解決一致性,比如通過領域服務來完成多個聚合的協同創建、刪除、修改,並能通過數據庫事務來保證嚴格的強一致性;
  • DDD領域建模會對領域概念進行抽象,所以再領域模型中,在有些業務系統如組織架構管理系統中,也許就沒有公司了,而是只有部門,把公司也看成是一個頂層的部門就行,所以自然就不會有公司這個聚合根了;

所以,在進行DDD聚合設計時,如果僅從整體消亡後部分是否仍然存在意義這個點去推導的話,那考慮的就太單薄了,很有可能會得出不合理的聚合設計,最終很可能會導致聚合設計過大。這是沒有認真分析業務需求,沒有分析業務規則不變性,沒有對領域概念進行合理抽象,沒有進行OO軟件設計原則的應用的表現。

所以,以上案例由於需求不明,無法進行聚合設計。

題外話

我覺得DDD是對OOA/D的一個衍生,OOA/D是一種面向對象分析與設計的思想,強調通過設計對象,為對象分配職責,並讓對象之間通過協作的方式來完成軟件功能。而DDD則是對OO中的對象進行進一步的細化,比如首次提出了聚合、聚合根、實體、值對象、工廠、領域服務、領域事件,等。尤其是聚合的提出,讓OO設計更加豐富,大大減少了對象之間的關係複雜度,以及對象之間邊界的更加清晰。但是聚合的設計也很有難度,比如技術人員需要從我上面列舉的這些角度(不限於此)去進行聚合分析設計,這對開發人員的能力素質是一個很大的要求,可以說如果不會OOA/D的分析思維,就很難進行DDD領域建模。所以,我想這也是DDD很難在業務系統中落地的一個很大的原因之一吧。

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

HTTP認證之摘要認證——Digest(一)

導航

  • HTTP認證之基本認證——Basic(一)
  • HTTP認證之基本認證——Basic(二)
  • HTTP認證之摘要認證——Digest(一)
  • HTTP認證之摘要認證——Digest(二)

一、概述

Digest認證是為了修復基本認證協議的嚴重缺陷而設計的,秉承“絕不通過明文在網絡發送密碼”的原則,通過“密碼摘要”進行認證,大大提高了安全性。

相對於基本認證,主要有如下改進:

  • 絕不通過明文在網絡上發送密碼
  • 可以有效防止惡意用戶進行重放攻擊
  • 可以有選擇的防止對報文內容的篡改

需要注意的是,摘要認證除了能夠保護密碼之外,並不能保護其他內容,與HTTPS配合使用仍是一個良好的選擇。以下是摘要認證的具體流程圖:

看到上面出現了那麼多之前沒見過的參數,是不是有點慌(或是興奮)?別著急,這裏先給出一個概覽:

  • WWW-Authentication:用來定義使用何種方式(Basic、Digest、Bearer等)去進行認證以獲取受保護的資源
  • realm:表示Web服務器中受保護文檔的安全域(比如公司財務信息域和公司員工信息域),用來指示需要哪個域的用戶名和密碼
  • qop:保護質量,包含auth(默認的)和auth-int(增加了報文完整性檢測)兩種策略,(可以為空,但是)不推薦為空值
  • nonce:服務端向客戶端發送質詢時附帶的一個隨機數,這個數會經常發生變化。客戶端計算密碼摘要時將其附加上去,使得多次生成同一用戶的密碼摘要各不相同,用來防止重放攻擊
  • nc:nonce計數器,是一個16進制的數值,表示同一nonce下客戶端發送出請求的數量。例如,在響應的第一個請求中,客戶端將發送“nc=00000001”。這個指示值的目的是讓服務器保持這個計數器的一個副本,以便檢測重複的請求
  • cnonce:客戶端隨機數,這是一個不透明的字符串值,由客戶端提供,並且客戶端和服務器都會使用,以避免用明文文本。這使得雙方都可以查驗對方的身份,並對消息的完整性提供一些保護
  • response:這是由用戶代理軟件計算出的一個字符串,以證明用戶知道口令
  • Authorization-Info:用於返回一些與授權會話相關的附加信息
  • nextnonce:下一個服務端隨機數,使客戶端可以預先發送正確的摘要
  • rspauth:響應摘要,用於客戶端對服務端進行認證
  • stale:當密碼摘要使用的隨機數過期時,服務器可以返回一個附帶有新隨機數的401響應,並指定stale=true,表示服務器在告知客戶端用新的隨機數來重試,而不再要求用戶重新輸入用戶名和密碼了

二、剖析

1.當打開需要認證的頁面時,會彈出一個對話框,要求用戶輸入用戶名和密碼

2.使用Fidder監聽請求,可以看到在未進行認證或認證失敗的情況下,服務端會返回401 Unauthorized給客戶端,並附帶Challenge

3.輸入正確的用戶名和密碼后,瀏覽器會生成密碼摘要以及其他信息發送給服務端,服務端認證成功后,返回一些與授權會話相關的附加信息,放在Authorization-Info中。

其中,客戶端選擇的保護質量策略為authresponse就是通過計算得到的密碼摘要,具體計算方式如下(使用默認的MD5加密算法):

MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

算法 A1
MD5(默認) <username>:<realm>:<password>
MD5-sess MD5(<username>:<realm>:<password>):<nonce>:<cnonce>
qop A2
auth(默認) <request-method>:<uri>
auth-int <request-method>:<uri>:MD5(<request-entity-body>)

另外,rspauth使得客戶端可以對服務器進行認證,稱為響應摘要。響應摘要的計算與請求摘要類似,但由於響應中沒有方法,而且報文實體數據有所不同,所有隻有報文主題信息A2不同。具體區別如下:

qop A2
auth(默認) :<uri>
auth-int :<uri>:MD5(<response-entity-body>)

4.當服務端隨機數過期時,再次請求認證,可以看到質詢中增加了stale=true,用戶無需再次輸入用戶名和密碼,瀏覽器會自動使用新的質詢參數進行密碼摘要的計算。

三、注意事項

1.預授權:服務端預先告知客戶端下一個隨機數是多少,使得客戶端可以直接生成正確的Authorization首部,避免了多次“請求/質詢”。常用的有一下三種方式:

  • 服務器預先在Authorization-Info成功首部中發送下一個隨機數nextnonce。雖然這種機制加快了事務處理的速度,但是它也破壞了對同一台服務器的多次請求進行管道化的功能,可能會造成很大的損失。
  • 服務器允許在一小段時間內使用同一個隨機數。這也就是我們上面剖析中使用的機制,在一定時間內使用同一個隨機數或限制某個隨機數的重用次數,當過期時,聲明stale=true。雖然這確實降低了安全性,但是重用的隨機數的生存周期是可控的,應該在安全和性能之間找到平衡。
  • 客戶端和服務器使用同步的、可預測的隨機數生成算法。

2.RFC 2617建議採用這個假想的隨機數公式:

BASE64(timestamp MD5(timestamp “:” ETag “:” private-key))

其中,timestamp是服務器產生隨機數的時間或其他不重複的值,ETag是與所請求實體有關的HTTP ETag首部的值,private-key是只有服務器知道的私鑰。

【精選推薦文章】

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

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

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

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

Python 裝飾器

裝飾器

什麼是裝飾器

裝飾器顧名思義就是一個有裝飾功能的工具,那麼裝飾器又是用來裝飾什麼的?為什麼要裝飾這個東西?裝飾的目的是什麼呢?本文會一一作答讓小弟一個一個說

開放封閉原則

談及裝飾器就要引申一個概念,那就是開放封閉原則,那麼問題又來了 什麼是開放封閉……好好好,直接說這個,開放封閉本來是兩個對立的概念,也就是說是一對反義詞,那麼為什麼要提出開放封閉原則呢?原因是在日常的開發工作中,一般最初上線的產品的功能是不盡完善的,就是說不夠完美但已經能夠支撐日常使用,其餘的功能可以日後擴展,舉例:N年前的QQ和現在的QQ(PS:雖然現在本人不怎麼用了)。在後期擴展功能時,因為函數已經是寫好的,而且存在大量調用,所以要直接去給函數增添新的功能顯然不現實,所以我們只能新建函數去給原來的函數擴展功能(其實這就是一個裝飾器啦)

那麼開放封閉原則到底是什麼? 答案是對源碼封閉,對新功能開放

  • 封閉原則:不要改變源代碼

  • 開放原則:能增加一些額外的功能

    如果還有客觀對開放封閉原則似知似解的話,沒關係接着往下看,不影響您食用本文,因為Python裝飾器本身就是對開放封閉原則完美的詮釋

裝飾器初識

不低調的說,裝飾器就是一個函數,名字本來很高大上,但本質就是一個函數,裝飾器函數的功能就是要裝飾一個函數,在不改變被裝飾函數的源代碼及調用方式的前提下,為其增加額外的功能。是不是有點門道了,是不是覺得這玩意也沒啥啦,真是優秀的同學。

代碼show(技術博客不寫代碼干白話說出去丟人)

def warpper(f):  #定義一個函數(裝飾器),傳入的參數是被修飾的函數的函數名

def inner(*args,**kwargs): #嵌套一個內存函數,這個函數主題才是執行被裝飾函數源碼的關鍵

    '''這塊可以加要在被裝飾函數執行之前的操作哈'''

    ret = f(*args,**kwargs) #這裏的形參我會在下面說明

    '''這裏可以寫被裝飾函數之後的,兄嘚,別客氣,想加啥方法加什麼'''

    return ret #這裏的返回值如果我一會兒不忘的話也會在下面說明

return inner

@warpper #這個叫語法糖,嗯……可以吃(可能老外命名的時候就是這麼想的),結構是@加函數名,作用下面會說明
def func():
  print('我就是那個被裝飾的函數')
簡單說明

因為本人比較懶,所以原諒我直接把代碼甩上去了,後面有註釋,看懂了的大佬可以say goodbye啦,想打我的接着聽我白話,那我就把備註再重複一遍,哈哈,你也看到了,裝飾器用到了函數的嵌套(再具體點就是閉包,要問什麼是閉包,百度吧,哈哈),首先外層函數接收到一個函數名,然後返回值是內層函數的函數名;再來內層函數可以接收參數,其中ret = f(*args,**kwargs)有兩個作用,一是執行了傳入的函數,也就是執行了被修飾的函數,二是將返回值賦給了一個變量,此處要說明一下,對被修飾的函數功能的擴展要寫在這裏哦,最後 ret作為內層函數的返回值返回給函數執行者。

語法糖

@warpper這東西和 func = wrapper(func)是一樣的,也就是說最後三行代碼可以這樣寫

#@warpper 
def func():
  print('我就是那個被裝飾的函數')
func = wrapper(func)   #注意奧,這玩意要寫在被裝飾函數的下邊,語法糖才寫上邊

至於為什麼要寫着東西,或者為什麼要用語法糖?請聽下回分解~~~,收起你滴拳頭,是這樣,我剛開始的時候談到了,裝飾器是要在不改變源碼和其調用方式的前提下給其增加新的功能,注意到了么 調用方式 嗯……沒錯就是調用方式,如果我不這樣寫那我是不是要wrapper(func)()這樣去調用啊,是不是有點繞了,但是我把wrapper(func)賦值給了一個和被裝飾函數同名的變量,那我此時要怎麼調用,是不是就是func(),這樣就滿足了開放封閉原則,完美!其實本質就是要把裝飾器“偽裝”成原函數,包括調用方式、參數、返回值,裝就要裝的像一點,對吧。

裝飾帶參數的函數
def wrapper(f):
def inner(*args,**kwargs):
f(*args,**kwargs)
return inner
@wrapper
def func(a,b):
print(a,b)

函數func中有兩個形參a和b,說一下這兩個參數在裝飾器中的旅程,函數inner的萬能參數接收到a和b打包,然後inner函數充當中間商將打包后的元組給了f,在f中打散又成了變量a和b,在裝飾器時就不會影響傳參了,這就是裝飾帶參數的函數

裝飾有返回值的函數

def wrapper(f):
def inner():
ret = f()
return ret
return inner
@wrapper
def func():
return('我不管,我最帥')

哈哈,這段代碼中有我的心聲,你們都懂的,很簡單,裝飾有返回值的參數,在inner函數中將f()賦值給ret,這樣ret就接收到了返回值,再將ret返回給inner(),以此來達到“模擬”被裝飾函數的返回值,也可以說是通過這種方法來拿到被裝飾函數的返回值。

裝飾器帶參數

先舉個例子,當我們需要寫一個簡單的登陸認證功能的時候,我們的目的是要用裝飾器給調用的函數增加一個認證是否登陸過此網站的功能,也就是要驗證用戶名和密碼,但比如不同公司的網站數據庫肯定並非同一個,這個時候就需要帶參數的裝飾器啦,它可以實現我們要用一個裝飾器裝飾多個類似函數的目的

def wrapper_out(n):
   def wrapper(f):
       def inner(*args,**kwargs):
           with open(n,encoding='utf-8') as f1:
           '''此部分省略認證的詳細功能'''
           f(*args,**kwargs)
       return inner
   return wrapper

@wrapper_out('webpage1')
def wangzhi1():
   print('歡迎訪問網址1')
   
@wrapper_out('webpage2')
def wangzhi1():
   print('歡迎訪問網址2')

如果使用標準的裝飾器函數的話只能裝飾其中的一個函數,當裝飾另一個函數時會因為訪問不到正確的數據庫而報錯。

@wrapper_out('webpage1')這段代碼先執行wrapper_out('webpage1')這個函數先把參數webpage傳給n,並且返回一個wrapper,此時@和wrapper結合在一塊有沒有很眼熟,沒錯,這就是我們所熟悉的標準的裝飾器了,餘下的流程就和標準的裝飾器完全相同了。

多個裝飾器裝飾一個函數

這是一種特殊的情況,下面重點分析這種情況的結果是如何產生的,會有點繞。

def wrapper1(func1):
    def inner1():
        print('wrapper1 ,before func')
        func1()
        print('wrapper1 ,after func')
    return inner1

def wrapper2(func2): # func2 == inner1
    def inner2():
        print('wrapper2 ,before func')
        func2() # inner1
        print('wrapper2 ,after func')
    return inner2

@wrapper2  
@wrapper1  
def f():
print('in f') # 3

f()  

結果:

wrapper2,before func
wrapper1,before func
in f
wrapper1,after func
wrapper2,after func

怎麼說?是不是和你預期的結果有所不同,下面來按步驟說一下為什麼會產生這樣的結果(我盡量表達清楚哈)

1. 函數定義不調用不執行直接pass
2.   @wrapper2
@wrapper1  
def f():
單獨看下面這兩行,標準裝飾器哈 @wrapper1 等價於 f = wrapper1(f) = inner1(返回值哈)
3. @wrapper2
  @wrapper1   #注意哈 看步驟2 這裏現在是inner1咯、
  @wrapper2 等價於 inner1 = wrapper2(inner1) = inner2(返回值)
4. 此時的 f = inner2 執行f() 就從inner2()開始執行
5. 執行inner2 首先打印'wrapper2 ,before func'
6. 然後執行func2,由步驟3可知當前wrapper2中的參數是inner1 也就是執行inner1 所以打印wrapper1 ,before func
7. 然後執行func1 參考步驟2 可知這裏的func1()執行的是f()也就是真正的原函數,打印in f
8. 順序執行,打印wrapper1 ,after func
9. inner1執行完(其實就是func2執行完)還是順序執行 打印wrapper2 ,after func
10. 結果出來了、哈哈

這部分本人能力也只能寫成這樣了,各位看官看不懂那一定是小弟沒表述清楚,不過沒關係,在下再支一招,看圖

 

結合結果分析,相信各位老闆也能推導出正確的結果了,嗯、真帥!

好,到此對Python裝飾器應該有那麼一丟丟的認識了哈,我不管,就得有認識。下面的內容和文章關係不大哈

第一篇正經寫的文章,可能文章內容表達不盡如人意的正經哈,但初心是好的,就是分享知識,分享心得嘛,嗯,不管怎麼說,我還是很欣慰的對自己,哈哈,能有人從中有收穫就更perfect嘍

【精選推薦文章】

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

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

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

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