量子邏輯門

量子態的演化

在前面量子糾纏1中我們已經提到了量子比特的線性代數表示,即,對於一個量子態 \(\alpha_0 | 0\rangle +\alpha_1 | 1\rangle\)我們可以化簡成$ \left[ \begin{array}{}{\alpha_0} \ {\alpha_1}\end{array}\right]$ 。

量子態不是一成不變的,就像高電平會變成低電平,一個量子態也能演化成另一個量子態,量子態的演化就是在Hilbert空間中的旋轉,如圖(a)所示。

通過一個U操作,我們就將 \(| 0\rangle\) 變成了 \(U| 0\rangle\)\(| 1\rangle\) 變成了 \(U| 1\rangle\)\(| u\rangle\) 變成了 \(U| u\rangle\) ,如圖(b)所示。需要注意的是,我添加一個U操作,沒有改變 \(| 0\rangle\)\(| 1\rangle\)\(| u\rangle\) 之間的關係, \(U| 0\rangle\)\(U| 1\rangle\)\(| 0\rangle\)\(| 1\rangle\) 一樣,他們之間的關係依舊是垂直。

$ (| 0\rangle, | 1\rangle)=0$

$ (U| 0\rangle, U| 1\rangle)=0$

\((,)\) 是內積的意思, $ (| 0\rangle, | 1\rangle)= \left[ \begin{array}{}{1}&{0}\end{array}\right]\left[ \begin{array}{}{0} \ {1}\end{array}\right]$ ,同樣,也可以簡寫成 \(\langle0| 1\rangle\)\(\langle0|\) 表明是 \(| 0\rangle\) 的共軛轉置。

對於這種兩個向量之間夾角不會變的旋轉稱為剛性旋轉 rigid rotation。

而這種U操作被成為酉操作,也是unitary transformation

Unitary Transformation

量子比特我們用向量來表示,因為我們量子比特的演化是線性的,所以在量子比特上的操作,可以用矩陣來表示。

單量子比特是 \(2*1\) 的向量,則單量子比特門是 \(2*2\) 的矩陣。

\(| 0\rangle\) 變到 \(U| 0\rangle\) 在線性代數上就是 $ \left[ \begin{array}{}{1} \ {0}\end{array}\right]$ 變到 $ \left[ \begin{array}{}{\frac{1}{\sqrt2}} \ {\frac{1}{\sqrt2}}\end{array}\right]$

\[ \left[ \begin{array}{}{\frac{1}{\sqrt2}} \\ {\frac{1}{\sqrt2}}\end{array}\right]=U\left[ \begin{array}{}{1} \\ {0}\end{array}\right]\]

\[U= \left[ \begin{array}{}{\frac{1}{\sqrt2}} &{-\frac{1}{\sqrt2}} \\ {\frac{1}{\sqrt2}}&{\frac{1}{\sqrt2}} \end{array}\right]\]

對於旋轉了 \(\theta\) 角度的操作,都可以用 \(U_{\theta}= \left[ \begin{array}{}{cos\theta} &{-sin\theta} \\ {sin\theta}&{cos\theta} \end{array}\right]\) 表達。

如果要做相反操作,就是將順時針轉 \(\theta\) 角度, \(U_{-\theta}= \left[ \begin{array}{}{cos\theta} &{sin\theta} \\ {-sin\theta}&{cos\theta} \end{array}\right]\)

很巧的是, \(U_\theta^\dagger=U_{-\theta}\)\(\dagger\) 是共軛轉置的意思。

\(U_\theta U_{\theta}^\dagger=I\) ,意思也很好理解,因為順時針 \(\theta\)\(-\theta\) ,正好就回到原位。

事實上所有的量子操作都是可逆的,所有的量子操作都酉操作。

那麼什麼是酉操作呢?

U is unitary iff \(U^\dagger U =I\)

對於酉矩陣的更多特徵會在線性代數的章節提到,這裏主要提一個,酉矩陣是保內積的。

保內積又是什麼意思?

兩個向量在乘以相同的U后,他們的內積不變。

\[(U| a\rangle, U| b\rangle)=\langle a|U^\dagger U|b\rangle=\langle a|I|b\rangle=\langle a|b\rangle\]

單量子邏輯門

量子邏輯門和經典邏輯門一個巨大的不同是——量子邏輯門可逆。

經過了經典的邏輯門與門或者非門,我們的信息會丟失,告訴你與門后的輸出結果是0,你知道與門前的輸入嗎?(0,0)、(0,1)、(1,0)都有可能。

而對於量子邏輯門來說,我經過U變換后的結果是 \(|a\rangle\) ,那麼 \(U^\dagger |a\rangle\) 就是變換前的輸入了。

舉例幾個常用的單量子邏輯門:

\(X=\left[ \begin{array}{}{0} &{1} \\ {1}&{0} \end{array}\right]\),X門又稱為比特翻轉,他可以把 \(|0\rangle\) 變成 \(|1\rangle\) ,把 \(|1\rangle\) 變成 \(|0\rangle\)

\(Y=\left[ \begin{array}{}{0} &{-i} \\ {i}&{0} \end{array}\right]\)

\(Z=\left[ \begin{array}{}{1} &{0} \\ {0}&{-1} \end{array}\right]\),Z門又稱為相位翻轉門,可以把 \(|+\rangle\) 變成 \(|-\rangle\)\(-|1\rangle\) 變成 \(|1\rangle\)

以及一個特別有用的門,Hadamard門:
\(H=\left[ \begin{array}{}{\frac{1}{\sqrt2}} &{\frac{1}{\sqrt2}} \\ {\frac{1}{\sqrt2}}&{-\frac{1}{\sqrt2}} \end{array}\right]\) ,他的作用是把 \(|1\rangle\) 變成 \(|-\rangle\)\(|0\rangle\) 變成 \(|+\rangle\)

兩量子邏輯門

對於兩量子比特來說,他們的狀態是 \(\alpha_{00} | 00\rangle+\alpha_{01} | 01\rangle+\alpha_{10} | 10\rangle+\alpha_{11} | 11\rangle\) ,需要用 \(4*1\) 的向量來描述,也就是 $ \left[ \begin{array}{}{\alpha_{00}} \ {\alpha_{01}} \ {\alpha_{10}} \ {\alpha_{11}} \end{array} \right]$ ,對應操作兩比特的邏輯門,也就是 \(4*4\) 的矩陣了。

兩比特的量子門有各自管各自的,如圖(c),也有一個控制另一個的,如圖(d)。

對於圖c來說, \(U=u_1\otimes u_2\) ,如果 \(u_1=\left[ \begin{array}{}{a} &{c} \\ {b}&{d} \end{array}\right],u_2=\left[ \begin{array}{}{e} &{g} \\ {f}&{h} \end{array}\right]\) ,那麼, \(U=\left[ \begin{array}{}{a\left[ \begin{array}{}{e} &{g} \\ {f}&{h} \end{array}\right]} &{c\left[ \begin{array}{}{e} &{g} \\ {f}&{h} \end{array}\right]} \\ {b\left[ \begin{array}{}{e} &{g} \\ {f}&{h} \end{array}\right]}&{d\left[ \begin{array}{}{e} &{g} \\ {f}&{h} \end{array}\right]} \end{array}\right]\) ,這也就是張量積的算法。

對於圖d來說,這是一個受控非門CNOT門,他的意思是,如果a是0,那麼b保持不變,如果a是1,那麼b就是變成相反的,比如 \(|0\rangle\) 變成 \(|1\rangle\) ,或者把 \(|1\rangle\) 變成 \(|0\rangle\)

\(|00\rangle to|00\rangle,|01\rangle to|01\rangle,|10\rangle to|11\rangle,|11\rangle to|10\rangle\)

用矩陣來描述就是 \(\left[\begin{array}{cccc}{1} & {0} & {0} & {0} \\ {0} & {1} & {0} & {0} \\ {0} & {0} & {0} & {1} \\ {0} & {0} & {1} & {0}\end{array}\right]\)

至此,主要的量子邏輯門就介紹完畢,如果想要動手實踐的話,有阿里的量子計算雲平台、華為的hiQ、IMB的IBM Q

參考資料

Quantume Mechanics & Quantume Computation Lecture 5

【精選推薦文章】

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

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

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

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

Windows性能計數器監控實踐

Windows性能計數器(Performance Counter)是Windows提供的一種系統功能,它能實時採集、分析系統內的應用程序、服務、驅動程序等的性能數據,以此來分析系統的瓶頸、監控組件的表現,最終幫助用戶對系統進行合理調優。市面上採集Windows性能計數器指標的產品參差不齊,尤其在處理某類應用程序有多個進程實例時,採集的數據更是差強人意。所幸微軟為碼農精心準備了獲得性能計數器指標的接口,用於靈活獲得相關性能計數器指標值,但進程級別Windows性能計數器指標的採集監控,並沒有想象的那麼美好。因此本文結合筆者應用實踐,探討進程級別Windows性能計數器指標統一採集監控方案,以及在應用實踐中遇到的坑,作為避坑指南,供感興趣的同行參考。

進程級別Windows性能計數器指標作為特來電監控平台的一部分,對深入掌握系統進程級別運行狀態,定位系統存在的問題,以便更快、更準的發現潛在的線上問題,起到了舉足輕重的作用。

針對Windows性能計數器的監控,統一的採集監控方案如下所示:

 

 性能計數器指標統一採集監控方案

本文重點關注指標管理與指標採集,對指標存儲及指標展現只做概要闡述。

一、        指標管理

Windows性能計數器指標類別比較多,因此我們需要對關注的指標進行分類管理。針對進程級別監控,我們主要關注CLR以及進程相關類別指標:.NET CLR Memory、.NET CLR Exception、.NET CLR Jit、.NET CLR Loading、Process等。

一個Windows性能計數器主要由3個屬性來標識:指標類別(Category Name)、指標名稱(Counter Name)、指標實例(Instance Name)。為了能對某類應用程序的多個進程實例進行統一採集,我們不對指標實例進行管理,而對指標實例對應的進程名稱進行管理,同時支持一個性能計數器指標關聯多個進程名稱,並且在運行時動態計算出每個進程名稱對應的多個進程實例,從而大幅降低指標管理的工作量。

二、        指標採集

指標採集主要解決採集插件運行時的空間(採集範圍)與時間(採集頻率)問題。並不是所有機器都部署了我們關注的應用程序,因此需要通過採集範圍,確定需要對哪些機器上的性能計數器指標進行採集,同時需要確定採集頻率,比如10秒、1分鐘、5分鐘等。

