Java多線程(二):Thread類

Thread類的實例方法

start()

start方法內部會調用方法start方法啟動一個線程,該線程返回start方法,同時Java虛擬機調用native start0啟動另一個線程調用run方法,此時有兩個線程并行執行;
我們來分析下start0方法,start0到底是如何調用run方法的

Thread類里有一個本地方法叫registerNatives,此方法註冊一些本地方法給Thread類使用
在OpenJDK官網找到Thread.c

#include "jni.h"
#include "jvm.h"

#include "java_lang_Thread.h"

#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread}, //Java中Thread類的start方法所調用的start0方法
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};

......

根據關鍵字”JVM_StartThread”再找到jvm.cpp

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
  bool throw_illegal_thread_state = false;

  {
    MutexLocker mu(Threads_lock);
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {

      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz); //請看這裏,實例化了一個線程native_thread

      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

sz是大小參數,忽略之,我們看thread_entry是什麼

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  JavaCalls::call_virtual(&result,
                          obj,
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                          vmSymbols::run_method_name(),  //請看這裏,jvm調用run_method_name方法
                          vmSymbols::void_method_signature(),
                          THREAD);
}

run_method_name在vmSymbols.hpp被定義

  /* common method and field names */                                                             
  template(run_method_name,                           "run")      //run_method_name的名稱是"run"

簡言之:當前線程調用start方法通知ThreadGroup當前線程可以運行了,可以被加入了,當前線程啟動后,當前線程狀態為”Runnable”。另一個線程等待CPU時間片,調用run方法(線程真正執行)。產生一個異步執行的效果;
用start方法來啟動線程,真正實現了多線程運行,這時無需等待run方法體代碼執行完畢而直接繼續執行下面的代碼。
代碼如下