雖然微軟提供了性能計數器接口用於採集對應的指標值,但當一個應用程序有多個進程實例時(比如一個機器上部署了多個IIS站點,進程名稱都是w3wp,在性能計數器中的實例名稱是w3wp、w3wp#1、…、w3wp#n),進行指標採集的坑會比較多,這裏介紹幾個比較典型的問題。

由於性能計數器默認不显示進程ID,所以無法直接建立進程實例和性能計數器指標實例的關聯關係,相同的性能計數器指標實例名稱,可能屬於一個或多個不同的進程實例。

 

 進程實例與性能計數器實例關聯關係

比如在.NET CLR Memory和Process中實例名稱同為w3wp#1的性能計數器,可能對應同一個進程實例,也可能對應不同的進程實例,這是最詭異的坑!市面上一些監控產品無法準確採集同一應用程序對應多個進程實例的性能計數器指標值,可能與此有關。

為了能建立進程實例與性能計數器實例的關聯關係,需要在显示性能計數器實例時帶上進程ID。

方案一:修改註冊表。但潛在的坑也很明顯:只適用於.NET CLR Memory以及Process類別的性能計數器,同時可能會導致第三方監控工具失效,並且修改生產環境的註冊表風險不可控,不是首選方案。

方案二:動態設置環境變量。針對.NET CLR相關的性能計數器,在調用性能計數器接口之前,進行如下環境變量設置:

Environment.SetEnvironmentVariable(“COMPlus_ProcessNameFormat”, “1”);

該方案是進程級別的,設置后得到的性能計數器實例會自動帶上進程ID,並且不會影響到全局設置或者其它應用程序,是推薦方案。

採集進程級別指標時,有時需要根據IIS站點進程ID獲得對應的應用程序池以及物理路徑:

 

 通過進程ID獲得應用程序池以及物理路徑

方案一:調用WMI(Windows Management Instrumentation)接口獲得應用程序池。

Select * from Win32_Process WHERE processID=PID

該方案存在的坑:頻繁調用會導致機器CPU飆升,不是首選方案。

方案二:調用Appcmd.exe命令獲得應用程序池。

appcmd.exe list wp

該方案通過命令獲得結果后,只需要進行字符串解析,即可獲得進程ID與應用程序池的關聯關係,是推薦方案。

三、        指標存儲

指標存儲在時序數據庫中,每個性能計數器類別(Category Name)+性能計數器名稱(Counter Name)對應一個指標表,表中按進程名稱進行分類,每一行表示一個進程實例對應性能計數器實例的指標值。

四、        指標展現

指標展現可以按進程名稱、進程實例、機器等維度進行分類聚合展現,相比登錄到每個機器設置性能計數器,指標集中展現大幅提升了工作效率。

五、        總結

本文探討了Windows性能計數器監控實踐,主要涉及指標管理、指標採集、指標存儲、指標展現四個方面,同時介紹了同一應用程序對應多個進程實例時,指標採集中遇到的坑。

【精選推薦文章】

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

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

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

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

python算法與數據結構-希爾排序(35)

一、希爾排序的介紹

  希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的記錄越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。  

二、希爾排序的原理

  在前面文章中介紹的直接插入排序,它對於已經基本有序的數據進行排序,效率會很高,而如果對於最初的數據是倒序排列的,則每次比較都需要移動數據,導致算法效率降低。

      希爾排序的基本思想就是:將需要排序的序列邏輯上劃分為若干個較小的序列(但並非真的分割成若干分區),對這些邏輯上序列進行直接插入排序,通過這樣的操作可使需要排序的數列基本有序,最後再使用一次直接插入排序。

      在希爾排序中首先要解決的是怎樣劃分序列,對於子序列的構成不是簡單地分段,而是採取將相隔某個增量的數據組成一個序列。一般選擇增量的規則是:取上一個增量的一半作為此次子序列劃分的增量,一般初始值元素的總數量的一半。

三、希爾排序的圖解 

 

四、希爾排序的python代碼實現

# 創建一個希爾排序的函數
def shell_sort(alist):
    # 需要排序數組的個數
    N = len(alist)
    # 最初選取的步長
    gap = N//2
    
    # 根據每次不同的步長,對分組內的數據進行排序
    # 如果步長沒有減為1就繼續執行
    while gap>0:
        # 對每個分組進行插入排序,
        # 因為插入排序從第二個元素開始,而這裏第二個元素的下標就是gap
        # 所以i的起始點是gap
        for i in range(gap,N):
            # 控制每個分組內相鄰的兩個元素,邏輯上相鄰的兩個元素間距為gap,
            # j的前一個元素比它少一個gap距離,所以for循環中j的步長為 -gap
            for j in  range(i,0,-gap):
                # 判斷和邏輯上的分組相鄰的兩個數據大小
                if alist[j]<alist[j-gap] and j-gap>=0:
                    # 交換
                    temp = alist[j]
                    alist[j] = alist[j-gap]
                    alist[j-gap] = temp
        # 改變步長
        gap = gap//2
    
    
numlist = [5,7,8,3,1,2,4,6,9]
print("排序前:%s"%numlist)
shell_sort(numlist)
print("排序后:%s"%numlist)

運行結果為:

排序前:[5, 7, 8, 3, 1, 2, 4, 6, 9]
排序后:[1, 2, 3, 4, 5, 6, 7, 8, 9]

五、希爾排序的C語言實現

#include <stdio.h>
// 創建一個希爾排序的函數
void shell_sort(int arr[],int arrLength,int gap)
{
    // 根據每次不同的步長,對分組內的數據進行排序
    // 如果步長沒有減為1就繼續執行
    while (gap>0)
    {
        // 對每個分組進行插入排序,
        // 因為插入排序從第二個元素開始,而這裏第二個元素的下標就是gap,
        // 所以i的起始點是gap
        for (int i = gap; i<arrLength; i++)
        {
            // 控制每個分組內相鄰的兩個元素,邏輯上相鄰的兩個元素間距為gap,
            // j的前一個元素比它少一個gap距離,所以for循環中j每次減少一個gap
            // 因為j-gap是上一個元素的下標,也必須保證大於等於0
            for (int j = i; j>0&&j-gap>=0; j=j-gap)
            {
                // 判斷和邏輯上的分組相鄰的兩個數據大小
                if (arr[j]<arr[j-gap])
                {
                    // 交換
                    int temp = arr[j];
                    arr[j] = arr[j-gap];
                    arr[j-gap] = temp;
                }
            }
        }
        gap = gap/2;
    }
}

int main(int argc, const char * argv[]) {
   
    // 定義數組
    int array[] = {5,7,8,3,1,2,4,6,9};
    // 希爾排序的聲明
    void shell_sort(int arr[],int arrLength,int gap);
    // 計算數組長度
    int len = sizeof(array)/sizeof(int);
    // 制定gap為二分之一的長度
    int g = len/2;
    // 使用希爾排序
    shell_sort(array, len, g);
    // 驗證
    for (int i = 0; i<len; i++)
    {
        printf("%d ",array[i]);
    }
    
    return 0;
}

運行結果為:

1 2 3 4 5 6 7 8 9

 

六、希爾排序的時間複雜度

  • 最優時間複雜度:根據步長序列的不同而不同
  • 最壞時間複雜度:O(n2)

七、希爾排序的穩定性

  由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。

 

【精選推薦文章】

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

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

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

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

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

[NewLife.XCode]角色權限

NewLife.XCode是一個有10多年歷史的開源數據中間件,支持nfx/netcore,由新生命團隊(2002~2019)開發完成並維護至今,以下簡稱XCode。

整個系列教程會大量結合示例代碼和運行日誌來進行深入分析,蘊含多年開發經驗於其中,代表作有百億級大數據實時計算項目。

開源地址:https://github.com/NewLifeX/X (求star, 864+)

 

前面講解了XCode的各種用法,這一章我們來講講內置的Membership,同時也是XCode的第一標準示例!

 

設計背景

現代管理信息系統絕大部分採用BS架構,無一例外需要用戶角色權限的支持!

結合團隊諸多兄弟姐妹的經驗,設計了一個大小適中的用戶權限系統Membership,目標是滿足80%的使用場景,並具備一定的擴展性。

 

Membership剛開始就採用了角色授權體系,每個用戶只有一種角色,角色擁有菜單資源權限集。

隨着Membership實用性日益增加,2015年初正式合併進入XCode,作為一個模塊存在。

 

2016年第二代魔方NewLife.Cube採用ASP.Net MVC5重構,讓Membership的榮譽達到了鼎峰!

在MVC中,每個Controller就是一個菜單資源,其下的Search/Detail/Insert/Update/Delete等Action作為角色在該菜單資源下的權限子項,保存在角色屬性數據中。

 

2018年為了增強魔方功能,在某些場景下支持單用戶多角色,且兼容已有系統,用戶表增加RoleIDs字段,保存擴展角色,原來的RoleID作為主角色。

 

管理提供者

管理提供者接口 IManageProvider ,提供了Membership基本操作實現。

  1. 當前登錄用戶 GetCurrent、SetCurrent,靜態訪問 ManageProvider.User
  2. 查找用戶 FindByID、FindByName
  3. 註冊登錄註銷 Register、Login、Logout
  4. 當前用戶主機(訪問者IP)ManageProvider.UserHost
  5. IManageProvider 默認由XCode.Membership中的UserX/Role/Menu支持,如若用戶使用自己的用戶權限表,可重新實現該接口

 

用戶權限

用戶 UserX

用戶數據模型:

  <Table Name="User" Description="用戶" RenderGenEntity="true">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="Name" DataType="String" Master="True" Nullable="False" Description="名稱。登錄用戶名" />
      <Column Name="Password" DataType="String" Description="密碼" />
      <Column Name="DisplayName" DataType="String" Description="昵稱" />
      <Column Name="Sex" DataType="Int32" Description="性別。未知、男、女" Type="SexKinds" />
      <Column Name="Mail" DataType="String" Description="郵件" />
      <Column Name="Mobile" DataType="String" Description="手機" />
      <Column Name="Code" DataType="String" Description="代碼。身份證、員工編號等" />
      <Column Name="Avatar" DataType="String" Length="200" Description="頭像" />
      <Column Name="RoleID" DataType="Int32" Description="角色。主要角色" />
      <Column Name="RoleIDs" DataType="String" Length="200" Description="角色組。次要角色集合" />
      <Column Name="DepartmentID" DataType="Int32" Description="部門。組織機構" />
      <Column Name="Online" DataType="Boolean" Description="在線" />
      <Column Name="Enable" DataType="Boolean" Description="啟用" />
      <Column Name="Logins" DataType="Int32" Description="登錄次數" />
      <Column Name="LastLogin" DataType="DateTime" Description="最後登錄" />
      <Column Name="LastLoginIP" DataType="String" Description="最後登錄IP" />
      <Column Name="RegisterTime" DataType="DateTime" Description="註冊時間" />
      <Column Name="RegisterIP" DataType="String" Description="註冊IP" />
      <Column Name="Ex1" DataType="Int32" Description="擴展1" />
      <Column Name="Ex2" DataType="Int32" Description="擴展2" />
      <Column Name="Ex3" DataType="Double" Description="擴展3" />
      <Column Name="Ex4" DataType="String" Description="擴展4" />
      <Column Name="Ex5" DataType="String" Description="擴展5" />
      <Column Name="Ex6" DataType="String" Description="擴展6" />
      <Column Name="UpdateUser" DataType="String" Description="更新用戶" />
      <Column Name="UpdateUserID" DataType="Int32" Description="更新用戶" />
      <Column Name="UpdateIP" DataType="String" Description="更新地址" />
      <Column Name="UpdateTime" DataType="DateTime" Nullable="False" Description="更新時間" />
      <Column Name="Remark" DataType="String" Length="200" Description="備註" />
    </Columns>
    <Indexes>
      <Index Columns="Name" Unique="True" />
      <Index Columns="RoleID" />
      <Index Columns="UpdateTime" />
    </Indexes>
  </Table>

常用字段有ID、用戶名和密碼,登錄註冊相關信息;

角色RoleID、RoleIDs用於實現權限集控制;

部分場景需要郵箱Mail、手機Mobile或者工號Code登錄;

如果仍然不能滿足要求,可以考慮使用Ex1~Ex6等擴展字段。

 

常用功能點:

  1. 初始化時,如果數據表為空,自動插入admin/admin用戶賬號,角色是“管理員”
  2. 支持註冊登錄,使用MD5保存密碼
  3. 支持編號查詢FindByID和名稱查詢FindByName,分別採用了對象緩存和對象從鍵,輕鬆實現百萬級賬號快速查詢
  4. 支持IIdentity接口

 

角色 Role

角色數據模型:

  <Table Name="Role" Description="角色" RenderGenEntity="true">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="Name" DataType="String" Master="True" Nullable="False" Description="名稱" />
      <Column Name="Enable" DataType="Boolean" Description="啟用" />
      <Column Name="IsSystem" DataType="Boolean" Description="系統。用於業務系統開發使用,不受數據權限約束,禁止修改名稱或刪除" />
      <Column Name="Permission" DataType="String" Length="500" Description="權限。對不同資源的權限,逗號分隔,每個資源的權限子項豎線分隔" />
      <Column Name="Ex1" DataType="Int32" Description="擴展1" />
      <Column Name="Ex2" DataType="Int32" Description="擴展2" />
      <Column Name="Ex3" DataType="Double" Description="擴展3" />
      <Column Name="Ex4" DataType="String" Description="擴展4" />
      <Column Name="Ex5" DataType="String" Description="擴展5" />
      <Column Name="Ex6" DataType="String" Description="擴展6" />
      <Column Name="CreateUser" DataType="String" Description="創建用戶" />
      <Column Name="CreateUserID" DataType="Int32" Description="創建用戶" />
      <Column Name="CreateIP" DataType="String" Description="創建地址" />
      <Column Name="CreateTime" DataType="DateTime" Nullable="False" Description="創建時間" />
      <Column Name="UpdateUser" DataType="String" Description="更新用戶" />
      <Column Name="UpdateUserID" DataType="Int32" Description="更新用戶" />
      <Column Name="UpdateIP" DataType="String" Description="更新地址" />
      <Column Name="UpdateTime" DataType="DateTime" Nullable="False" Description="更新時間" />
      <Column Name="Remark" DataType="String" Length="200" Description="備註" />
    </Columns>
    <Indexes>
      <Index Columns="Name" Unique="True" />
    </Indexes>
  </Table>

角色表比較簡單主要是名稱和啟用,以及保存菜單權限數據的Permission

角色支持的操作權限:

    /// <summary>操作權限</summary>
    [Flags]
    [Description("操作權限")]
    public enum PermissionFlags
    {
        /// <summary>無權限</summary>
        [Description("無權限")]
        None = 0,

        /// <summary>查看權限</summary>
        [Description("查看")]
        Detail = 1,

        /// <summary>添加權限</summary>
        [Description("添加")]
        Insert = 2,

        /// <summary>修改權限</summary>
        [Description("修改")]
        Update = 4,

        /// <summary>刪除權限</summary>
        [Description("刪除")]
        Delete = 8,

        /// <summary>所有權限</summary>
        [Description("所有")]
        All = 0xFF,
    }

主要功能點:

  1. 數據表為空時初始化4個基本角色:管理員、高級用戶、普通用戶、遊客
  2. 啟動時角色權限校驗,清理角色中無效的權限項(可能菜單已刪除),以及授權管理員訪問所有角色都無權訪問的新菜單
  3. 支持編號查詢FindByID和名稱查詢FindByID,採用實體緩存,目標系統不會超過1000個角色
  4. 支持權限判斷與設置 Has/Get/Set/Reset 等
  5. 重載實體類 Delete/Save/Update/OnLoad/OnPropertyChanged,加載實體對象時展開權限,保存時合併

 

菜單 Menu

菜單數據模型:

  <Table Name="Menu" Description="菜單" BaseType="EntityTree" RenderGenEntity="true">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="Name" DataType="String" Master="True" Nullable="False" Description="名稱" />
      <Column Name="DisplayName" DataType="String" Description="显示名" />
      <Column Name="FullName" DataType="String" Length="200" Description="全名" />
      <Column Name="ParentID" DataType="Int32" Description="父編號" />
      <Column Name="Url" DataType="String" Length="200" Description="鏈接" />
      <Column Name="Sort" DataType="Int32" Description="排序" />
      <Column Name="Icon" DataType="String" Description="圖標" />
      <Column Name="Visible" DataType="Boolean" Description="可見" />
      <Column Name="Necessary" DataType="Boolean" Description="必要。必要的菜單,必須至少有角色擁有這些權限,如果沒有則自動授權給系統角色" />
      <Column Name="Permission" DataType="String" Length="200" Description="權限子項。逗號分隔,每個權限子項名值豎線分隔" />
      <Column Name="Ex1" DataType="Int32" Description="擴展1" />
      <Column Name="Ex2" DataType="Int32" Description="擴展2" />
      <Column Name="Ex3" DataType="Double" Description="擴展3" />
      <Column Name="Ex4" DataType="String" Description="擴展4" />
      <Column Name="Ex5" DataType="String" Description="擴展5" />
      <Column Name="Ex6" DataType="String" Description="擴展6" />
      <Column Name="CreateUser" DataType="String" Description="創建用戶" />
      <Column Name="CreateUserID" DataType="Int32" Description="創建用戶" />
      <Column Name="CreateIP" DataType="String" Description="創建地址" />
      <Column Name="CreateTime" DataType="DateTime" Nullable="False" Description="創建時間" />
      <Column Name="UpdateUser" DataType="String" Description="更新用戶" />
      <Column Name="UpdateUserID" DataType="Int32" Description="更新用戶" />
      <Column Name="UpdateIP" DataType="String" Description="更新地址" />
      <Column Name="UpdateTime" DataType="DateTime" Nullable="False" Description="更新時間" />
      <Column Name="Remark" DataType="String" Length="200" Description="備註" />
    </Columns>
    <Indexes>
      <Index Columns="Name" />
      <Index Columns="ParentID,Name" Unique="True" />
    </Indexes>
  </Table>

菜單實體類採用樹形實體基類 EntityTree ,通過 ParentID 實現上下級關聯,同級 ParentID+Name 唯一

 

主要功能點:

  1. 支持自動掃描Controller作為菜單,因此魔方只需要增加Controller,即可在菜單表看到新頁面
  2. 實體樹適用於1000行以內樹形數據表,一次性加載數據到內存,在內存中根據ParentID構造實體對象樹,最常用樹形是Parent/Childs

 

日誌統計

日誌 Log

數據模型:

  <Table Name="Log" Description="日誌" ConnName="Log" RenderGenEntity="true">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="Category" DataType="String" Description="類別" />
      <Column Name="Action" DataType="String" Description="操作" />
      <Column Name="LinkID" DataType="Int32" Description="鏈接" />
      <Column Name="UserName" DataType="String" Description="用戶名" />
      <Column Name="Ex1" DataType="Int32" Description="擴展1" />
      <Column Name="Ex2" DataType="Int32" Description="擴展2" />
      <Column Name="Ex3" DataType="Double" Description="擴展3" />
      <Column Name="Ex4" DataType="String" Description="擴展4" />
      <Column Name="Ex5" DataType="String" Description="擴展5" />
      <Column Name="Ex6" DataType="String" Description="擴展6" />
      <Column Name="CreateUser" DataType="String" Description="創建用戶" />
      <Column Name="CreateUserID" DataType="Int32" Description="用戶編號" />
      <Column Name="CreateIP" DataType="String" Description="IP地址" />
      <Column Name="CreateTime" DataType="DateTime" Nullable="False" Description="時間" />
      <Column Name="Remark" DataType="String" Length="500" Description="詳細信息" />
    </Columns>
    <Indexes>
      <Index Columns="Category" />
      <Index Columns="CreateUserID" />
      <Index Columns="CreateTime" />
    </Indexes>
  </Table>

日誌表記錄分類、操作和日誌內容。

主要功能點:

  1. 日誌提供者LogProvider,提供了唯一核心方法 WriteLog,默認實現就是寫該日誌表。可從對象容器取得日誌提供者 ObjectContainer.Resolve<LogProvider>()
  2. 從IManageProvider接口獲取當前登錄用戶以及遠程訪問IP寫入日誌相應字段

 

在線 UserOnline

數據模型:

  <Table Name="UserOnline" Description="用戶在線" ConnName="Log">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="UserID" DataType="Int32" Description="用戶" />
      <Column Name="Name" DataType="String" Master="True" Description="名稱" />
      <Column Name="SessionID" DataType="String" Description="會話。Web的SessionID或Server的會話編號" />
      <Column Name="Times" DataType="Int32" Description="次數" />
      <Column Name="Page" DataType="String" Description="頁面" />
      <Column Name="Status" DataType="String" Length="200" Description="狀態" />
      <Column Name="OnlineTime" DataType="Int32" Description="在線時間。本次在線總時間,秒" />
      <Column Name="CreateIP" DataType="String" Description="創建地址" />
      <Column Name="CreateTime" DataType="DateTime" Nullable="False" Description="創建時間" />
      <Column Name="UpdateTime" DataType="DateTime" Nullable="False" Description="修改時間" />
    </Columns>
    <Indexes>
      <Index Columns="UserID" />
      <Index Columns="SessionID" />
      <Index Columns="CreateTime" />
    </Indexes>
  </Table>

藉助用戶行為模塊 UserBehaviorModule , 維護用戶在線記錄,持久化在 UserOnline 表

 

訪問統計 VisitStat

  <Table Name="VisitStat" Description="訪問統計" ConnName="Log">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="Level" DataType="Int32" Description="層級" Type="XCode.Statistics.StatLevels" />
      <Column Name="Time" DataType="DateTime" Description="時間" />
      <Column Name="Page" DataType="String" Nullable="False" Description="頁面" />
      <Column Name="Title" DataType="String" Master="True" Description="標題" />
      <Column Name="Times" DataType="Int32" Description="次數" />
      <Column Name="Users" DataType="Int32" Description="用戶" />
      <Column Name="IPs" DataType="Int32" Description="IP" />
      <Column Name="Error" DataType="Int32" Description="錯誤" />
      <Column Name="Cost" DataType="Int32" Description="耗時。毫秒" />
      <Column Name="MaxCost" DataType="Int32" Description="最大耗時。毫秒" />
      <Column Name="CreateTime" DataType="DateTime" Nullable="False" Description="創建時間" />
      <Column Name="UpdateTime" DataType="DateTime" Nullable="False" Description="更新時間" />
      <Column Name="Remark" DataType="String" Length="5000" Description="詳細信息" />
    </Columns>
    <Indexes>
      <Index Columns="Page,Level,Time" Unique="True" />
      <Index Columns="Level,Time" />
    </Indexes>
  </Table>

藉助用戶行為模塊 UserBehaviorModule , 維護用戶訪問記錄,寫入日誌表,並寫入訪問統計表。

主要功能要點:

  1. 記錄頁面訪問統計,簡單支持IP數和用戶數
  2. 支持年月日三級統計,作為XCode日期統計表的標準示例

 

其它

部門 Department

數據模型:

  <Table Name="Department" Description="部門。組織機構,多級樹狀結構" BaseType="EntityTree" RenderGenEntity="true">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="Code" DataType="String" Description="代碼" />
      <Column Name="Name" DataType="String" Master="True" Nullable="False" Description="名稱" />
      <Column Name="FullName" DataType="String" Length="200" Description="全名" />
      <Column Name="ParentID" DataType="Int32" Description="父級" />
      <Column Name="Level" DataType="Int32" Description="層級。樹狀結構的層級" />
      <Column Name="Sort" DataType="Int32" Description="排序。同級內排序" />
      <Column Name="Enable" DataType="Boolean" Description="啟用" />
      <Column Name="Visible" DataType="Boolean" Description="可見" />
      <Column Name="Ex1" DataType="Int32" Description="擴展1" />
      <Column Name="Ex2" DataType="Int32" Description="擴展2" />
      <Column Name="Ex3" DataType="Double" Description="擴展3" />
      <Column Name="Ex4" DataType="String" Description="擴展4" />
      <Column Name="Ex5" DataType="String" Description="擴展5" />
      <Column Name="Ex6" DataType="String" Description="擴展6" />
      <Column Name="CreateUser" DataType="String" Description="創建用戶" />
      <Column Name="CreateUserID" DataType="Int32" Description="創建用戶" />
      <Column Name="CreateIP" DataType="String" Description="創建地址" />
      <Column Name="CreateTime" DataType="DateTime" Nullable="False" Description="創建時間" />
      <Column Name="UpdateUser" DataType="String" Description="更新用戶" />
      <Column Name="UpdateUserID" DataType="Int32" Description="更新用戶" />
      <Column Name="UpdateIP" DataType="String" Description="更新地址" />
      <Column Name="UpdateTime" DataType="DateTime" Nullable="False" Description="更新時間" />
      <Column Name="Remark" DataType="String" Length="200" Description="備註" />
    </Columns>
    <Indexes>
      <Index Columns="Name" />
      <Index Columns="ParentID,Name" Unique="True" />
      <Index Columns="Code" />
      <Index Columns="UpdateTime" />
    </Indexes>
  </Table>

 

 

字典參數 Parameter

數據模型:

  <Table Name="Parameter" Description="字典參數">
    <Columns>
      <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />
      <Column Name="Category" DataType="String" Description="類別" />
      <Column Name="Name" DataType="String" Master="True" Description="名稱" />
      <Column Name="Value" DataType="String" Length="200" Description="數值" />
      <Column Name="LongValue" DataType="String" Length="2000" Description="長數值" />
      <Column Name="Kind" DataType="Int32" Description="種類。0普通,21列表,22名值" Type="XCode.Membership.ParameterKinds" />
      <Column Name="Enable" DataType="Boolean" Description="啟用" />
      <Column Name="Ex1" DataType="Int32" Description="擴展1" />
      <Column Name="Ex2" DataType="Int32" Description="擴展2" />
      <Column Name="Ex3" DataType="Double" Description="擴展3" />
      <Column Name="Ex4" DataType="String" Description="擴展4" />
      <Column Name="Ex5" DataType="String" Description="擴展5" />
      <Column Name="Ex6" DataType="String" Description="擴展6" />
      <Column Name="CreateUser" DataType="String" Description="創建用戶" />
      <Column Name="CreateUserID" DataType="Int32" Description="創建用戶" />
      <Column Name="CreateIP" DataType="String" Description="創建地址" />
      <Column Name="CreateTime" DataType="DateTime" Nullable="False" Description="創建時間" />
      <Column Name="UpdateUser" DataType="String" Description="更新用戶" />
      <Column Name="UpdateUserID" DataType="Int32" Description="更新用戶" />
      <Column Name="UpdateIP" DataType="String" Description="更新地址" />
      <Column Name="UpdateTime" DataType="DateTime" Nullable="False" Description="更新時間" />
      <Column Name="Remark" DataType="String" Length="200" Description="備註" />
    </Columns>
    <Indexes>
      <Index Columns="Category,Name" Unique="True" />
      <Index Columns="Name" />
      <Index Columns="UpdateTime" />
    </Indexes>
  </Table>

 

 

系列教程

NewLife.XCode教程系列[2019版]

  1. 增刪改查入門。快速展現用法,代碼配置連接字符串
  2. 數據模型文件。建立表格字段和索引,名字以及數據類型規範,推薦字段(時間,用戶,IP)
  3. 實體類詳解。數據類業務類,泛型基類,接口
  4. 功能設置。連接字符串,調試開關,SQL日誌,慢日誌,參數化,執行超時。代碼與配置文件設置,連接字符串局部設置
  5. 反向工程。自動建立數據庫數據表
  6. 數據初始化。InitData寫入初始化數據
  7. 高級增刪改。重載攔截,自增字段,Valid驗證,實體模型(時間,用戶,IP)
  8. 臟數據。如何產生,怎麼利用
  9. 增量累加。高併發統計
  10. 事務處理。單表和多表,不同連接,多種寫法
  11. 擴展屬性。多表關聯,Map映射
  12. 高級查詢。複雜條件,分頁,自定義擴展FieldItem,查總記錄數,查匯總統計
  13. 數據層緩存。Sql緩存,更新機制
  14. 實體緩存。全表整理緩存,更新機制
  15. 對象緩存。字典緩存,適用用戶等數據較多場景。
  16. 百億級性能。字段精鍊,索引完備,合理查詢,充分利用緩存
  17. 實體工廠。元數據,通用處理程序
  18. 角色權限。Membership
  19. 導入導出。Xml,Json,二進制,網絡或文件
  20. 分表分庫。常見拆分邏輯
  21. 高級統計。聚合統計,分組統計
  22. 批量寫入。批量插入,批量Upsert,異步保存
  23. 實體隊列。寫入級緩存,提升性能。
  24. 備份同步。備份數據,恢複數據,同步數據
  25. 數據服務。提供RPC接口服務,遠程執行查詢,例如SQLite網絡版
  26. 大數據分析。ETL抽取,調度計算處理,結果持久化

【精選推薦文章】

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

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

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

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

使用ASP.NET Web API和Web API Client Gen使Angular 2應用程序的開發更加高效

本文介紹“ 為ASP.NET Web API生成TypeScript客戶端API ”,重點介紹Angular 2+代碼示例和各自的SDLC。如果您正在開發.NET Core Web API後端,則可能需要閱讀為ASP.NET Core Web API生成C#Client API。

背景

WebApiClientGenAngular 2仍然在RC2時,2016年6月v1.9.0-beta 以來,對Angular2的支持已經可用並且在WebApiClientGenv2.0中提供了對Angular 2產品發布的支持希望NG2的發展不會如此頻繁地破壞我的CodeGen和我的Web前端應用程序。🙂

在2016年9月底發布Angular 2的第一個產品發布幾周后,我碰巧啟動了一個使用Angular2的主要Web應用程序項目,因此我WebApiClientGen對NG2應用程序開發的使用方法幾乎相同

推定

  1. 您正在開發ASP.NET Web API 2.x應用程序,並將基於Angular 2+為SPA開發TypeScript庫。
  2. 您和其他開發人員喜歡在服務器端和客戶端都通過強類型數據和函數進行高度抽象。
  3. Web API和實體框架代碼優先使用POCO類,您可能不希望將所有數據類和成員發布到客戶端程序源碼

並且可選地,如果您或您的團隊支持基於Trunk的開發,那麼更好,因為使用的設計WebApiClientGen和工作流程WebApiClientGen假設基於Trunk的開發,這比其他分支策略(如Feature Branching和GitFlow等)更有效。對於熟練掌握TDD的團隊。

為了跟進這種開發客戶端程序的新方法,最好有一個ASP.NET Web API項目。您可以使用現有項目,也可以創建演示項目

使用代碼

本文重點介紹Angular 2+的代碼示例。假設您有一個ASP.NET Web API項目和一個Angular2項目作為VS解決方案中的兄弟項目。如果你將它們分開,那麼為了使開發步驟無縫地編寫腳本應該不難。

我認為您已閱讀“ 為ASP.NET Web API生成TypeScript客戶端API ”。為jQuery生成客戶端API的步驟幾乎與為Angular 2生成客戶端API的步驟相同。演示TypeScript代碼基於TUTORIAL:TOUR OF HEROES,許多人從中學習了Angular2。因此,您將能夠看到如何WebApiClientGen適應並改進Angular2應用程序的典型開發周期。

這是Web API代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Runtime.Serialization;
using System.Collections.Concurrent;

namespace DemoWebApi.Controllers
{
    [RoutePrefix("api/Heroes")]
    public class HeroesController : ApiController
    {
        public Hero[] Get()
        {
            return HeroesData.Instance.Dic.Values.ToArray();
        }

        public Hero Get(long id)
        {
            Hero r;
            HeroesData.Instance.Dic.TryGetValue(id, out r);
            return r;
        }

        public void Delete(long id)
        {
            Hero r;
            HeroesData.Instance.Dic.TryRemove(id, out r);
        }

        public Hero Post(string name)
        {
            var max = HeroesData.Instance.Dic.Keys.Max();
            var hero = new Hero { Id = max + 1, Name = name };
            HeroesData.Instance.Dic.TryAdd(max + 1, hero);
            return hero;
        }

        public Hero Put(Hero hero)
        {
            HeroesData.Instance.Dic[hero.Id] = hero;
            return hero;
        }

        [HttpGet]
        public Hero[] Search(string name)
        {
            return HeroesData.Instance.Dic.Values.Where(d => d.Name.Contains(name)).ToArray();
        }          
    }

    [DataContract(Namespace = DemoWebApi.DemoData.Constants.DataNamespace)]
    public class Hero
    {
        [DataMember]
        public long Id { get; set; }

        [DataMember]
        public string Name { get; set; }
    }

    public sealed class HeroesData
    {
        private static readonly Lazy<HeroesData> lazy =
            new Lazy<HeroesData>(() => new HeroesData());

        public static HeroesData Instance { get { return lazy.Value; } }

        private HeroesData()
        {
            Dic = new ConcurrentDictionary<long, Hero>(new KeyValuePair<long, Hero>[] {
                new KeyValuePair<long, Hero>(11, new Hero {Id=11, Name="Mr. Nice" }),
                new KeyValuePair<long, Hero>(12, new Hero {Id=12, Name="Narco" }),
                new KeyValuePair<long, Hero>(13, new Hero {Id=13, Name="Bombasto" }),
                new KeyValuePair<long, Hero>(14, new Hero {Id=14, Name="Celeritas" }),
                new KeyValuePair<long, Hero>(15, new Hero {Id=15, Name="Magneta" }),
                new KeyValuePair<long, Hero>(16, new Hero {Id=16, Name="RubberMan" }),
                new KeyValuePair<long, Hero>(17, new Hero {Id=17, Name="Dynama" }),
                new KeyValuePair<long, Hero>(18, new Hero {Id=18, Name="Dr IQ" }),
                new KeyValuePair<long, Hero>(19, new Hero {Id=19, Name="Magma" }),
                new KeyValuePair<long, Hero>(20, new Hero {Id=29, Name="Tornado" }),

                });
        }

        public ConcurrentDictionary<long, Hero> Dic { get; private set; }
    }
}

 

步驟0:將NuGet包WebApiClientGen安裝到Web API項目

安裝還將安裝依賴的NuGet包Fonlow.TypeScriptCodeDOMFonlow.Poco2Ts項目引用。

此外,用於觸發CodeGen的CodeGenController.cs被添加到Web API項目的Controllers文件夾中。

CodeGenController只在調試版本開發過程中應該是可用的,因為客戶端API應該用於Web API的每個版本生成一次。

提示

如果您正在使用@ angular / http中定義的Angular2的Http服務,那麼您應該使用WebApiClientGenv2.2.5。如果您使用的HttpClient是@ angular / common / http中定義的Angular 4.3中可用服務,並且在Angular 5中已棄用,那麼您應該使用WebApiClientGenv2.3.0。

第1步:準備JSON配置數據

下面的JSON配置數據是POSTCodeGen Web API:

{
    "ApiSelections": {
        "ExcludedControllerNames": [
            "DemoWebApi.Controllers.Account"
        ],

        "DataModelAssemblyNames": [
            "DemoWebApi.DemoData",
            "DemoWebApi"
        ],
        "CherryPickingMethods": 1
    },

    "ClientApiOutputs": {
        "ClientLibraryProjectFolderName": "DemoWebApi.ClientApi",
        "GenerateBothAsyncAndSync": true,

        "CamelCase": true,
        "TypeScriptNG2Folder": "..\\DemoAngular2\\clientapi",
        "NGVersion" : 5

    }
}

 

提示

Angular 6正在使用RxJS v6,它引入了一些重大變化,特別是對於導入Observable默認情況下,WebApiClientGen2.4和更高版本默認將導入聲明為import { Observable } from 'rxjs';  。如果您仍在使用Angular 5.x,則需要"NGVersion" : 5在JSON配置中聲明,因此生成的代碼中的導入將是更多詳細信息,import { Observable } from 'rxjs/Observable'; . 請參閱RxJS v5.x至v6更新指南RxJS:版本6的TSLint規則

備註

您應確保“ TypeScriptNG2Folder”存在的文件夾存在,因為WebApiClientGen不會為您創建此文件夾,這是設計使然。

建議到JSON配置數據保存到與文件類似的這一個位於Web API項目文件夾。

如果您在Web API項目中定義了所有POCO類,則應將Web API項目的程序集名稱放在“ DataModelAssemblyNames” 數組中如果您有一些專用的數據模型程序集可以很好地分離關注點,那麼您應該將相應的程序集名稱放入數組中。您可以選擇為jQuery或NG2或C#客戶端API代碼生成TypeScript客戶端API代碼,或者全部三種。

“ TypeScriptNG2Folder”是Angular2項目的絕對路徑或相對路徑。例如,“ .. \\ DemoAngular2 \\ ClientApi ”表示DemoAngular2作為Web API項目的兄弟項目創建的Angular 2項目“ ”。

CodeGen根據“從POCO類生成強類型打字稿接口CherryPickingMethods,其在下面的文檔註釋描述”:

/// <summary>
/// Flagged options for cherry picking in various development processes.
/// </summary>
[Flags]
public enum CherryPickingMethods
{
    /// <summary>
    /// Include all public classes, properties and properties.
    /// </summary>
    All = 0,

    /// <summary>
    /// Include all public classes decorated by DataContractAttribute,
    /// and public properties or fields decorated by DataMemberAttribute.
    /// And use DataMemberAttribute.IsRequired
    /// </summary>
    DataContract =1,

    /// <summary>
    /// Include all public classes decorated by JsonObjectAttribute,
    /// and public properties or fields decorated by JsonPropertyAttribute.
    /// And use JsonPropertyAttribute.Required
    /// </summary>
    NewtonsoftJson = 2,

    /// <summary>
    /// Include all public classes decorated by SerializableAttribute,
    /// and all public properties or fields
    /// but excluding those decorated by NonSerializedAttribute.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    Serializable = 4,

    /// <summary>
    /// Include all public classes, properties and properties.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    AspNet = 8,
}

 

默認選項是DataContract選擇加入。您可以使用任何方法或組合方法。

第2步:運行Web API項目的DEBUG構建

步驟3:POST JSON配置數據以觸發客戶端API代碼的生成

在IIS Express上的IDE中運行Web項目。

然後使用CurlPoster或任何您喜歡的客戶端工具POST到http:// localhost:10965 / api / CodeGen,with content-type=application/json

提示

基本上,每當Web API更新時,您只需要步驟2來生成客戶端API,因為您不需要每次都安裝NuGet包或創建新的JSON配置數據。

編寫一些批處理腳本來啟動Web API和POST JSON配置數據應該不難。為了您的方便,我實際起草了一個:Powershell腳本文件CreateClientApi.ps1,它在IIS Express上啟動Web(API)項目,然後發布JSON配置文件以觸發代碼生成

基本上,您可以製作Web API代碼,包括API控制器和數據模型,然後執行CreateClientApi.ps1而已!WebApiClientGenCreateClientApi.ps1將為您完成剩下的工作。

發布客戶端API庫

現在您在TypeScript中生成了客戶端API,類似於以下示例:

import { Injectable, Inject } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
export namespace DemoWebApi_DemoData_Client {
    export enum AddressType {Postal, Residential}

    export enum Days {Sat=1, Sun=2, Mon=3, Tue=4, Wed=5, Thu=6, Fri=7}

    export interface PhoneNumber {
        fullNumber?: string;
        phoneType?: DemoWebApi_DemoData_Client.PhoneType;
    }

    export enum PhoneType {Tel, Mobile, Skype, Fax}

    export interface Address {
        id?: string;
        street1?: string;
        street2?: string;
        city?: string;
        state?: string;
        postalCode?: string;
        country?: string;
        type?: DemoWebApi_DemoData_Client.AddressType;
        location?: DemoWebApi_DemoData_Another_Client.MyPoint;
    }

    export interface Entity {
        id?: string;
        name: string;
        addresses?: Array<DemoWebApi_DemoData_Client.Address>;
        phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
    }

    export interface Person extends DemoWebApi_DemoData_Client.Entity {
        surname?: string;
        givenName?: string;
        dob?: Date;
    }

    export interface Company extends DemoWebApi_DemoData_Client.Entity {
        businessNumber?: string;
        businessNumberType?: string;
        textMatrix?: Array<Array<string>>;
        int2DJagged?: Array<Array<number>>;
        int2D?: number[][];
        lines?: Array<string>;
    }

    export interface MyPeopleDic {
        dic?: {[id: string]: DemoWebApi_DemoData_Client.Person };
        anotherDic?: {[id: string]: string };
        intDic?: {[id: number]: string };
    }
}

export namespace DemoWebApi_DemoData_Another_Client {
    export interface MyPoint {
        x: number;
        y: number;
    }

}

export namespace DemoWebApi_Controllers_Client {
    export interface FileResult {
        fileNames?: Array<string>;
        submitter?: string;
    }

    export interface Hero {
        id?: number;
        name?: string;
    }
}

   @Injectable()
    export class Heroes {
        constructor(@Inject('baseUri') private baseUri: string = location.protocol + '//' + 
        location.hostname + (location.port ? ':' + location.port : '') + '/', private http: Http){
        }

        /**
         * Get all heroes.
         * GET api/Heroes
         * @return {Array<DemoWebApi_Controllers_Client.Hero>}
         */
        get(): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
            return this.http.get(this.baseUri + 'api/Heroes').map(response=> response.json());
        }

        /**
         * Get a hero.
         * GET api/Heroes/{id}
         * @param {number} id
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        getById(id: number): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.get(this.baseUri + 'api/Heroes/'+id).map(response=> response.json());
        }

        /**
         * DELETE api/Heroes/{id}
         * @param {number} id
         * @return {void}
         */
        delete(id: number): Observable<Response>{
            return this.http.delete(this.baseUri + 'api/Heroes/'+id);
        }

        /**
         * Add a hero
         * POST api/Heroes?name={name}
         * @param {string} name
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        post(name: string): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.post(this.baseUri + 'api/Heroes?name='+encodeURIComponent(name), 
            JSON.stringify(null), { headers: new Headers({ 'Content-Type': 
            'text/plain;charset=UTF-8' }) }).map(response=> response.json());
        }

        /**
         * Update hero.
         * PUT api/Heroes
         * @param {DemoWebApi_Controllers_Client.Hero} hero
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        put(hero: DemoWebApi_Controllers_Client.Hero): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.put(this.baseUri + 'api/Heroes', JSON.stringify(hero), 
            { headers: new Headers({ 'Content-Type': 'text/plain;charset=UTF-8' 
            }) }).map(response=> response.json());
        }

        /**
         * Search heroes
         * GET api/Heroes?name={name}
         * @param {string} name keyword contained in hero name.
         * @return {Array<DemoWebApi_Controllers_Client.Hero>} Hero array matching the keyword.
         */
        search(name: string): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
            return this.http.get(this.baseUri + 'api/Heroes?name='+
            encodeURIComponent(name)).map(response=> response.json());
        }
    }

 

提示

如果您希望生成的TypeScript代碼符合JavaScript和JSON的camel大小寫,則可以在WebApiConfigWeb API的腳手架代碼添加以下行

config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = 
            new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

然後屬性名稱和函數名稱將在camel大小寫中,前提是C#中的相應名稱都在Pascal大小寫中。有關詳細信息,請查看camelCasing或PascalCasing

客戶端應用編程

在像Visual Studio這樣的正常文本編輯器中編寫客戶端代碼時,您可能會獲得很好的智能感知。

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import * as namespaces from '../clientapi/WebApiNG2ClientAuto';
import DemoWebApi_Controllers_Client = namespaces.DemoWebApi_Controllers_Client;

@Component({
    moduleId: module.id,
    selector: 'my-heroes',
    templateUrl: 'heroes.component.html',
    styleUrls: ['heroes.component.css']
})

 

通過IDE進行設計時類型檢查,並在生成的代碼之上進行編譯時類型檢查,可以更輕鬆地提高客戶端編程的效率和產品質量。

不要做計算機可以做的事情,讓計算機為我們努力工作。我們的工作是為客戶提供自動化解決方案,因此最好先自行完成自己的工作。

興趣點

在典型的角2個教程,包括官方的一個  這已經存檔,作者經常督促應用程序開發者製作一個服務類,如“ HeroService”,而黃金法則是:永遠委託給配套服務類的數據訪問