public class MyThread03 extends Thread{
    public void run()
    {
        try
        {
            for (int i = 0; i < 3; i++)
            {
                Thread.sleep((int)(Math.random() * 1000));
                System.out.println("run = " + Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        MyThread03 mt = new MyThread03();
        mt.start();

        try
        {
            for (int i = 0; i < 3; i++)
            {
                Thread.sleep((int)(Math.random() * 1000));
                System.out.println("run = " + Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

執行結果如下,可以看到,Thead-0和main線程交叉執行,是無序的。很好理解,因為main和Thread-0在爭搶CPU資源,這個過程是無序的。

run = main
run = Thread-0
run = main
run = main
run = Thread-0
run = Thread-0

再看一個例子,代碼如下

public class MyThread04 extends Thread{
    public void run()
    {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args)
    {
        MyThread04 mt0 = new MyThread04();
        MyThread04 mt1 = new MyThread04();
        MyThread04 mt2 = new MyThread04();

        mt0.start();
        mt1.start();
        mt2.start();
    }
}

執行結果如下

Thread-0
Thread-2
Thread-1

我們依次啟動mt0,mt1,mt2,這說明線程啟動順序也是無序的。因為start方法僅僅返回調用,線程想要執行必須得到CPU時間片再執行run方法,CPU時間片的獲得是無序的。

run()

run方法是Thread類的一個普通方法,執行run方法其實是單線程執行

public class MyThread05 extends Thread{

    public void run()
    {
        System.out.println("run = " + Thread.currentThread().getName());
    }

    public static void main(String[] args)
    {
        MyThread05 mt = new MyThread05();
        mt.run();

        try
        {
            for (int i = 0; i < 3; i++)
            {
                Thread.sleep((int)(Math.random() * 1000));
                System.out.println("run = " + Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

輸出結果如下

run = main
run = main
run = main
run = main

main線程循環了3次,run方法1次,結果是main線程執行了四次,我們寫在run方法體內的被main線程執行,這說明調用run方法執行多線程是不可行的。

isAlive()

判斷線程是否存活

public class MyThread06 extends Thread{
    public void run()
    {
        System.out.println("run = " + this.isAlive());
    }


    public static void main(String[] args) throws Exception
    {
        MyThread06 mt = new MyThread06();
        System.out.println("begin == " + mt.isAlive());
        mt.start();
        Thread.sleep(100);
        System.out.println("end == " + mt.isAlive());
    }
}

輸出結果如下,增加0.1秒延遲,讓線程執行完

begin == false
run = true
end == false

可以看到,執行前false,執行中true,執行后false

getId()

返回線程的標識符,線程ID是正值,線程ID在生命周期內不會變化,當線程終止了,線程ID可能會被重用

getName()

返回線程名稱

getPriority()和setPriority(int)

返回優先級和設置優先級
優先級越高的線程獲取CPU時間片的概率越高
請看如下的例子

public class MyThread07_0 extends Thread{
    public void run()
    {
        System.out.println("MyThread07_0 run priority = " +
                this.getPriority());
    }

    public static void main(String[] args)
    {
        System.out.println("main thread begin, priority = " +
                Thread.currentThread().getPriority());
        System.out.println("main thread end, priority = " +
                Thread.currentThread().getPriority());
        MyThread07_0 thread = new MyThread07_0();
        thread.start();
    }
}

運行結果如下

main thread begin, priority = 5
main thread end, priority = 5
MyThread07_0 run priority = 5

線程的默認優先級是5
再看如下的例子

public class MyThread07_1 extends Thread {

    public void run()
    {
        System.out.println("MyThread07_1 run priority = " +
                this.getPriority());
        MyThread07_0 thread = new MyThread07_0();
        thread.start();
    }

    public static void main(String[] args)
    {
        System.out.println("main thread begin, priority = " +
                Thread.currentThread().getPriority());
        System.out.println("main thread end, priority = " +
                Thread.currentThread().getPriority());
        MyThread07_1 thread = new MyThread07_1();
        thread.start();
    }
}

我們在MyThread07_1線程內部啟動MyThread07_0線程,我們觀察MyThread07_1和MyThread07_0的優先級有什麼關係。
運行結果如下

main thread begin, priority = 5
main thread end, priority = 5
MyThread07_1 run priority = 5
MyThread07_0 run priority = 5

MyThread07_0和MyThread07_1線程的優先級一致,說明線程具有繼承性。
現在我們來設置優先級

public class MyThread08 {

    static class MyThread08_0 extends Thread {
        public void run() {
            long beginTime = System.currentTimeMillis();
            for (int j = 0; j < 1000000; j++) {}
            long endTime = System.currentTimeMillis();
            System.out.println("★★★★ MyThread08_0 use time = " +
                    (endTime - beginTime));
        }
    }

    static class MyThread08_1 extends Thread {
        public void run()
        {
            long beginTime = System.currentTimeMillis();
            for (int j = 0; j < 1000000; j++){}
            long endTime = System.currentTimeMillis();
            System.out.println("☆☆☆☆ MyThread08_1 use time = " +
                    (endTime - beginTime));
        }
    }

    public static void main(String[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            MyThread08_0 mt0 = new MyThread08_0();
            mt0.setPriority(5);
            mt0.start();
            MyThread08_1 mt1 = new MyThread08_1();
            mt1.setPriority(4);
            mt1.start();
        }
    }

}

我們給MyThread08_0線程設置更高的優先級5
運行結果如下

★★★★ MyThread08_0 use time = 7
☆☆☆☆ MyThread08_1 use time = 4
★★★★ MyThread08_0 use time = 18
★★★★ MyThread08_0 use time = 16
★★★★ MyThread08_0 use time = 20
★★★★ MyThread08_0 use time = 17
☆☆☆☆ MyThread08_1 use time = 0
☆☆☆☆ MyThread08_1 use time = 10
☆☆☆☆ MyThread08_1 use time = 9
☆☆☆☆ MyThread08_1 use time = 8

可以看到MyThread08_0先執行的次數更多,輸出結果為實心五角星的這個。
多運行幾次,都會是MyThread08_0先打印完,每次結果都不盡相同,CPU會盡量先讓MyThread08_0執行完。

isDaemon()和setDaemon(boolean)

isDaemon方法判斷是否是守護線程;
setDaemon設置守護線程
在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)
我們自定義的線程和main線程都是用戶線程,我們熟知的GC(垃圾回收器)就是守護線程。守護線程是用戶線程的“奴僕”,當用戶線程執行完畢,守護線程就會終止,因為它沒有存在的必要了。
如用戶線程執行結束,GC無垃圾可回收,它只能死亡
看如下代碼

public class MyThread09 extends Thread{
    private int i = 0;

    public void run()
    {
        try
        {
            while (true)
            {
                i++;
                System.out.println(Thread.currentThread().getName()+" i = " + i);
                Thread.sleep(1000);
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        try
        {
            MyThread09 mt = new MyThread09();
            mt.setDaemon(true);
            mt.start();
            Thread.sleep(5000);
            System.out.println("現在是"+Thread.currentThread().getName()+"線程");
            Thread.sleep(1);

        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

我們自定義MyThread09線程的run方法里是死循環,如果是用戶線程,它應該永遠地執行下去,現在把它設置成守護線程。
注意:mt.setDaemon(true);要在mt.start();之前,見

否則會拋出IllegalThreadStateException異常
運行結果如下

Thread-0 i = 1
Thread-0 i = 2
Thread-0 i = 3
Thread-0 i = 4
Thread-0 i = 5
現在是main線程
Thread-0 i = 6
MyThread09變成了守護線程,它的使命已經完成。現在是main線程

Thread.sleep(5000)的目的是使main線程沉睡5s,即用戶線程(main線程)仍在執行,此時main線程輸出,再沉睡1ms,當main線程執行完畢,守護線程就沒有存在的意義了,即死亡;
main線程總共執行了大約5001ms(略大於這個數值),Thread-0打印到i=6,說明守護線程在main線程之後死亡,這個時間差極小

interrupt()

設置中斷標誌位,無法中斷線程

public class MyThread10 extends Thread{
    public void run()
    {
        for (int i = 0; i < 500000; i++)
        {
            System.out.println("i = " + (i + 1));
        }
    }

    public static void main(String[] args)
    {
        try
        {
            MyThread10 mt = new MyThread10();
            mt.start();
            Thread.sleep(2000);
            mt.interrupt();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

輸出結果如下

......
i = 499993
i = 499994
i = 499995
i = 499996
i = 499997
i = 499998
i = 499999
i = 500000

可以看到,interrupt()沒有中斷線程,interrupt()後續將會詳細講解

isInterrupted()

判斷線程是否被中斷

join()

等待這個線程死亡,舉例說明:
線程A執行join方法,會阻塞線程B,線程A join方法執行完畢,才能執行線程B
代碼如下

public class MyThread11 extends Thread{
    public void run()
    {
        try
        {
            int secondValue = (int)(Math.random() * 1000);
            System.out.println(secondValue);
            Thread.sleep(secondValue);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception
    {
        MyThread11 mt = new MyThread11();
        mt.start();
        mt.join();
        System.out.println("MyThread11執行完畢之後我再執行");
    }
}

輸出結果如下

75
MyThread11執行完畢之後我再執行

可以看到,main線程在mt線程之後執行。mt調用join方法,使main線程阻塞,待mt線程執行完畢,方可執行main線程。

Thread類的靜態方法

currentThread()

返回當前正在執行線程的引用

public class MyThread12 extends Thread{

    static
    {
        System.out.println("靜態塊的打印:" +
                Thread.currentThread().getName());
    }

    public MyThread12()
    {
        System.out.println("構造方法的打印:" +
                Thread.currentThread().getName());
    }

    public void run()
    {
        System.out.println("run()方法的打印:" +
                Thread.currentThread().getName());
    }



    public static void main(String[] args)
    {
        MyThread12 mt = new MyThread12();
        mt.start();
    }


}

輸出結果

靜態塊的打印:main
構造方法的打印:main
run()方法的打印:Thread-0

可以看到,構造方法和靜態塊是main線程在調用,重寫的run方法是線程自己在調用。
再看個例子

public class MyThread13 extends Thread{
    public MyThread13()
    {
        System.out.println("MyThread13----->Begin");
        System.out.println("Thread.currentThread().getName()----->" +
                Thread.currentThread().getName());
        System.out.println("this.getName()----->" + this.getName());
        System.out.println("MyThread13----->end");
    }

    public void run()
    {
        System.out.println("run----->Begin");
        System.out.println("Thread.currentThread().getName()----->" +
                Thread.currentThread().getName());
        System.out.println("this.getName()----->" + this.getName());
        System.out.println("run----->end");
    }



    public static void main(String[] args)
    {
        MyThread13 mt = new MyThread13();
        mt.start();
    }


}

輸出結果

MyThread13----->Begin
Thread.currentThread().getName()----->main
this.getName()----->Thread-0
MyThread13----->end
run----->Begin
Thread.currentThread().getName()----->Thread-0
this.getName()----->Thread-0
run----->end

可以看到,執行MyThread13構造方法的線程是main,執行MyThread13的線程是Thread-0(當前線程),run方法就是被線程實例所執行。

sleep(long)

讓當前線程沉睡若干毫秒

public class MyThread14 extends Thread{
    public void run()
    {
        try
        {
            System.out.println("run threadName = " +
                    this.getName() + " begin");
            Thread.sleep(2000);
            System.out.println("run threadName = " +
                    this.getName() + " end");
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        MyThread14 mt = new MyThread14();
        mt.start();
    }
}

輸出結果如下

run threadName = Thread-0 begin
run threadName = Thread-0 end

打印完第一句兩秒后打印第二句。

yield()

當前線程放棄CPU的使用權,這裏的放棄是指當前線程少用CPU資源,最後線程還是會執行完成

public class MyThread15 extends Thread {
    public void run()
    {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 5000000; i++)
        {
            Thread.yield();
            count = count + i + 1;
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用時:" + (endTime - beginTime) + "毫秒!");
    }



    public static void main(String[] args)
    {
        MyThread15 mt = new MyThread15();
        mt.start();
    }


}

輸出結果如下

用時:4210毫秒!

可以看到,任務執行完畢,當我們把Thread.yield();註釋掉,執行時間只需要7ms。說明當前線程放棄了一些CPU資源。

interrupted()

判斷當前線程是否中斷,靜態版的isInterrupted方法。多線程中斷機制,後續會詳細解析。

【精選推薦文章】

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

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

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

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

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

量子邏輯門

量子態的演化

在前面量子糾纏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中工業界的主流話題,我們看到比如“客戶端-服務器”和“架構風格”在研究和實踐方面有非常類似的趨勢。“軟件架構設計”和“架構設計決策”雖然研究和實踐方面有不同的趨勢,但都保持着重要的地位。最明顯的就是“架構描述&語言”,在研究領域炙手可熱,而實踐中少得多。

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

 

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

歡迎關注微信公眾號:

【精選推薦文章】

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

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

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

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

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