WebApiClientGen為您生成此服務類DemoWebApi_Controllers_Client.Heroes,它將使用真正的Web API而不是內存中的Web API。在開發過程中WebApiClientGen,我創建了一個演示項目DemoAngular2各自用於測試的Web API控制器

典型的教程還建議使用模擬服務進行單元測試。WebApiClientGen使用真正的Web API服務要便宜得多,因此您可能不需要創建模擬服務。您應該在開發期間平衡使用模擬或實際服務的成本/收益,具體取決於您的上下文。通常,如果您的團隊已經能夠在每台開發機器中使用持續集成環境,那麼使用真實服務運行測試可能非常無縫且快速。

在典型的SDLC中,在初始設置之後,以下是開發Web API和NG2應用程序的典型步驟:

  1. 升級Web API
  2. 運行CreateClientApi.ps1以更新TypeScript for NG2中的客戶端API。
  3. 使用生成的TypeScript客戶端API代碼或C#客戶端API代碼,在Web API更新時創建新的集成測試用例。
  4. 相應地修改NG2應用程序。
  5. 要進行測試,請運行StartWebApi.ps1以啟動Web API,並在VS IDE中運行NG2應用程序。

提示

對於第5步,有其他選擇。例如,您可以使用VS IDE同時以調試模式啟動Web API和NG2應用程序。一些開發人員可能更喜歡使用“ npm start”。

本文最初是為Angular 2編寫的,具有Http服務。Angular 4.3中引入了WebApiClientGen2.3.0支持HttpClient並且生成的API在接口級別保持不變。這使得從過時的Http服務遷移到HttpClient服務相當容易或無縫,與Angular應用程序編程相比,不使用生成的API而是直接使用Http服務。

順便說一句,如果你沒有完成向Angular 5的遷移,那麼這篇文章可能有所幫助:  升級到Angular 5和HttpClient如果您使用的是Angular 6,則應使用WebApiClientGen2.4.0+。

【精選推薦文章】

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

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

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

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

HBase 系統架構及數據結構

一、基本概念

一個典型的Hbase Table 表如下:

1.1 Row Key (行鍵)

Row Key是用來檢索記錄的主鍵。想要訪問HBase Table中的數據,只有以下三種方式:

  • 通過指定的Row Key進行訪問;
  • 通過Row Key的range進行訪問,即訪問指定範圍內的行;
  • 進行全表掃描。

Row Key可以是任意字符串,存儲時數據按照Row Key的字典序進行排序。這裏需要注意以下兩點:

  • 因為字典序對Int排序的結果是1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。如果你使用整型的字符串作為行鍵,那麼為了保持整型的自然序,行鍵必須用0作左填充。
  • 行的一次讀寫操作時原子性的 (不論一次讀寫多少列)。

1.2 Column Family(列族)

HBase表中的每個列,都歸屬於某個列族。列族是表的Schema的一部分,所以列族需要在創建表時進行定義。列族的所有列都以列族名作為前綴,例如courses:historycourses:math都屬於courses這個列族。

1.3 Column Qualifier (列限定符)

列限定符,你可以理解為是具體的列名,例如courses:historycourses:math都屬於courses這個列族,它們的列限定符分別是historymath。需要注意的是列限定符不是表Schema的一部分,你可以在插入數據的過程中動態創建列。

1.4 Column(列)

HBase中的列由列族和列限定符組成,它們由:(冒號)進行分隔,即一個完整的列名應該表述為列族名 :列限定符

1.5 Cell

Cell是行,列族和列限定符的組合,並包含值和時間戳。你可以等價理解為關係型數據庫中由指定行和指定列確定的一個單元格,但不同的是HBase中的一個單元格是由多個版本的數據組成的,每個版本的數據用時間戳進行區分。

1.6 Timestamp(時間戳)

HBase 中通過row keycolumn確定的為一個存儲單元稱為Cell。每個Cell都保存着同一份數據的多個版本。版本通過時間戳來索引,時間戳的類型是 64位整型,時間戳可以由HBase在數據寫入時自動賦值,也可以由客戶顯式指定。每個Cell中,不同版本的數據按照時間戳倒序排列,即最新的數據排在最前面。

二、存儲結構

2.1 Regions

HBase Table中的所有行按照Row Key的字典序排列。HBase Tables 通過行鍵的範圍(row key range)被水平切分成多個Region, 一個Region包含了在start key 和 end key之間的所有行。

每個表一開始只有一個Region,隨着數據不斷增加,Region會不斷增大,當增大到一個閥值的時候,Region就會等分為兩個新的Region。當Table中的行不斷增多,就會有越來越多的Region

Region是HBase中分佈式存儲和負載均衡的最小單元。這意味着不同的Region可以分佈在不同的Region Server上。但一個Region是不會拆分到多個Server上的。

2.2 Region Server

Region Server運行在HDFS的DataNode上。它具有以下組件:

  • WAL(Write Ahead Log,預寫日誌):用於存儲尚未進持久化存儲的數據記錄,以便在發生故障時進行恢復。
  • BlockCache:讀緩存。它將頻繁讀取的數據存儲在內存中,如果存儲不足,它將按照最近最少使用原則清除多餘的數據。
  • MemStore:寫緩存。它存儲尚未寫入磁盤的新數據,並會在數據寫入磁盤之前對其進行排序。每個Region上的每個列族都有一個MemStore。
  • HFile :將行數據按照Key\Values的形式存儲在文件系統上。

Region Server存取一個子表時,會創建一個Region對象,然後對錶的每個列族創建一個Store實例,每個Store會有 0 個或多個StoreFile與之對應,每個StoreFile則對應一個HFile,HFile 就是實際存儲在HDFS上的文件。

三、Hbase系統架構

3.1 系統架構

HBase系統遵循Master/Salve架構,由三種不同類型的組件組成:

Zookeeper

  1. 保證任何時候,集群中只有一個Master;
  2. 存貯所有Region的尋址入口;
  3. 實時監控Region Server的狀態,將Region Server的上線和下線信息實時通知給Master;
  4. 存儲HBase的Schema,包括有哪些Table,每個Table有哪些Column Family等信息。

Master

  1. 為Region Server分配Region ;
  2. 負責Region Server的負載均衡 ;
  3. 發現失效的Region Server並重新分配其上的Region;
  4. GFS上的垃圾文件回收;
  5. 處理Schema的更新請求。

Region Server

  1. Region Server負責維護Master分配給它的Region ,並處理髮送到Region上的IO請求;
  2. Region Server負責切分在運行過程中變得過大的Region。

3.2 組件間的協作

HBase使用ZooKeeper作為分佈式協調服務來維護集群中的服務器狀態。 Zookeeper負責維護可用服務列表,並提供服務故障通知等服務:

  • 每個Region Server都會在ZooKeeper上創建一個臨時節點,Master通過Zookeeper的Watcher機制對節點進行監控,從而可以發現新加入的Region Server或故障退出的Region Server;
  • 所有Masters會競爭性地在Zookeeper上創建同一個臨時節點,由於Zookeeper只能有一個同名節點,所以必然只有一個Master能夠創建成功,此時該Master就是主Master,主Master會定期向Zookeeper發送心跳。備用Masters則通過Watcher機制對主HMaster所在節點進行監聽;
  • 如果主Master未能定時發送心跳,則其持有的Zookeeper會話會過期,相應的臨時節點也會被刪除,這會觸發定義在該節點上的Watcher事件,使得備用的Master Servers得到通知。所有備用的Master Servers在接到通知后,會再次去競爭性地創建臨時節點,完成主Master的選舉。

四、數據的讀寫流程簡述

4.1 寫入數據的流程

  1. Client向Region Server提交寫請求;
  2. Region Server找到目標Region;
  3. Region檢查數據是否與Schema一致;
  4. 如果客戶端沒有指定版本,則獲取當前系統時間作為數據版本;
  5. 將更新寫入WAL Log;
  6. 將更新寫入Memstore;
  7. 判斷Memstore存儲是否已滿,如果存儲已滿則需要flush為Store Hfile文件。

更為詳細寫入流程可以參考:HBase - 數據寫入流程解析

4.2 讀取數據的流程

以下是客戶端首次讀寫HBase上數據的流程:

  1. 客戶端從Zookeeper獲取META表所在的Region Server;
  2. 客戶端訪問META表所在的Region Server,從META表中查詢到訪問行鍵所在的Region Server,之後客戶端將緩存這些信息以及META表的位置;
  3. 客戶端從行鍵所在的Region Server上獲取數據。

如果再次讀取,客戶端將從緩存中獲取行鍵所在的Region Server。這樣客戶端就不需要再次查詢META表,除非Region移動導致緩存失效,這樣的話,則將會重新查詢並更新緩存。

注:META表是HBase中一張特殊的表,它保存了所有Region的位置信息,META表自己的位置信息則存儲在ZooKeeper上。

更為詳細讀取數據流程參考:

HBase原理-數據讀取流程解析

HBase原理-遲到的‘數據讀取流程部分細節

參考資料

本篇文章內容主要參考自官方文檔和以下兩篇博客,圖片也主要引用自以下兩篇博客:

  • HBase Architectural Components
  • Hbase系統架構及數據結構

官方文檔:

  • Apache HBase ™ Reference Guide

更多大數據系列文章可以參見個人 GitHub 開源項目: 大數據入門指南

【精選推薦文章】

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

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

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

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

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

死磕 java同步系列之StampedLock源碼解析

問題

(1)StampedLock是什麼?

(2)StampedLock具有什麼特性?

(3)StampedLock是否支持可重入?

(4)StampedLock與ReentrantReadWriteLock的對比?

簡介

StampedLock是java8中新增的類,它是一個更加高效的讀寫鎖的實現,而且它不是基於AQS來實現的,它的內部自成一片邏輯,讓我們一起來學習吧。

StampedLock具有三種模式:寫模式、讀模式、樂觀讀模式。

ReentrantReadWriteLock中的讀和寫都是一種悲觀鎖的體現,StampedLock加入了一種新的模式——樂觀讀,它是指當樂觀讀時假定沒有其它線程修改數據,讀取完成后再檢查下版本號有沒有變化,沒有變化就讀取成功了,這種模式更適用於讀多寫少的場景。

使用方法

讓我們通過下面的例子了解一下StampedLock三種模式的使用方法:

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) {
        // 獲取寫鎖,返回一個版本號(戳)
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            // 釋放寫鎖,需要傳入上面獲取的版本號
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() {
        // 樂觀讀
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        // 驗證版本號是否有變化
        if (!sl.validate(stamp)) {
            // 版本號變了,樂觀讀轉悲觀讀
            stamp = sl.readLock();
            try {
                // 重新讀取x、y的值
                currentX = x;
                currentY = y;
            } finally {
                // 釋放讀鎖,需要傳入上面獲取的版本號
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    void moveIfAtOrigin(double newX, double newY) {
        // 獲取悲觀讀鎖
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                // 轉為寫鎖
                long ws = sl.tryConvertToWriteLock(stamp);
                // 轉換成功
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                }
                else {
                    // 轉換失敗
                    sl.unlockRead(stamp);
                    // 獲取寫鎖
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // 釋放鎖
            sl.unlock(stamp);
        }
    }
}

從上面的例子我們可以與ReentrantReadWriteLock進行對比:

(1)寫鎖的使用方式基本一對待;

(2)讀鎖(悲觀)的使用方式可以進行升級,通過tryConvertToWriteLock()方式可以升級為寫鎖;

(3)樂觀讀鎖是一種全新的方式,它假定數據沒有改變,樂觀讀之後處理完業務邏輯再判斷版本號是否有改變,如果沒改變則樂觀讀成功,如果有改變則轉化為悲觀讀鎖重試;

下面我們一起來學習它的源碼是怎麼實現的。

源碼分析

主要內部類

static final class WNode {
    // 前一個節點
    volatile WNode prev;
    // 后一個節點
    volatile WNode next;
    // 讀線程所用的鏈表(實際是一個棧結果)
    volatile WNode cowait;    // list of linked readers
    // 阻塞的線程
    volatile Thread thread;   // non-null while possibly parked
    // 狀態
    volatile int status;      // 0, WAITING, or CANCELLED
    // 讀模式還是寫模式
    final int mode;           // RMODE or WMODE
    WNode(int m, WNode p) { mode = m; prev = p; }
}

隊列中的節點,類似於AQS隊列中的節點,可以看到它組成了一個雙向鏈表,內部維護着阻塞的線程。

主要屬性

// 一堆常量
// 讀線程的個數佔有低7位
private static final int LG_READERS = 7;
// 讀線程個數每次增加的單位
private static final long RUNIT = 1L;
// 寫線程個數所在的位置
private static final long WBIT  = 1L << LG_READERS;  // 128 = 1000 0000
// 讀線程個數所在的位置
private static final long RBITS = WBIT - 1L;  // 127 = 111 1111
// 最大讀線程個數
private static final long RFULL = RBITS - 1L;  // 126 = 111 1110
// 讀線程個數和寫線程個數的掩碼
private static final long ABITS = RBITS | WBIT;  // 255 = 1111 1111
// 讀線程個數的反數,高25位全部為1
private static final long SBITS = ~RBITS;  // -128 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000

// state的初始值
private static final long ORIGIN = WBIT << 1;  // 256 = 1 0000 0000
// 隊列的頭節點
private transient volatile WNode whead;
// 隊列的尾節點
private transient volatile WNode wtail;
// 存儲着當前的版本號,類似於AQS的狀態變量state
private transient volatile long state;

通過屬性可以看到,這是一個類似於AQS的結構,內部同樣維護着一個狀態變量state和一個CLH隊列。

構造方法

public StampedLock() {
    state = ORIGIN;
}

state的初始值為ORIGIN(256),它的二進制是 1 0000 0000,也就是初始版本號。

writeLock()方法

獲取寫鎖。

public long writeLock() {
    long s, next;
    // ABITS = 255 = 1111 1111
    // WBITS = 128 = 1000 0000
    // state與ABITS如果等於0,嘗試原子更新state的值加WBITS
    // 如果成功則返回更新的值,如果失敗調用acquireWrite()方法
    return ((((s = state) & ABITS) == 0L &&
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}

我們以state等於初始值為例,則state & ABITS的結果為:

此時state為初始狀態,與ABITS與運算后的值為0,所以執行後面的CAS方法,s + WBITS的值為384 = 1 1000 0000。

到這裏我們大膽猜測:state的高24位存儲的是版本號,低8位存儲的是是否有加鎖,第8位存儲的是寫鎖,低7位存儲的是讀鎖被獲取的次數,而且如果只有第8位存儲寫鎖的話,那麼寫鎖只能被獲取一次,也就不可能重入了。

到底我們猜測的對不對呢,走着瞧^^

我們接着來分析acquireWrite()方法:

(手機橫屏看源碼更方便)

private long acquireWrite(boolean interruptible, long deadline) {
    // node為新增節點,p為尾節點(即將成為node的前置節點)
    WNode node = null, p;
    
    // 第一次自旋——入隊
    for (int spins = -1;;) { // spin while enqueuing
        long m, s, ns;
        // 再次嘗試獲取寫鎖
        if ((m = (s = state) & ABITS) == 0L) {
            if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
                return ns;
        }
        else if (spins < 0)
            // 如果自旋次數小於0,則計算自旋的次數
            // 如果當前有寫鎖獨佔且隊列無元素,說明快輪到自己了
            // 就自旋就行了,如果自旋完了還沒輪到自己才入隊
            // 則自旋次數為SPINS常量
            // 否則自旋次數為0
            spins = (m == WBIT && wtail == whead) ? SPINS : 0;
        else if (spins > 0) {
            // 當自旋次數大於0時,當前這次自旋隨機減一次自旋次數
            if (LockSupport.nextSecondarySeed() >= 0)
                --spins;
        }
        else if ((p = wtail) == null) {
            // 如果隊列未初始化,新建一個空節點並初始化頭節點和尾節點
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)
            // 如果新增節點還未初始化,則新建之,並賦值其前置節點為尾節點
            node = new WNode(WMODE, p);
        else if (node.prev != p)
            // 如果尾節點有變化,則更新新增節點的前置節點為新的尾節點
            node.prev = p;
        else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
            // 嘗試更新新增節點為新的尾節點成功,則退出循環
            p.next = node;
            break;
        }
    }

    // 第二次自旋——阻塞並等待喚醒
    for (int spins = -1;;) {
        // h為頭節點,np為新增節點的前置節點,pp為前前置節點,ps為前置節點的狀態
        WNode h, np, pp; int ps;
        // 如果頭節點等於前置節點,說明快輪到自己了
        if ((h = whead) == p) {
            if (spins < 0)
                // 初始化自旋次數
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                // 增加自旋次數
                spins <<= 1;
            
            // 第三次自旋,不斷嘗試獲取寫鎖
            for (int k = spins;;) { // spin at head
                long s, ns;
                if (((s = state) & ABITS) == 0L) {
                    if (U.compareAndSwapLong(this, STATE, s,
                                             ns = s + WBIT)) {
                        // 嘗試獲取寫鎖成功,將node設置為新頭節點並清除其前置節點(gc)
                        whead = node;
                        node.prev = null;
                        return ns;
                    }
                }
                // 隨機立減自旋次數,當自旋次數減為0時跳出循環再重試
                else if (LockSupport.nextSecondarySeed() >= 0 &&
                         --k <= 0)
                    break;
            }
        }
        else if (h != null) { // help release stale waiters
            // 這段代碼很難進來,是用於協助喚醒讀節點的
            // 我是這麼調試進來的:
            // 起三個寫線程,兩個讀線程
            // 寫線程1獲取鎖不要釋放
            // 讀線程1獲取鎖,讀線程2獲取鎖(會阻塞)
            // 寫線程2獲取鎖(會阻塞)
            // 寫線程1釋放鎖,此時會喚醒讀線程1
            // 在讀線程1裏面先不要喚醒讀線程2
            // 寫線程3獲取鎖,此時就會走到這裏來了
            WNode c; Thread w;
            // 如果頭節點的cowait鏈表(棧)不為空,喚醒裏面的所有節點
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        
        // 如果頭節點沒有變化
        if (whead == h) {
            // 如果尾節點有變化,則更新
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            }
            else if ((ps = p.status) == 0)
                // 如果尾節點狀態為0,則更新成WAITING
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                // 如果尾節點狀態為取消,則把它從鏈表中刪除
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                // 有超時時間的處理
                long time; // 0 argument to park means no timeout
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    // 已超時,剔除當前節點
                    return cancelWaiter(node, node, false);
                // 當前線程
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);
                // 把node的線程指向當前線程
                node.thread = wt;
                if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
                    whead == h && node.prev == p)
                    // 阻塞當前線程
                    U.park(false, time);  // 等同於LockSupport.park()
                    
                // 當前節點被喚醒后,清除線程
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                // 如果中斷了,取消當前節點
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

這裏對acquireWrite()方法做一個總結,這個方法裏面有三段自旋邏輯:

第一段自旋——入隊:

(1)如果頭節點等於尾節點,說明沒有其它線程排隊,那就多自旋一會,看能不能嘗試獲取到寫鎖;

(2)否則,自旋次數為0,直接讓其入隊;

第二段自旋——阻塞並等待被喚醒 + 第三段自旋——不斷嘗試獲取寫鎖:

(1)第三段自旋在第二段自旋內部;

(2)如果頭節點等於前置節點,那就進入第三段自旋,不斷嘗試獲取寫鎖;

(3)否則,嘗試喚醒頭節點中等待着的讀線程;

(4)最後,如果當前線程一直都沒有獲取到寫鎖,就阻塞當前線程並等待被喚醒;

這麼一大段邏輯看着比較鬧心,其實真正分解下來還是比較簡單的,無非就是自旋,把很多狀態的處理都糅合到一個for循環裏面處理了。

unlockWrite()方法

釋放寫鎖。

public void unlockWrite(long stamp) {
    WNode h;
    // 檢查版本號對不對
    if (state != stamp || (stamp & WBIT) == 0L)
        throw new IllegalMonitorStateException();
    // 這行代碼實際有兩個作用:
    // 1. 更新版本號加1
    // 2. 釋放寫鎖
    // stamp + WBIT實際會把state的第8位置為0,也就相當於釋放了寫鎖
    // 同時會進1,也就是高24位整體加1了
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    // 如果頭節點不為空,並且狀態不為0,調用release方法喚醒它的下一個節點
    if ((h = whead) != null && h.status != 0)
        release(h);
}
private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 將其狀態改為0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        // 如果頭節點的下一個節點為空或者其狀態為已取消
        if ((q = h.next) == null || q.status == CANCELLED) {
            // 從尾節點向前遍歷找到一個可用的節點
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        // 喚醒q節點所在的線程
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

寫鎖的釋放過程比較簡單:

(1)更改state的值,釋放寫鎖;

(2)版本號加1;

(3)喚醒下一個等待着的節點;

readLock()方法

獲取讀鎖。

public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    // 沒有寫鎖佔用,並且讀鎖被獲取的次數未達到最大值
    // 嘗試原子更新讀鎖被獲取的次數加1
    // 如果成功直接返回,如果失敗調用acquireRead()方法
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}

獲取讀鎖的時候先看看現在有沒有其它線程佔用着寫鎖,如果沒有的話再檢測讀鎖被獲取的次數有沒有達到最大,如果沒有的話直接嘗試獲取一次讀鎖,如果成功了直接返回版本號,如果沒成功就調用acquireRead()排隊。

下面我們一起來看看acquireRead()方法,這又是一個巨長無比的方法,請保持耐心,我們一步步來分解:

(手機橫屏看源碼更方便)

private long acquireRead(boolean interruptible, long deadline) {
    // node為新增節點,p為尾節點
    WNode node = null, p;
    // 第一段自旋——入隊
    for (int spins = -1;;) {
        // 頭節點
        WNode h;
        // 如果頭節點等於尾節點
        // 說明沒有排隊的線程了,快輪到自己了,直接自旋不斷嘗試獲取讀鎖
        if ((h = whead) == (p = wtail)) {
            // 第二段自旋——不斷嘗試獲取讀鎖
            for (long m, s, ns;;) {
                // 嘗試獲取讀鎖,如果成功了直接返回版本號
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                    // 如果讀線程個數達到了最大值,會溢出,返回的是0
                    return ns;
                else if (m >= WBIT) {
                    // m >= WBIT表示有其它線程先一步獲取了寫鎖
                    if (spins > 0) {
                        // 隨機立減自旋次數
                        if (LockSupport.nextSecondarySeed() >= 0)
                            --spins;
                    }
                    else {
                        // 如果自旋次數為0了,看看是否要跳出循環
                        if (spins == 0) {
                            WNode nh = whead, np = wtail;
                            if ((nh == h && np == p) || (h = nh) != (p = np))
                                break;
                        }
                        // 設置自旋次數
                        spins = SPINS;
                    }
                }
            }
        }
        // 如果尾節點為空,初始化頭節點和尾節點
        if (p == null) { // initialize queue
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)
            // 如果新增節點為空,初始化之
            node = new WNode(RMODE, p);
        else if (h == p || p.mode != RMODE) {
            // 如果頭節點等於尾節點或者尾節點不是讀模式
            // 當前節點入隊
            if (node.prev != p)
                node.prev = p;
            else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
                p.next = node;
                break;
            }
        }
        else if (!U.compareAndSwapObject(p, WCOWAIT,
                                         node.cowait = p.cowait, node))
            // 接着上一個elseif,這裏肯定是尾節點為讀模式了
            // 將當前節點加入到尾節點的cowait中,這是一個棧
            // 上面的CAS成功了是不會進入到這裏來的
            node.cowait = null;
        else {
            // 第三段自旋——阻塞當前線程並等待被喚醒
            for (;;) {
                WNode pp, c; Thread w;
                // 如果頭節點不為空且其cowait不為空,協助喚醒其中等待的讀線程
                if ((h = whead) != null && (c = h.cowait) != null &&
                    U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null) // help release
                    U.unpark(w);
                // 如果頭節點等待前前置節點或者等於前置節點或者前前置節點為空
                // 這同樣說明快輪到自己了
                if (h == (pp = p.prev) || h == p || pp == null) {
                    long m, s, ns;
                    // 第四段自旋——又是不斷嘗試獲取鎖
                    do {
                        if ((m = (s = state) & ABITS) < RFULL ?
                            U.compareAndSwapLong(this, STATE, s,
                                                 ns = s + RUNIT) :
                            (m < WBIT &&
                             (ns = tryIncReaderOverflow(s)) != 0L))
                            return ns;
                    } while (m < WBIT); // 只有當前時刻沒有其它線程佔有寫鎖就不斷嘗試
                }
                // 如果頭節點未曾改變且前前置節點也未曾改
                // 阻塞當前線程
                if (whead == h && p.prev == pp) {
                    long time;
                    // 如果前前置節點為空,或者頭節點等於前置節點,或者前置節點已取消
                    // 從第一個for自旋開始重試
                    if (pp == null || h == p || p.status > 0) {
                        node = null; // throw away
                        break;
                    }
                    // 超時檢測
                    if (deadline == 0L)
                        time = 0L;
                    else if ((time = deadline - System.nanoTime()) <= 0L)
                        // 如果超時了,取消當前節點
                        return cancelWaiter(node, p, false);
                    
                    // 當前線程
                    Thread wt = Thread.currentThread();
                    U.putObject(wt, PARKBLOCKER, this);
                    // 設置進node中
                    node.thread = wt;
                    // 檢測之前的條件未曾改變
                    if ((h != pp || (state & ABITS) == WBIT) &&
                        whead == h && p.prev == pp)
                        // 阻塞當前線程並等待被喚醒
                        U.park(false, time);
                    
                    // 喚醒之後清除線程
                    node.thread = null;
                    U.putObject(wt, PARKBLOCKER, null);
                    // 如果中斷了,取消當前節點
                    if (interruptible && Thread.interrupted())
                        return cancelWaiter(node, p, true);
                }
            }
        }
    }
    
    // 只有第一個讀線程會走到下面的for循環處,參考上面第一段自旋中有一個break,當第一個讀線程入隊的時候break出來的
    
    // 第五段自旋——跟上面的邏輯差不多,只不過這裏單獨搞一個自旋針對第一個讀線程
    for (int spins = -1;;) {
        WNode h, np, pp; int ps;
        // 如果頭節點等於尾節點,說明快輪到自己了
        // 不斷嘗試獲取讀鎖
        if ((h = whead) == p) {
            // 設置自旋次數
            if (spins < 0)
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                spins <<= 1;
                
            // 第六段自旋——不斷嘗試獲取讀鎖
            for (int k = spins;;) { // spin at head
                long m, s, ns;
                // 不斷嘗試獲取讀鎖
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {
                    // 獲取到了讀鎖
                    WNode c; Thread w;
                    whead = node;
                    node.prev = null;
                    // 喚醒當前節點中所有等待着的讀線程
                    // 因為當前節點是第一個讀節點,所以它是在隊列中的,其它讀節點都是掛這個節點的cowait棧中的
                    while ((c = node.cowait) != null) {
                        if (U.compareAndSwapObject(node, WCOWAIT,
                                                   c, c.cowait) &&
                            (w = c.thread) != null)
                            U.unpark(w);
                    }
                    // 返回版本號
                    return ns;
                }
                // 如果當前有其它線程佔有着寫鎖,並且沒有自旋次數了,跳出當前循環
                else if (m >= WBIT &&
                         LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
                    break;
            }
        }
        else if (h != null) {
            // 如果頭節點不等待尾節點且不為空且其為讀模式,協助喚醒裏面的讀線程
            WNode c; Thread w;
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        
        // 如果頭節點未曾變化
        if (whead == h) {
            // 更新前置節點及其狀態等
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            }
            else if ((ps = p.status) == 0)
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                // 第一個讀節點即將進入阻塞
                long time;
                // 超時設置
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    // 如果超時了取消當前節點
                    return cancelWaiter(node, node, false);
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);
                node.thread = wt;
                if (p.status < 0 &&
                    (p != h || (state & ABITS) == WBIT) &&
                    whead == h && node.prev == p)
                    // 阻塞第一個讀節點並等待被喚醒
                    U.park(false, time);
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

讀鎖的獲取過程比較艱辛,一共有六段自旋,Oh my god,讓我們來大致地分解一下:

(1)讀節點進來都是先判斷是頭節點如果等於尾節點,說明快輪到自己了,就不斷地嘗試獲取讀鎖,如果成功了就返回;

(2)如果頭節點不等於尾節點,這裏就會讓當前節點入隊,這裏入隊又分成了兩種;

(3)一種是首個讀節點入隊,它是會排隊到整個隊列的尾部,然後跳出第一段自旋;

(4)另一種是非第一個讀節點入隊,它是進入到首個讀節點的cowait棧中,所以更確切地說應該是入棧;

(5)不管是入隊還入棧后,都會再次檢測頭節點是不是等於尾節點了,如果相等,則會再次不斷嘗試獲取讀鎖;

(6)如果頭節點不等於尾節點,那麼才會真正地阻塞當前線程並等待被喚醒;

(7)上面說的首個讀節點其實是連續的讀線程中的首個,如果是兩個讀線程中間夾了一個寫線程,還是老老實實的排隊。

自旋,自旋,自旋,旋轉的木馬,讓我忘了傷^^

unlockRead()方法

釋放讀鎖。

public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) {
        // 檢查版本號
        if (((s = state) & SBITS) != (stamp & SBITS) ||
            (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            throw new IllegalMonitorStateException();
        // 讀線程個數正常
        if (m < RFULL) {
            // 釋放一次讀鎖
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                // 如果讀鎖全部都釋放了,且頭節點不為空且狀態不為0,喚醒它的下一個節點
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        else if (tryDecReaderOverflow(s) != 0L)
            // 讀線程個數溢出檢測
            break;
    }
}

private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 將其狀態改為0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        // 如果頭節點的下一個節點為空或者其狀態為已取消
        if ((q = h.next) == null || q.status == CANCELLED) {
            // 從尾節點向前遍歷找到一個可用的節點
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        // 喚醒q節點所在的線程
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

讀鎖釋放的過程就比較簡單了,將state的低7位減1,當減為0的時候說明完全釋放了讀鎖,就喚醒下一個排隊的線程。

tryOptimisticRead()方法

樂觀讀。

public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

如果沒有寫鎖,就返回state的高25位,這裏把寫所在位置一起返回了,是為了後面檢測數據有沒有被寫過。

validate()方法

檢測樂觀讀版本號是否變化。

public boolean validate(long stamp) {
    // 強制加入內存屏障,刷新數據
    U.loadFence();
    return (stamp & SBITS) == (state & SBITS);
}

檢測兩者的版本號是否一致,與SBITS與操作保證不受讀操作的影響。

變異的CLH隊列

StampedLock中的隊列是一種變異的CLH隊列,圖解如下:

總結

StampedLock的源碼解析到這裏就差不多了,讓我們來總結一下:

(1)StampedLock也是一種讀寫鎖,它不是基於AQS實現的;

(2)StampedLock相較於ReentrantReadWriteLock多了一種樂觀讀的模式,以及讀鎖轉化為寫鎖的方法;

(3)StampedLock的state存儲的是版本號,確切地說是高24位存儲的是版本號,寫鎖的釋放會增加其版本號,讀鎖不會;

(4)StampedLock的低7位存儲的讀鎖被獲取的次數,第8位存儲的是寫鎖被獲取的次數;

(5)StampedLock不是可重入鎖,因為只有第8位標識寫鎖被獲取了,並不能重複獲取;

(6)StampedLock中獲取鎖的過程使用了大量的自旋操作,對於短任務的執行會比較高效,長任務的執行會浪費大量CPU;

(7)StampedLock不能實現條件鎖;

彩蛋

StampedLock與ReentrantReadWriteLock的對比?

答:StampedLock與ReentrantReadWriteLock作為兩種不同的讀寫鎖方式,彤哥大致歸納了它們的異同點:

(1)兩者都有獲取讀鎖、獲取寫鎖、釋放讀鎖、釋放寫鎖的方法,這是相同點;

(2)兩者的結構基本類似,都是使用state + CLH隊列;

(3)前者的state分成三段,高24位存儲版本號、低7位存儲讀鎖被獲取的次數、第8位存儲寫鎖被獲取的次數;

(4)後者的state分成兩段,高16位存儲讀鎖被獲取的次數,低16位存儲寫鎖被獲取的次數;

(5)前者的CLH隊列可以看成是變異的CLH隊列,連續的讀線程只有首個節點存儲在隊列中,其它的節點存儲的首個節點的cowait棧中;

(6)後者的CLH隊列是正常的CLH隊列,所有的節點都在這個隊列中;

(7)前者獲取鎖的過程中有判斷首尾節點是否相同,也就是是不是快輪到自己了,如果是則不斷自旋,所以適合執行短任務;

(8)後者獲取鎖的過程中非公平模式下會做有限次嘗試;

(9)前者只有非公平模式,一上來就嘗試獲取鎖;

(10)前者喚醒讀鎖是一次性喚醒連續的讀鎖的,而且其它線程還會協助喚醒;

(11)後者是一個接着一個地喚醒的;

(12)前者有樂觀讀的模式,樂觀讀的實現是通過判斷state的高25位是否有變化來實現的;

(13)前者各種模式可以互轉,類似tryConvertToXxx()方法;

(14)前者寫鎖不可重入,後者寫鎖可重入;

(15)前者無法實現條件鎖,後者可以實現條件鎖;

差不多就這麼多吧,如果你還能想到,也歡迎補充哦^^

推薦閱讀

1、死磕 java同步系列之開篇

2、死磕 java魔法類之Unsafe解析

3、死磕 java同步系列之JMM(Java Memory Model)

4、死磕 java同步系列之volatile解析

5、死磕 java同步系列之synchronized解析

6、死磕 java同步系列之自己動手寫一個鎖Lock

7、死磕 java同步系列之AQS起篇

8、死磕 java同步系列之ReentrantLock源碼解析(一)——公平鎖、非公平鎖

9、死磕 java同步系列之ReentrantLock源碼解析(二)——條件鎖

10、死磕 java同步系列之ReentrantLock VS synchronized

11、死磕 java同步系列之ReentrantReadWriteLock源碼解析

12、死磕 java同步系列之Semaphore源碼解析

13、死磕 java同步系列之CountDownLatch源碼解析

14、死磕 java同步系列之AQS終篇

歡迎關注我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢遊源碼的海洋。

【精選推薦文章】

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

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

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

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

SpringBoot啟動流程分析(四):IoC容器的初始化過程

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

第五步:刷新應用上下文

一、前言

  在前面的博客中談到IoC容器的初始化過程,主要分下面三步:

1 BeanDefinition的Resource定位
2 BeanDefinition的載入
3 向IoC容器註冊BeanDefinition

  在上一篇文章介紹了prepareContext()方法,在準備刷新階段做了什麼工作。本文我們主要從refresh()方法中總結IoC容器的初始化過程。
  從run方法的,refreshContext()方法一路跟下去,最終來到AbstractApplicationContext類的refresh()方法。

 1 @Override
 2 public void refresh() throws BeansException, IllegalStateException {
 3     synchronized (this.startupShutdownMonitor) {
 4         // Prepare this context for refreshing.
 5         //刷新上下文環境
 6         prepareRefresh();
 7         // Tell the subclass to refresh the internal bean factory.
 8         //這裡是在子類中啟動 refreshBeanFactory() 的地方
 9         ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
10         // Prepare the bean factory for use in this context.
11         //準備bean工廠,以便在此上下文中使用
12         prepareBeanFactory(beanFactory);
13         try {
14             // Allows post-processing of the bean factory in context subclasses.
15             //設置 beanFactory 的後置處理
16             postProcessBeanFactory(beanFactory);
17             // Invoke factory processors registered as beans in the context.
18             //調用 BeanFactory 的后處理器,這些處理器是在Bean 定義中向容器註冊的
19             invokeBeanFactoryPostProcessors(beanFactory);
20             // Register bean processors that intercept bean creation.
21             //註冊Bean的后處理器,在Bean創建過程中調用
22             registerBeanPostProcessors(beanFactory);
23             // Initialize message source for this context.
24             //對上下文中的消息源進行初始化
25             initMessageSource();
26             // Initialize event multicaster for this context.
27             //初始化上下文中的事件機制
28             initApplicationEventMulticaster();
29             // Initialize other special beans in specific context subclasses.
30             //初始化其他特殊的Bean
31             onRefresh();
32             // Check for listener beans and register them.
33             //檢查監聽Bean並且將這些監聽Bean向容器註冊
34             registerListeners();
35             // Instantiate all remaining (non-lazy-init) singletons.
36             //實例化所有的(non-lazy-init)單件
37             finishBeanFactoryInitialization(beanFactory);
38             // Last step: publish corresponding event.
39             //發布容器事件,結束Refresh過程
40             finishRefresh();
41         } catch (BeansException ex) {
42             if (logger.isWarnEnabled()) {
43                 logger.warn("Exception encountered during context initialization - " +
44                         "cancelling refresh attempt: " + ex);
45             }
46             // Destroy already created singletons to avoid dangling resources.
47             destroyBeans();
48             // Reset 'active' flag.
49             cancelRefresh(ex);
50             // Propagate exception to caller.
51             throw ex;
52         } finally {
53             // Reset common introspection caches in Spring's core, since we
54             // might not ever need metadata for singleton beans anymore...
55             resetCommonCaches();
56         }
57     }
58 }

   從以上代碼中我們可以看到,refresh()方法中所作的工作也挺多,我們沒辦法面面俱到,主要根據IoC容器的初始化步驟和IoC依賴注入的過程進行分析,圍繞以上兩個過程,我們主要介紹重要的方法,其他的請看註釋。

 

二、obtainFreshBeanFactory();

  在啟動流程的第三步:初始化應用上下文。中我們創建了應用的上下文,並觸發了GenericApplicationContext類的構造方法如下所示,創建了beanFactory,也就是創建了DefaultListableBeanFactory類。

1 public GenericApplicationContext() {
2     this.beanFactory = new DefaultListableBeanFactory();
3 }

  關於obtainFreshBeanFactory()方法,其實就是拿到我們之前創建的beanFactory。

 1 protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
 2     //刷新BeanFactory
 3     refreshBeanFactory();
 4     //獲取beanFactory
 5     ConfigurableListableBeanFactory beanFactory = getBeanFactory();
 6     if (logger.isDebugEnabled()) {
 7         logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
 8     }
 9     return beanFactory;
10 }

  從上面代碼可知,在該方法中主要做了三個工作,刷新beanFactory,獲取beanFactory,返回beanFactory。

  首先看一下refreshBeanFactory()方法,跟下去來到GenericApplicationContext類的refreshBeanFactory()發現也沒做什麼。

1 @Override
2 protected final void refreshBeanFactory() throws IllegalStateException {
3     if (!this.refreshed.compareAndSet(false, true)) {
4         throw new IllegalStateException(
5                 "GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
6     }
7     this.beanFactory.setSerializationId(getId());
8 }
TIPS:
  1,AbstractApplicationContext類有兩個子類實現了refreshBeanFactory(),但是在前面第三步初始化上下文的時候,
實例化了GenericApplicationContext類,所以沒有進入AbstractRefreshableApplicationContext中的refreshBeanFactory()方法。
  2,this.refreshed.compareAndSet(false, true) 
  這行代碼在這裏表示:GenericApplicationContext只允許刷新一次   
  這行代碼,很重要,不是在Spring中很重要,而是這行代碼本身。首先看一下this.refreshed屬性: 
private final AtomicBoolean refreshed = new AtomicBoolean(); 
  java J.U.C併發包中很重要的一個原子類AtomicBoolean。通過該類的compareAndSet()方法可以實現一段代碼絕對只實現一次的功能。
感興趣的自行百度吧。

 

三、prepareBeanFactory(beanFactory);

  從字面意思上可以看出準備BeanFactory。

  看代碼,具體看看做了哪些準備工作。這個方法不是重點,看註釋吧。

 1 protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
 2     // Tell the internal bean factory to use the context's class loader etc.
 3     // 配置類加載器:默認使用當前上下文的類加載器
 4     beanFactory.setBeanClassLoader(getClassLoader());
 5     // 配置EL表達式:在Bean初始化完成,填充屬性的時候會用到
 6     beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
 7     // 添加屬性編輯器 PropertyEditor
 8     beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
 9 
10     // Configure the bean factory with context callbacks.
11     // 添加Bean的後置處理器
12     beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
13     // 忽略裝配以下指定的類
14     beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
15     beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
16     beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
17     beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
18     beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
19     beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
20 
21     // BeanFactory interface not registered as resolvable type in a plain factory.
22     // MessageSource registered (and found for autowiring) as a bean.
23     // 將以下類註冊到 beanFactory(DefaultListableBeanFactory) 的resolvableDependencies屬性中
24     beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
25     beanFactory.registerResolvableDependency(ResourceLoader.class, this);
26     beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
27     beanFactory.registerResolvableDependency(ApplicationContext.class, this);
28 
29     // Register early post-processor for detecting inner beans as ApplicationListeners.
30     // 將早期后處理器註冊為application監聽器,用於檢測內部bean
31     beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));
32 
33     // Detect a LoadTimeWeaver and prepare for weaving, if found.
34     //如果當前BeanFactory包含loadTimeWeaver Bean,說明存在類加載期織入AspectJ,
35     // 則把當前BeanFactory交給類加載期BeanPostProcessor實現類LoadTimeWeaverAwareProcessor來處理,
36     // 從而實現類加載期織入AspectJ的目的。
37     if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
38         beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
39         // Set a temporary ClassLoader for type matching.
40         beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
41     }
42 
43     // Register default environment beans.
44     // 將當前環境變量(environment) 註冊為單例bean
45     if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
46         beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
47     }
48     // 將當前系統配置(systemProperties) 註冊為單例Bean
49     if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
50         beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
51     }
52     // 將當前系統環境 (systemEnvironment) 註冊為單例Bean
53     if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
54         beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
55     }
56 }

 

四、postProcessBeanFactory(beanFactory);

  postProcessBeanFactory()方法向上下文中添加了一系列的Bean的後置處理器。後置處理器工作的時機是在所有的beanDenifition加載完成之後,bean實例化之前執行。簡單來說Bean的後置處理器可以修改BeanDefinition的屬性信息。

  關於這個方法就先這樣吧,有興趣的可以直接百度該方法。篇幅有限,對該方法不做過多介紹。

 

五、invokeBeanFactoryPostProcessors(beanFactory);(重點)

  上面說過,IoC容器的初始化過程包括三個步驟,在invokeBeanFactoryPostProcessors()方法中完成了IoC容器初始化過程的三個步驟。

  1,第一步:Resource定位

  在SpringBoot中,我們都知道他的包掃描是從主類所在的包開始掃描的,prepareContext()方法中,會先將主類解析成BeanDefinition,然後在refresh()方法的invokeBeanFactoryPostProcessors()方法中解析主類的BeanDefinition獲取basePackage的路徑。這樣就完成了定位的過程。其次SpringBoot的各種starter是通過SPI擴展機制實現的自動裝配,SpringBoot的自動裝配同樣也是在invokeBeanFactoryPostProcessors()方法中實現的。還有一種情況,在SpringBoot中有很多的@EnableXXX註解,細心點進去看的應該就知道其底層是@Import註解,在invokeBeanFactoryPostProcessors()方法中也實現了對該註解指定的配置類的定位加載。

  常規的在SpringBoot中有三種實現定位,第一個是主類所在包的,第二個是SPI擴展機制實現的自動裝配(比如各種starter),第三種就是@Import註解指定的類。(對於非常規的不說了)

  2,第二步:BeanDefinition的載入

  在第一步中說了三種Resource的定位情況,定位后緊接着就是BeanDefinition的分別載入。所謂的載入就是通過上面的定位得到的basePackage,SpringBoot會將該路徑拼接成:classpath*:org/springframework/boot/demo/**/*.class這樣的形式,然後一個叫做PathMatchingResourcePatternResolver的類會將該路徑下所有的.class文件都加載進來,然後遍歷判斷是不是有@Component註解,如果有的話,就是我們要裝載的BeanDefinition。大致過程就是這樣的了。

TIPS:
    @Configuration,@Controller,@Service等註解底層都是@Component註解,只不過包裝了一層罷了。

  3、第三個過程:註冊BeanDefinition

   這個過程通過調用上文提到的BeanDefinitionRegister接口的實現來完成。這個註冊過程把載入過程中解析得到的BeanDefinition向IoC容器進行註冊。通過上文的分析,我們可以看到,在IoC容器中將BeanDefinition注入到一個ConcurrentHashMap中,IoC容器就是通過這個HashMap來持有這些BeanDefinition數據的。比如DefaultListableBeanFactory 中的beanDefinitionMap屬性。

  OK,總結完了,接下來我們通過代碼看看具體是怎麼實現的。

 1 protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
 2     PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
 3     ...
 4 }
 5 // PostProcessorRegistrationDelegate類
 6 public static void invokeBeanFactoryPostProcessors(
 7         ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
 8     ...
 9     invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
10     ...
11 }
12 // PostProcessorRegistrationDelegate類
13 private static void invokeBeanDefinitionRegistryPostProcessors(
14         Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {
15 
16     for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
17         postProcessor.postProcessBeanDefinitionRegistry(registry);
18     }
19 }
20 // ConfigurationClassPostProcessor類
21 @Override
22 public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
23     ...
24     processConfigBeanDefinitions(registry);
25 }
26 // ConfigurationClassPostProcessor類
27 public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
28     ...
29     do {
30         parser.parse(candidates);
31         parser.validate();
32         ...
33     }
34     ...
35 }

   一路跟蹤調用棧,來到ConfigurationClassParser類的parse()方法。

 1 // ConfigurationClassParser類
 2 public void parse(Set<BeanDefinitionHolder> configCandidates) {
 3     this.deferredImportSelectors = new LinkedList<>();
 4     for (BeanDefinitionHolder holder : configCandidates) {
 5         BeanDefinition bd = holder.getBeanDefinition();
 6         try {
 7             // 如果是SpringBoot項目進來的,bd其實就是前面主類封裝成的 AnnotatedGenericBeanDefinition(AnnotatedBeanDefinition接口的實現類)
 8             if (bd instanceof AnnotatedBeanDefinition) {
 9                 parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
10             } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
11                 parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
12             } else {
13                 parse(bd.getBeanClassName(), holder.getBeanName());
14             }
15         } catch (BeanDefinitionStoreException ex) {
16             throw ex;
17         } catch (Throwable ex) {
18             throw new BeanDefinitionStoreException(
19                     "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
20         }
21     }
22     // 加載默認的配置---》(對springboot項目來說這裏就是自動裝配的入口了)
23     processDeferredImportSelectors();
24 }

   看上面的註釋,在前面的prepareContext()方法中,我們詳細介紹了我們的主類是如何一步步的封裝成AnnotatedGenericBeanDefinition,並註冊進IoC容器的beanDefinitionMap中的。

TIPS:
  至於processDeferredImportSelectors();方法,後面我們分析SpringBoot的自動裝配的時候會詳細講解,各種starter是如何一步步的實現自動裝配的。<SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現>

 

  繼續沿着parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());方法跟下去

  1 // ConfigurationClassParser類
  2 protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
  3     processConfigurationClass(new ConfigurationClass(metadata, beanName));
  4 }
  5 // ConfigurationClassParser類
  6 protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
  7     ...
  8     // Recursively process the configuration class and its superclass hierarchy.
  9     //遞歸地處理配置類及其父類層次結構。
 10     SourceClass sourceClass = asSourceClass(configClass);
 11     do {
 12         //遞歸處理Bean,如果有父類,遞歸處理,直到頂層父類
 13         sourceClass = doProcessConfigurationClass(configClass, sourceClass);
 14     }
 15     while (sourceClass != null);
 16 
 17     this.configurationClasses.put(configClass, configClass);
 18 }
 19 // ConfigurationClassParser類
 20 protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
 21         throws IOException {
 22 
 23     // Recursively process any member (nested) classes first
 24     //首先遞歸處理內部類,(SpringBoot項目的主類一般沒有內部類)
 25     processMemberClasses(configClass, sourceClass);
 26 
 27     // Process any @PropertySource annotations
 28     // 針對 @PropertySource 註解的屬性配置處理
 29     for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
 30             sourceClass.getMetadata(), PropertySources.class,
 31             org.springframework.context.annotation.PropertySource.class)) {
 32         if (this.environment instanceof ConfigurableEnvironment) {
 33             processPropertySource(propertySource);
 34         } else {
 35             logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
 36                     "]. Reason: Environment must implement ConfigurableEnvironment");
 37         }
 38     }
 39 
 40     // Process any @ComponentScan annotations
 41     // 根據 @ComponentScan 註解,掃描項目中的Bean(SpringBoot 啟動類上有該註解)
 42     Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
 43             sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
 44     if (!componentScans.isEmpty() &&
 45             !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
 46         for (AnnotationAttributes componentScan : componentScans) {
 47             // The config class is annotated with @ComponentScan -> perform the scan immediately
 48             // 立即執行掃描,(SpringBoot項目為什麼是從主類所在的包掃描,這就是關鍵了)
 49             Set<BeanDefinitionHolder> scannedBeanDefinitions =
 50                     this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
 51             // Check the set of scanned definitions for any further config classes and parse recursively if needed
 52             for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
 53                 BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
 54                 if (bdCand == null) {
 55                     bdCand = holder.getBeanDefinition();
 56                 }
 57                 // 檢查是否是ConfigurationClass(是否有configuration/component兩個註解),如果是,遞歸查找該類相關聯的配置類。
 58                 // 所謂相關的配置類,比如@Configuration中的@Bean定義的bean。或者在有@Component註解的類上繼續存在@Import註解。
 59                 if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
 60                     parse(bdCand.getBeanClassName(), holder.getBeanName());
 61                 }
 62             }
 63         }
 64     }
 65 
 66     // Process any @Import annotations
 67     //遞歸處理 @Import 註解(SpringBoot項目中經常用的各種@Enable*** 註解基本都是封裝的@Import)
 68     processImports(configClass, sourceClass, getImports(sourceClass), true);
 69 
 70     // Process any @ImportResource annotations
 71     AnnotationAttributes importResource =
 72             AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
 73     if (importResource != null) {
 74         String[] resources = importResource.getStringArray("locations");
 75         Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
 76         for (String resource : resources) {
 77             String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
 78             configClass.addImportedResource(resolvedResource, readerClass);
 79         }
 80     }
 81 
 82     // Process individual @Bean methods
 83     Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
 84     for (MethodMetadata methodMetadata : beanMethods) {
 85         configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
 86     }
 87 
 88     // Process default methods on interfaces
 89     processInterfaces(configClass, sourceClass);
 90 
 91     // Process superclass, if any
 92     if (sourceClass.getMetadata().hasSuperClass()) {
 93         String superclass = sourceClass.getMetadata().getSuperClassName();
 94         if (superclass != null && !superclass.startsWith("java") &&
 95                 !this.knownSuperclasses.containsKey(superclass)) {
 96             this.knownSuperclasses.put(superclass, configClass);
 97             // Superclass found, return its annotation metadata and recurse
 98             return sourceClass.getSuperClass();
 99         }
100     }
101 
102     // No superclass -> processing is complete
103     return null;
104

  看doProcessConfigurationClass()方法。(SpringBoot的包掃描的入口方法,重點哦)

  我們先大致說一下這個方法裏面都幹了什麼,然後稍後再閱讀源碼分析。

TIPS:
  在以上代碼的第60行parse(bdCand.getBeanClassName(), holder.getBeanName());會進行遞歸調用,
因為當Spring掃描到需要加載的類會進一步判斷每一個類是否滿足是@Component/@Configuration註解的類,
如果滿足會遞歸調用parse()方法,查找其相關的類。
  同樣的第68行processImports(configClass, sourceClass, getImports(sourceClass), true);
通過@Import註解查找到的類同樣也會遞歸查找其相關的類。
  兩個遞歸在debug的時候會很亂,用文字敘述起來更讓人難以理解,所以,我們只關注對主類的解析,及其類的掃描過程。

  上面代碼的第29行 for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(… 獲取主類上的@PropertySource註解(關於該註解是怎麼用的請自行百度),解析該註解並將該註解指定的properties配置文件中的值存儲到Spring的 Environment中,Environment接口提供方法去讀取配置文件中的值,參數是properties文件中定義的key值。

  42行 Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); 解析主類上的@ComponentScan註解,呃,怎麼說呢,42行後面的代碼將會解析該註解並進行包掃描。

  68行 processImports(configClass, sourceClass, getImports(sourceClass), true); 解析主類上的@Import註解,並加載該註解指定的配置類。

TIPS:

  在spring中好多註解都是一層一層封裝的,比如@EnableXXX,是對@Import註解的二次封裝。@SpringBootApplication註解=@ComponentScan+@EnableAutoConfiguration+@Import+@Configuration+@Component。@Controller,@Service等等是對@Component的二次封裝。。。

 

5.1、看看42-64行幹了啥

  從上面的42行往下看,來到第49行 Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); 

  進入該方法

 1 // ComponentScanAnnotationParser類
 2 public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
 3     ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
 4             componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
 5     ...
 6     // 根據 declaringClass (如果是SpringBoot項目,則參數為主類的全路徑名)
 7     if (basePackages.isEmpty()) {
 8         basePackages.add(ClassUtils.getPackageName(declaringClass));
 9     }
10     ...
11     // 根據basePackages掃描類
12     return scanner.doScan(StringUtils.toStringArray(basePackages));
13 }

  發現有兩行重要的代碼

  為了驗證代碼中的註釋,debug,看一下declaringClass,如下圖所示確實是我們的主類的全路徑名。

  跳過這一行,繼續debug,查看basePackages,該set集合中只有一個,就是主類所在的路徑。

TIPS:
  為什麼只有一個還要用一個集合呢,因為我們也可以用@ComponentScan註解指定掃描路徑。

  到這裏呢IoC容器初始化三個步驟的第一步,Resource定位就完成了,成功定位到了主類所在的包。

  接着往下看 return scanner.doScan(StringUtils.toStringArray(basePackages)); Spring是如何進行類掃描的。進入doScan()方法。

 1 // ComponentScanAnnotationParser類
 2 protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
 3     Assert.notEmpty(basePackages, "At least one base package must be specified");
 4     Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
 5     for (String basePackage : basePackages) {
 6         // 從指定的包中掃描需要裝載的Bean
 7         Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
 8         for (BeanDefinition candidate : candidates) {
 9             ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
10             candidate.setScope(scopeMetadata.getScopeName());
11             String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
12             if (candidate instanceof AbstractBeanDefinition) {
13                 postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
14             }
15             if (candidate instanceof AnnotatedBeanDefinition) {
16                 AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
17             }
18             if (checkCandidate(beanName, candidate)) {
19                 BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
20                 definitionHolder =
21                         AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
22                 beanDefinitions.add(definitionHolder);
23                 //將該 Bean 註冊進 IoC容器(beanDefinitionMap)
24                 registerBeanDefinition(definitionHolder, this.registry);
25             }
26         }
27     }
28     return beanDefinitions;
29 }

  這個方法中有兩個比較重要的方法,第7行 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); 從basePackage中掃描類並解析成BeanDefinition,拿到所有符合條件的類后在第24行 registerBeanDefinition(definitionHolder, this.registry); 將該類註冊進IoC容器。也就是說在這個方法中完成了IoC容器初始化過程的第二三步,BeanDefinition的載入,和BeanDefinition的註冊。

 

5.1.1、findCandidateComponents(basePackage);

  跟蹤調用棧

 1 // ClassPathScanningCandidateComponentProvider類
 2 public Set<BeanDefinition> findCandidateComponents(String basePackage) {
 3     ...
 4     else {
 5         return scanCandidateComponents(basePackage);
 6     }
 7 }
 8 // ClassPathScanningCandidateComponentProvider類
 9 private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
10     Set<BeanDefinition> candidates = new LinkedHashSet<>();
11     try {
12         //拼接掃描路徑,比如:classpath*:org/springframework/boot/demo/**/*.class
13         String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
14                 resolveBasePackage(basePackage) + '/' + this.resourcePattern;
15         //從 packageSearchPath 路徑中掃描所有的類
16         Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
17         boolean traceEnabled = logger.isTraceEnabled();
18         boolean debugEnabled = logger.isDebugEnabled();
19         for (Resource resource : resources) {
20             if (traceEnabled) {
21                 logger.trace("Scanning " + resource);
22             }
23             if (resource.isReadable()) {
24                 try {
25                     MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
26                     // //判斷該類是不是 @Component 註解標註的類,並且不是需要排除掉的類
27                     if (isCandidateComponent(metadataReader)) {
28                         //將該類封裝成 ScannedGenericBeanDefinition(BeanDefinition接口的實現類)類
29                         ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
30                         sbd.setResource(resource);
31                         sbd.setSource(resource);
32                         if (isCandidateComponent(sbd)) {
33                             if (debugEnabled) {
34                                 logger.debug("Identified candidate component class: " + resource);
35                             }
36                             candidates.add(sbd);
37                         } else {
38                             if (debugEnabled) {
39                                 logger.debug("Ignored because not a concrete top-level class: " + resource);
40                             }
41                         }
42                     } else {
43                         if (traceEnabled) {
44                             logger.trace("Ignored because not matching any filter: " + resource);
45                         }
46                     }
47                 } catch (Throwable ex) {
48                     throw new BeanDefinitionStoreException(
49                             "Failed to read candidate component class: " + resource, ex);
50                 }
51             } else {
52                 if (traceEnabled) {
53                     logger.trace("Ignored because not readable: " + resource);
54                 }
55             }
56         }
57     } catch (IOException ex) {
58         throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
59     }
60     return candidates;
61 }

  在第13行將basePackage拼接成classpath*:org/springframework/boot/demo/**/*.class,在第16行的getResources(packageSearchPath);方法中掃描到了該路徑下的所有的類。然後遍歷這些Resources,在第27行判斷該類是不是 @Component 註解標註的類,並且不是需要排除掉的類。在第29行將掃描到的類,解析成ScannedGenericBeanDefinition,該類是BeanDefinition接口的實現類。OK,IoC容器的BeanDefinition載入到這裏就結束了。

  回到前面的doScan()方法,debug看一下結果(截圖中所示的就是我定位的需要交給Spring容器管理的類)。

 

5.1.2、registerBeanDefinition(definitionHolder, this.registry);

  查看registerBeanDefinition()方法。是不是有點眼熟,在前面介紹prepareContext()方法時,我們詳細介紹了主類的BeanDefinition是怎麼一步一步的註冊進DefaultListableBeanFactory的beanDefinitionMap中的。在此呢我們就省略1w字吧。完成了BeanDefinition的註冊,就完成了IoC容器的初始化過程。此時,在使用的IoC容器DefaultListableFactory中已經建立了整個Bean的配置信息,而這些BeanDefinition已經可以被容器使用了。他們都在BeanbefinitionMap里被檢索和使用。容器的作用就是對這些信息進行處理和維護。這些信息是容器簡歷依賴反轉的基礎。

1 protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
2     BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
3 }

   OK,到這裏IoC容器的初始化過程的三個步驟就梳理完了。當然這隻是針對SpringBoot的包掃描的定位方式的BeanDefinition的定位,加載,和註冊過程。前面我們說過,還有兩種方式@Import和SPI擴展實現的starter的自動裝配。

 

5.2、@Import註解的解析過程

  相信不說大家也應該知道了,各種@EnableXXX註解,很大一部分都是對@Import的二次封裝(其實也是為了解耦,比如當@Import導入的類發生變化時,我們的業務系統也不需要改任何代碼)。

  呃,我們又要回到上文中的ConfigurationClassParser類的doProcessConfigurationClass方法的第68行processImports(configClass, sourceClass, getImports(sourceClass), true);,跳躍性比較大。上面解釋過,我們只針對主類進行分析,因為這裡有遞歸。

  processImports(configClass, sourceClass, getImports(sourceClass), true);中configClass和sourceClass參數都是主類相對應的哦。

TIPS:
  在分析這一塊的時候,我在主類上加了@EnableCaching註解。

  首先看getImports(sourceClass);

1 private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
2     Set<SourceClass> imports = new LinkedHashSet<>();
3     Set<SourceClass> visited = new LinkedHashSet<>();
4     collectImports(sourceClass, imports, visited);
5     return imports;
6 }

   debug

  正是@EnableCaching註解中的@Import註解指定的類。另外兩個呢是主類上的@SpringBootApplication中的@Import註解指定的類。不信你可以一層層的剝開@SpringBootApplication註解的皮去一探究竟。

  至於processImports()方法,大家自行debug吧,相信看到這裏,思路大家都已經很清楚了。

  

  凌晨兩點了,睡覺,明天繼續上班。

 

 

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

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

 

【精選推薦文章】

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

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

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

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

從實踐者的角度看軟件架構的歷史

無論什麼東西,套用宋丹丹的話,就是都有它的過去、現在和將(jiǎng)來。因此學習一樣東西,如果能多學一點它的歷史,會讓我們對其為何有如此現狀少一些糾結,同時才有可能對其未來趨勢有靠譜一點點的洞見。昨夜窗外雨聲稀疏,難以入眠,突然想到軟件架構的發展史是怎樣的,於是今晨起來網上逛一圈,邂逅到這篇論文《The History of Software Architecture – In the Eye of the Practitioner》,因此,這是一篇譯文。

小弟不才,沒有能力自己去梳理這麼龐大的論題,因此只能翻譯了。不過我並沒有翻譯這篇論文的全部內容,比如附錄就沒有翻譯。在翻譯的過程中,一度覺得這論文的英文及其拗口,跟我閱讀過的其它英文書比實在是難讀。開弓沒有回頭箭,我還是把要譯的部分譯完了,難免有詞不達意之處,還望海涵。

以下是譯文:

 

1. 目標和動機

那些權威論文把軟件架構當作獨立的學科,一些科技期刊也把架構視角、架構描述語言、架構進化作為他們研究和實踐的基石,從這些角度來看的話,軟件架構正好迎來了它的25周年慶。

隨着基於雲交付的普及,分佈式系統的各個部分需要動態集成,商業和社會数字化的曲線越來越陡,使得一個合理的設計決策包含了更大和更複雜的問題空間。軟件架構的重要性前所未有,開展重要項目的組織離不開相應架構實踐的支撐。然而,這25年來,軟件架構的實踐是如何進化的呢?未來又將面臨哪些挑戰?

對軟件架構研究和實踐現狀的總結,曾有過各種不同的嘗試,然而都缺少了從實踐者的角度來看待上面的兩個問題。

為了填補這個空缺,我們首先從5622篇科技論文中抽出10大主題,如圖3。然後我們根據這些主題設計一個在線問卷調查,並由擁有5到20年不等經驗的57位軟件架構實踐者填寫了這份問卷調查。

 

2. 實踐者對過去25年軟件架構及未來之路的看法

這篇論文中,我們的調查聚焦於,在過去25年裡,軟件架構方面最突出的話題有哪些,以及在當下和不久的將來,軟件架構方面的哪些話題會具有最深遠的影響力。

調查問卷包含的問題有:

a) 參與者的背景、經驗和其它一些統計信息;

b) 目前他們供職的機構,過去幾年接手的項目類型;

c) 過去25年在軟件架構方面的實踐;

d) 最有影響力而且是目前軟件架構趨勢的話題(包括近兩年新興的);

e) 未來工業界可能在軟件架構方向的實踐(未來5年);

基於參與者的回答,我們精編得到以下結果。

過去:圖1.PAST 總結了實踐者們認為的過去25年最有影響力的10個軟件架構話題。我們可以看到:

1.最有影響力的話題屬於“軟件開發流程”、“面向服務的架構(SOA)”、“架構風格”、“物聯網(IoT)”。這些總共佔了38%的比重。

2.SOA就像圖中展示的那樣了,其它的話題則包含更多特定的子話題:“軟件開發流程”包含了敏捷開發、持續交付集成、DevOps、領域驅動設計、需求角色、遺留(系統/代碼)、風險和質量管理、溝通技能。“架構風格”包含隨着時間而演變的各種不同風格,從C-S和分佈式架構,到生產線架構、MVC、多層架構等等。最後,“IoT”包括数字化、Web、互聯網、工業4.0和移動優先。

圖1.PAST 標紅的數字錶示它是否出現在圖3中(科學文獻中前10的話題)。同樣的,我們可以看到:

1.工業界前4個話題同樣出現在學術界的前10個話題中,但影響力有些差別:“軟件開發流程”在工業界排第1,但在學術界只排第7,“SOA”在工業界和學術界都排第2,還有很明顯的,“架構風格”在工業界的影響力被認為是大得多,排在第3,而在學術界排第6。

2.真正有大分歧的話題是“架構描述&語言”,這在學術機構排第1,而在實踐者眼中只排到第8。不過這並不奇怪,符號和語言常常作為學術界深愛的研究課題,但工業界真正採用的並不多。不過也好,這正可以作為一個讓研究者和實踐者進行更好描述和高效溝通的契機。

還有一些話題,被實踐者們提及,但是卻不入專業研究者的法眼。最突出的比如:

1.軟件質量(第5):很顯然,質量成為軟件架構的屬性是過去25年的一個重要成果。“質量”有時候又被稱為質量符合性、性能、可擴展性、可維護性等等。

2.雲計算(第6)和微服務(第7):它們被認為非常(每樣14票)的有影響力。作為SOA的衍生品,它們被認為是同屬一個話題—這樣就一共獲得42票,變成了過去25年最有影響力的話題。

現在:圖1.PRESENT 使用同樣的分析方法得到現今軟件架構最有影響力的話題。這個結果中,“架構風格”排在第10(緊挨着“SOA”和“架構設計決策”)。另一方面:

1.“SOA”被“雲”和“微服務”替代:我們認為這是正常的技術進化,作為一種普遍的架構風格,SOA曾被應用到到各個應用領域。

2.“軟件開發流程”仍然保持穩定(第1)。相較於對過去25年的回答,今天“一切都是敏捷”:敏捷之後呢,是DevOps、持續架構(continuous-architecting)。我們注意到,在敏捷開發中,架構扮演更重要角色的意識正在增強。

3.同樣的,“IoT”保持穩定(第4)。然而相比過去,它被認為是有更大的影響力,關注點也從移動應用轉變到基於IoT的架構。這也和Gartner關於2018年重大技術策略趨勢的預測相符,預測中提到的“智能物件(intelligent things)”,就是將AI與IoT融合。

4.明顯的,“軟件質量”(第6)和“安全性(第7)”的影響力在下降。這或許是架構師們都知道了如何應對這些問題,又或許他們覺得有更重要的話題要關注。

總的來說,現今最有影響力的話題總共佔據了70%的答案。其中,流程、面向服務(雲和微服務)、IoT三者共佔了62%。

而且,在現今的前10個話題中,有一些新名詞引起了我們的注意:

1.“大數據”(第5):稱為大數據,或者AI、機器學習、機器分析。

2.“第三方軟件集成”(第8):在過去25年的部分曾被提及,但排在第14,不過都分別得到了5票。不過從答案中,可以看到軟件架構正從封閉走向開放。

未來:圖 1.FUTURE,我們從調查反饋中看到比較高的不確定性。即使實踐者們認為前4個話題在未來5年仍保持主流,但它們佔據總票數的52%,比現今的影響力要小10%。

圖1.FUTURE 展示了:

1.“軟件開發流程”、“大數據”、“微服務”和“雲計算”會繼續扮演非常重要的角色,然而:

2.對於“軟件開發流程”,實踐者們更關注如何管理不斷增加的複雜性,可能是跨組織的,並將注意力放到了更高的自動化上。

3.對於“大數據”,提到了AI將扮演的角色和大數據在我們日常生活中進行的各種預測。

4.“微服務”將成熟,新的“雲”將關注點放在基於雲架構的風格/模式,以及如何通過軟件架構來實現XaaS商業模型。

5.“自適應系統”(第5),在過去的幾年裡火熱於學術界,被認為在工業界也變得越來越重要。

6.在新的話題當中,“區塊鏈”也位於前10(不過反應平平,可能出乎你的意料)。

7.其它冒出的新鮮話題、不在前10名單中的有機器人、数字化轉型、智能互聯、綠色軟件、倫理學。

 

3. 反思與收穫

除了之前呈現的結果以外,我們還要求實踐者們根據自身經歷反饋哪些架構話題在他們過去的25年裡產生過最重大的影響,以每5年為一個周期,如下圖:

圖2 展示了過去(比如client-server架構,從1992-2001年)佔據主流的話題如何被新話題(比如“架構設計決策”、“架構知識體系”,從2002-2011年)超越的,以及最新的一些話題(“信息物理系統cyber-physical systems”和IoT,從2012-2017年)是如何湧現的。這讓我們得到至少以下的認知:

認知1. 在軟件架構的歷史中,架構的概念從先前的一系列系統結構,變成了處於大型的、複雜的、不斷進化的環境中的軟件系統。然而在這樣的環境中,不只是關乎技術,還包括人員、社交、組織生態和整個社會。軟件架構的實踐者關注的面比研究者們要廣泛,軟件架構實踐者同時追隨其它學科,從AI、IoT、自適應增強,到能源和倫理學。

認知2. “軟件開發流程”贏了。無論是過去,現在還是未來,軟件架構始終關注如何更敏捷的進行開發,怎樣的人員技能可以有助於開發。房間里的大象(明顯存在的問題)可以用來形容軟件架構溝通和形式化之間存在的窘境:“架構模型”和“架構設計”常常用來彌補“軟件開發流程”,但卻從來無法達到預期—將架構代碼化,使得架構可重用並且可靠。

認知3. 對於軟件架構這個話題,不存在革命性的新東西,只有在舊的東西之上默默地演化。比如“軟件開發流程”演化成各種形式的敏捷開發,“架構風格”從“SOA”演化到“微服務”和“雲”,“信息物理系統”進化融合到“IoT”和“自適應系統”。總的來說,我們討論的是軟件架構的全局趨勢,通向更便捷性的,可管理更多複雜性。

認知4. 軟件架構的研究和實踐始終保持一致。當我們對比圖3中的前10個研究課題和圖2中工業界的主流話題,我們看到比如“客戶端-服務器”和“架構風格”在研究和實踐方面有非常類似的趨勢。“軟件架構設計”和“架構設計決策”雖然研究和實踐方面有不同的趨勢,但都保持着重要的地位。最明顯的就是“架構描述&語言”,在研究領域炙手可熱,而實踐中少得多。

最後,我們希望從歷史角度看到的軟件架構演化史能給讀者帶來進一步的思考和靈感。

 

文章最初發表於:從實踐者的角度看軟件架構的歷史

歡迎關注微信公眾號:

【精選推薦文章】

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

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

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

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

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

asp.net core 系列之Response caching(1)

這篇文章簡單的講解了response caching:

講解了cache-control,及對其中的頭和值的作用,及設置來控制response caching;

簡單的羅列了其他的緩存技術:In-memory caching , Distributed Cache , Cache Tag Helper , Distributed Cache Tag Helper ;

講解了使用ResponseCache attribute來控制response caching生成合適的頭

 主要翻譯於官網,水平有限,見諒

Overview

響應緩存減少了客戶端或者代理到web服務器的請求的次數響應緩存也可以減少web服務器的生成響應的執行的工作量。響應緩存被頭部控制,頭部指出了你想要客戶端,代理和中間件怎樣緩存響應。

ResponseCache attribute 參加設置響應緩存頭部,which clients may honor when caching responses. (當緩存響應時,客戶端會受這些屬性影響)Response Caching Middleware 可以被用來在服務器上緩存響應。 中間件可以使用ResponseCacheAttribute屬性來影響服務端緩存行為。 

HTTP-based response caching 

HTTP 1.1 Caching specification(規格,詳述,說明書)描述了網絡緩存應該怎樣表現(Internet caches should behave.) 主要的用於緩存的HTTP頭,是Cache-Control, 它被用於指定緩存指令。這個指令控制緩存行為,當請求從客戶端到服務端的時候,並且當響應從服務端返回客戶端的時候。

公共的Cache-Control 指令在下錶中被展示了:

其他緩存頭在緩存中扮演的角色,羅列在下面了:

注意:Cache-Control,是用在從請求中的HTTP頭,可以用來控制服務器中緩存行為。

HTTP-based caching respects request Cache-Control directives

HTTP 1.1 Caching specification for the Cache-Control header (HTTP 1.1 緩存規格對於Cache-Control)要求一個緩存遵守一個有效的Cache-Control ,這個Cache-Control頭是被客戶端發送的。一個客戶端可以發送一個帶no-cacheheader,並且強制要求服務器為每個請求生成一個新的響應。

總是遵守客戶端Cache-Control請求頭是有意義的,如果你考慮HTTP緩存的目標。在官方的說明書下,

緩存意味着減少潛在因素和網絡管理,對滿足請求跨客戶端,代理和服務器網絡。它不是一種控制原服務器上的加載的必須的方式。

當使用Response Caching 中間件時,開發者是沒法對緩存行為控制的。因為中間件附着於官方緩存說明書。當決定提供一個緩存響應時,對這个中間件的計劃豐富(planned enhancements to the middleware)對於達到配置中間件來忽視請求的Cache-Control頭的目的,是一個機會(Planned enhancements to middleware are an opportunity to middleware to ignore a request’s Cache-Control header when deciding to serve a cached response.)。計劃的豐富提供了一個機會來更好的控制服務器加載。

Other caching technology in ASP.NET Core ASP.NET Core上的其他緩存技術

  • In-memory caching 內存緩存

    In-memory caching 使用服務器內存來存儲緩存數據。這種類型的緩存適合使用sticky sessionsticky:不動的)的一個或者多個服務器Sticky sessions 意味着從客戶端發出的請求總是路由到同一台服務器處理。

    更多信息:Cache in-memory in ASP.NET Core.

  • Distributed Cache 分佈式緩存

    使用一個分佈式緩存來存儲數據在內存中,當應用部署在雲上或者服務器集群上時。緩存是在這些處理請求的服務器之間共享的。客戶端可以提交一個請求,請求可以被組群里的任意服務器處理,如果緩存數據對於客戶端是可用的。ASP.NET Core提供了SQL ServerRedis分佈式緩存

    更多信息:Distributed caching in ASP.NET Core.

  • Cache Tag Helper

    使用Cache Tagmvc頁面或者Razor Page中緩存內容Cache Tag Helper用內存緩存數據

    更多信息:Cache Tag Helper in ASP.NET Core MVC

  • Distributed Cache Tag Helper

    在分佈式雲或者web集群場景中使用Distributed Cache Tag Helper 來緩存Mvc view或者Razor Page中的內容The Distributed Cache Tag Helper SQL Server或者Redis來緩存數據

    更多信息:Distributed Cache Tag Helper in ASP.NET Core.

ResponseCache attribute

為了在response caching (響應緩存)上設置合適的頭,ResponseCacheAttribute 指出了必須的參數。(即,可以通過ResponseCacheAttribute,設置response caching上的頭的值)

注意:對於包含驗證信息的客戶端內容,不允許緩存。對於那些不會基於用戶的身份或者用戶是否登錄而改變的內容,才應該允許被緩存。

VaryByQueryKeys 隨着給出的query keys的集合的值,改變存儲的響應。When a single value of * is provided, the middleware varies responses by all request query string parameters. 

Response Caching Middleware 必須被允許設置VaryByQueryKeys屬性。否則,一個運行時異常會被拋出。對於VaryByQueryKeys屬性,並沒有一個對應的HTTP頭部。這個屬性是一個被Response Caching Middleware 處理的HTTP 功能。對於中間件提供一個緩存的響應,查詢字符串和查詢字符串值必須匹配之前的請求.(即,如果查詢字符串和查詢字符串值和之前的一樣時,中間件會直接返回一個緩存的響應;否則,返回一個新的響應。)例如,考慮下錶中的一系列的請求和結果:

第一個請求被服務器返回,並且緩存到中間件中。第二個請求是被中間件返回,因為查詢字符串匹配之前的請求。第三個請求不是在中間件緩存中的,因為查詢字符串值不匹配之前的請求。

ResponseCacheAttribute用於配置和創建一個ResponseCacheFilter.    

ResponseCacheFilter執行的工作,更新合適的HTTP頭和響應的功能(即,ResponseCacheAttribute的功能)The filter:

  • 移除任何存在的Vary, Cache-Control, Pragma頭部
  • 根據設置在ResponseCacheAttribute中的屬性輸出合適的頭部

  • 更新the response caching HTTP feature如果VaryByQueryKeys被設置了

Vary

這個頭部會被寫,當VaryByHeader屬性被設置了。這個屬性(VaryByHeader)設置Vary屬性的值。下面是使用VaryByHeader屬性的例子:

[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)] public class Cache1Model : PageModel
{

用這個例子,使用瀏覽器工具觀察response headers(響應頭)。 下面的響應頭隨着Cache1 page response 被發送了。

Cache-Control: public,max-age=30
Vary: User-Agent
NoStore and Location.None

NoStore重寫了大部分的其他屬性。當這個屬性被設置為true,Cache-Control頭被設置為no-store.

如果Location設置為None:

  • Cache-Control 設置為no-store, no-cache

  • Pragma設置為no-cache.

如果NoStorefalse並且LocationNoneCache-Control ,Pragma被設置為no-cache.

NoStore是典型的被設置為true,為了error pages. 示例中的Cache2 page生成響應頭,指示客戶端不要存儲響應。

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class Cache2Model : PageModel
{

這個示例應用返回Cache2 page 帶有下面的頭:

Cache-Control: no-store,no-cache
Pragma: no-cache
Location and Duration

為了可以緩存,Duration必須被設置為一個積極的值並且Location必須是任意的或者Client. 這種情況下Cache-Control頭被設置為location的值,並且跟着一個響應的max-age.

注意:

Location’s options of Any and Client轉化為Cache-Control頭的值分別為publicprivate. 正如之前提到的,設置LocationNone會設置Cache-ControlPramga頭為no-cache:

[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false)] public class Cache3Model : PageModel
{

示例應用返回的Cache3 page 帶有下面的頭:

Cache-Control: public,max-age=10
Cache profiles

取代重複的在很多controller action attributes響應緩存設置,cache profiles 可以被設置為options,當在Startup.ConfigureService中設置MVC/Razor Pages. 在引用的cache profiles中發現的值被用作默認值,隨着ResponseCacheAttribute並且被這個attribute上指定的任意properties重寫。(即很多重複的響應緩存設置可以在Startup.ConfigureService中設置,再隨着ResponseCacheAttribute設置在action上)

建立一個cache profile. 下面的例子展示了30秒的cache profile,在示例應用的Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.CacheProfiles.Add("Default30", new CacheProfile() { Duration = 30 });
    }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

這個示例應用的Cache4 page model 引用了Default30 cache profile:

[ResponseCache(CacheProfileName = "Default30")] public class Cache4Model : PageModel
{

這個ResponseCacheAttribute可以被用在

  • Razor Page handlers(classes) – 屬性可以被用到處理方法

  • MVC controllers(classes)

  • MVC actions (methods) – Method-level attributes override the settings specified in class level attributes. 方法級別的會覆蓋類級別的

Default30 profile導致的應用於Cache4 page response 的頭是:

Cache-Control: public,max-age=30

 

下一篇:Cache in-memory

 參考資料:

https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-2.2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

【精選推薦文章】

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

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

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

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