曹工說JDK源碼(1)–ConcurrentHashMap,擴容前大家同在一個哈希桶,為啥擴容后,你去新數組的高位,我只能去低位?

如何計算,一對key/value應該放在哪個哈希桶

大家都知道,hashmap底層是數組+鏈表(不討論紅黑樹的情況),其中,這個數組,我們一般叫做哈希桶,大家如果去看jdk的源碼,會發現裏面有一些變量,叫做bin,這個bin,就是桶的意思,結合語境,就是哈希桶。

這裏舉個例子,假設一個hashmap的數組長度為4(0000 0100),那麼該hashmap就有4個哈希桶,分別為bucket[0]、bucket[1]、bucket[2]、bucket[3]。

現在有兩個node,hashcode分別是1(0000 0001),5(0000 0101). 我們當然知道,這兩個node,都應該放入第一個桶,畢竟1 mod 4,5 mod 4的結果,都是1。

但是,在代碼里,可不是用取模的方法來計算的,而是使用下面的方式:

int entryNodeIndex = (tableLength - 1) & hash;

應該說,在tableLength的值,為2的n次冪的時候,兩者是等價的,但是因為位運算的效率更高,因此,代碼一般都使用位運算,替代取模運算。

下面我們看看具體怎麼計算:

此處,tableLength即為哈希表的長度,此處為4. 4 – 1為3,3的二進製表示為:

0000 0011

那麼,和我們的1(0000 0001)相與:

0000 0001 -------- 1
0000 0011 -------- 3(tableLength - 1)
    相與(同為1,則為1;否則為0)
0000 0001 -------- 1     

結果為1,所以,應該放在第1個哈希桶,即數組下標為1的node。

接下來,看看5這個hashcode的節點要放在什麼位置,是怎麼計算:

0000 0101 -------- 5
0000 0011 -------- 3(tableLength - 1)
    相與(同為1,則為1;否則為0)后結果:
0000 0001 -------- 1     

擴容時,是怎麼對一個hash桶進行transfer的

此處,具體的整個transfer的細節,我們本講不會涉及太多,不過,大體的邏輯,我們可以來想一想。

以前面為例,哈希表一共4個桶,其中bucket[1]裏面,存放了兩個元素,假設是a、b,其hashcode分別是1,5.

現在,假設我們要擴容,一般來說,擴容的時候,都是新建一個bucket數組,其容量為舊錶的一倍,這裏舊錶為4,那新表就是8.

那,新表建立起來了,舊錶里的元素,就得搬到新表裡面去,等所有元素都搬到新表了,就會把新表和舊錶的指針交換。如下:

java.util.concurrent.ConcurrentHashMap#transfer

    private transient volatile Node<K,V>[] nextTable;

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

if (finishing) {
    // 1
    nextTable = null;
    // 2
    table = nextTab;
    // 3
    sizeCtl = (tabLength << 1) - (tabLength >>> 1);
    return;
}
  • 1處,將field:nextTable(也就是新表)設為null,擴容完了,這個field就會設為null

  • 2處,將局部變量nextTab,賦值給table,這個局部變量nextTab里,就是當前已經擴容完畢的新表

  • 3處,修改表的sizeCtl為:假設此處tabLength為4,tabLength << 1 左移1位,就是8;tabLength >>> 1,右移一位,就是2,。8 – 2 = 6,正好就等於 8(新表容量) * 0.75。

    所以,這裏的sizeCtl就是,新表容量 * 負載因子,超過這個容量,基本就會觸發擴容。

ok,接着說,我們要怎麼從舊錶往新表搬呢? 那以前面的bucket[1]舉例,遍歷這個鏈表,計算各個node,應該放到新表的什麼位置,不就完了嗎?是的,理論上這麼寫就完事了。

但是,我們會怎麼寫呢?

用hashcode對新bucket數組的長度取余嗎?

jdk對效率的追求那麼高,肯定不會這麼寫的,我們看看,它怎麼寫的:

java.util.concurrent.ConcurrentHashMap#transfer

// 1
for (Node<K,V> p = entryNode; p != null; p = p.next) {
    // 2
    int ph = p.hash;
    K pk = p.key;
    V pv = p.val;
    
	// 3
    if ((ph & tabLength) == 0){
        lowEntryNode = new Node<K,V>(ph, pk, pv, lowEntryNode);
    }
    else{
        highEntryNode = new Node<K,V>(ph, pk, pv, highEntryNode);
    }
}
  • 1處,即遍歷舊的哈希表的某個哈希桶,假設就是遍歷前面的bucket[1],裏面有a/b兩個元素,hashcode分別為1,5那個。

  • 2處,獲取該節點的hashcode,此處分別為1,5

  • 3處,如果hashcode 和 舊錶長度相與,結果為0,則,將該節點使用頭插法,插入新表的低位;如果結果不為0,則放入高位。

    ok,什麼是高位,什麼是低位。擴容后,新的bucket數組,長度為8,那麼,前面bucket[1]中的兩個元素,將分別放入bucket[1]和bucket[5].

    ok,這裏的bucket[1]就是低位,bucket[5]為高位。

首先,大家要知道,hashmap中,容量總是2的n次方,請牢牢記住這句話。

為什麼要這麼做?你想想,這樣是不是擴容很方便?

以前,hashcode 為1,5的,都在bucket[1];而現在,擴容為8后,hashcode為1的,還是在newbucket[1],hashcode為5的,則在newbucket[5];這樣的話,是不是有一半的元素,根本不用動?

這就是我覺得的,最大的好處;另外呢,運算也比較方便,都可以使用位運算代替,效率更高。

好的,那我們現在問題來了,下面這句的原理是什麼?

    if ((ph & tabLength) == 0){
        lowEntryNode = new Node<K,V>(ph, pk, pv, lowEntryNode);
    } else{
        highEntryNode = new Node<K,V>(ph, pk, pv, highEntryNode);
    }

為啥,hashcode & 舊哈希表的容量, 結果為0的,擴容后,就會在低位,也就是維持位置不變呢?而結果不為0的,擴容后,位置在高位呢?

背後的位運算原理(大白話)

代碼里用的如下判斷,滿足這個條件,去低位;否則,去高位。

 if ((ph & tabLength) == 0)

還是用前面的例子,假設當前元素為a,hashcode為1,和哈希桶大小4,去進行運算。

0000 0001  ---- 1
0000 0100  ---- 舊哈希表容量4
&運算(同為1則為1,否則為0)
結果:
0000 0000  ---- 結果為0    

ok,這裏算出來,結果為0;什麼情況下,結果會為0呢?

那我們現在開始倒推,什麼樣的數,和 0000 0100 相與,結果會為0?

???? ????  ---- 
0000 0100  ---- 舊哈希表容量
&運算(同為1則為1,否則為0)
結果:
0000 0000  ---- 結果為0    

因為與運算的規則是,同為1,則為1;否則都為0。那麼,我們這個例子里,舊哈希表容量為 0000 0100,假設表示為2的n次方,此處n為2,我們僅有第三位(第n+1)為1,那如果對方這一位為0,那結果中的這一位,就會為0,那麼,整個數,就為0.

所以,我們的結論是:假設哈希表容量,為2的n次方,表示為二進制后,第n+1位為1;那麼,只要我們節點的hashcode,在第n+1位上為0,則最終結果是0.

反之,如果我們節點的hashcode,在第n+1位為1,則最終結果不會是0.

比如,hashcode為5的時候,會是什麼樣子?

0000 0101  ---- 5
0000 0100  ---- 舊哈希表容量
&運算(同為1則為1,否則為0)
結果:
0000 0100  ---- 結果為4    

此時,5這個hashcode,在第n+1位上為1,所以結果不為0。

至此,我們離答案好像還很遠。ok,不慌,繼續。

假設現在擴容了,新bucket數組,長度為8.

a元素,hashcode依然是1,a元素應該放到新bucket數組的哪個bucket里呢?

我們用前面說的這個算法來計算:

int entryNodeIndex = (tableLength - 1) & hash;
0000 0001  ---- 1
0000 0111  ---- 8 - 1 = 7
&運算(同為1則為1,否則為0)
結果:
0000 0001  ---- 結果為1

結果沒錯,確實應該放到新bucket[1],但怎麼推論出來呢?

    // 1
	if ((ph & tabLength) == 0){
        // 2
        lowEntryNode = new Node<K,V>(ph, pk, pv, lowEntryNode);
    }

也就是說,假設一個數,滿足1處的條件:(ph & tabLength) == 0,那怎麼推論出2呢,即應該在低位呢?

ok,條件1,前面分析了,可以得出:

這個數,第n+1位為0.

接下來,看看數組長度 – 1這個數。

數組長度 2的n次方 二進製表示 1出現的位置 數組長度-1 數組長度-1的二進制
2 2的1次方 0000 0010 第2位 1 0000 0001
4 2的2次方 0000 0100 第3位 3 0000 0011
8 2的3次方 0000 1000 第4位 7 0000 0111

好了,兩個數都有了,

???????0???????   -- 1 節點的hashcode,第n + 1位為0
000000010000000   -- 2 老數組    
000000100000000   -- 3 新數組的長度,等於老數組長度 * 2
000000011111111   -- 4 新數組的長度 - 1
    
    運算:1和4相與
    

大家注意看紅字部分,還有框出來的那一列,這一列為0,導致,最終結果,肯定是比2那一行的数字小,2這行,不就是老數組的長度嗎,那你比老數組小;你比這一行小,在新數組裡,就只能在低位了。

反之,如果節點的hashcode,這一位為1,那麼,最終結果,至少是大於等於2這一行的数字,所以,會放在高位。

參考資料

https://www.jianshu.com/p/2829fe36a8dd

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

HashMap源碼閱讀(java1.8.0)

1.1 背景知識

1.1.1 紅黑樹

  二叉查找樹可能因為多次插入新節點導致失去平衡,使得查找效率低,查找的複雜度甚至可能會出現線性的,為了解決因為新節點的插入而導致查找樹不平衡,此時就出現了紅黑樹。

紅黑樹它一種特殊的二叉查找樹。紅黑樹的每個節點上都有存儲位表示節點的顏色,可以是紅(Red)或黑(Black)。它具有以下特點:

(1)每個節點或者是黑色,或者是紅色。

(2)根節點是黑色。

(3)每個恭弘=叶 恭弘子節點(恭弘=叶 恭弘子節點,是指為空(NIL或NULL)的恭弘=叶 恭弘子節點)是黑色。

(4)如果一個節點是紅色的,則它的子節點一定是黑色(即從根節點到恭弘=叶 恭弘子節點的路徑上不能有兩個重複的紅色節點)。

(5)從一個節點到其上每一個恭弘=叶 恭弘節點的所有路徑都具有相同的黑色節點個數。

紅黑樹的基本操作–添加

① 將紅黑樹當作一顆二叉查找樹,將節點插入。

② 將插入的節點着色為”紅色”。(因為條件5,從一個節點到其中每一個節點的的所有路徑都具有相同的黑色節點)。

③通過一系列的旋轉(左旋或右旋操作)或着色等操作,使之重新成為一顆紅黑樹。

                       

1.2 源碼

  在java 1.7之前是用數組和鏈表一起組合構成HashMap,在java1.8之後就使用當鏈表長度超過8之後,就會將鏈錶轉化為紅黑樹,縮小查找的時間(紅黑樹維護也會花費大量時間,包含左旋、右旋和變色過程)。

1.2.1 HashMap的初始化

hashmap構造函數會初始化三個值:

  • 初始容量initialCapacity:默認值是16,當儲存的數據越來越多的時候,就必須進行擴容操作。
  • 閾值threshold:hashmap的數組結構中所能存放的最大數量,超過該數量,則會對數組進行擴容。閾值的計算方式為:容量(initialCapacity)*負載因子(loadFactor)。
  • 負載因子loadFactor:當負載因子很大時,閾值會很大,table數組擴容的可能性比較小,會使得一個數組中的鏈表(紅黑樹)存放過多的數據,雖然節省了一定的空間,但會導致查詢時間很長。相反負載因子很小時,擴容的可能性會很高,使得數組中的數據變得相對少,查詢時間會縮短,但會花費較長的時間。

  在初始化一個hashmap對象的時候,指定鍵值對的同時,也可以指定初始map的容量大小,假設此處我們指定大小為11,則會在構造函數中調用tableSizeFor將容量改為2的n冪次,即比當前容量大,而且必須是2的指數次冪的最小數,就會變成16。這是因為2的指數次冪便於計算進行位運算操作,提升運行效率問題(位運算>加法>乘法>除法>取模)。

  hashmap的的默認值是16,負載因子默認是0.75,代碼如下:

//HashMap<String,String> hashMap = new HashMap<String, String>(11);

/**
 * Returns a power of two size for the given target capacity.
 **/
static final int tableSizeFor(int cap) {
    int n = cap - 1;   //10 防止在cap已經是2的n次冪的情況下
    // >>> 表示不關心符號位,對數據的二進制形式進行右移  |表示或運算
    n |= n >>> 1;	  //15
    n |= n >>> 2;     //15
    n |= n >>> 4;     //15
    n |= n >>> 8;     //15
    n |= n >>> 16;    //15
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; //16
}

1.2.2 HashMap的put操作

   這裏可以介紹一下&位運算,當我們將一對KV存儲到hashmap當中時,會通過(n – 1) & hash運算來定位將要插入的鍵值對放入到哈希表的某個桶中。其中n表示哈希表的長度,通常n為2的倍數,通過n-1即可n所表示的二進制數,除最高位外,全部轉化為1,藉助與運算即可快速完成取模操作。

 //hashMap.put("2020", "good luck");

 /**
  * Implements Map.put and related methods.
  *
  * @param hash hash for key
  * @param key the key
  * @param value the value to put
  * @param onlyIfAbsent if true, don't change existing value
  * @param evict if false, the table is in creation mode.
  * @return previous value, or null if none
  */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果hashtable沒有初始化,則初始化該table數組
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length; 
    //通過位運算找到數組中的下標位置,如果數組中對應下標為空,則可以直接存放下去
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //數組元素對應的位置已經有元素,產生碰撞
        Node<K,V> e; K k;
        //如果插入的元素key是已經存在的,則將新的value替換掉原來的舊值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果此時table數組對應的位置是紅黑樹結構,則將該節點插入紅黑樹中
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //如果此時table數組對應的位置是鏈表結構
            for (int binCount = 0; ; ++binCount) {
				//遍歷到數組尾端,沒有與插入鍵值對相同的key,則將新的鍵值對插入鏈表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //鏈表過長,將鏈錶轉化為紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //發現鏈表中的某個節點有與插入鍵值對相同的key,則跳出循環,在循環外部重新賦值
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //該key在hashmap已存在,更新與在鏈表跳出循環節點對應的值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //超過閾值則更新
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

1.2.3 HashMap的get操作

/**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //table數組不為空,且對應的下標位置也不為空。
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果第一個位置是對應的key,則返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //遍歷其他元素
            if ((e = first.next) != null) {
                //紅黑樹
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //鏈表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

1.2.4 HashMap的擴容操作

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //table不為空,且容量大於0
        if (oldCap > 0) {
            //如果舊的容量到達閾值,則不再擴容,閾值直接設置為最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果舊的容量沒有到達閾值,直接操作
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //閾值大於0,直接使用舊的閾值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //如果閾值為零,則使用默認的初始化值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //更新數組桶
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //將之前舊數組桶的數據重新移到新數組桶中
        if (oldTab != null) {
            //依次遍歷舊table中每個數組桶的元素
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //如果數組桶中含有元素
                if ((e = oldTab[j]) != null) {
                    //將下標數據清空
                    oldTab[j] = null;
                    //如果元組的某一桶中只有一個元素,則直接將該元素移到新的位置去
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果是紅黑樹結構
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                     //鏈表 -- 對舊桶里鏈表中的每一個元素重新計算哈希值得到下標
                    else { // preserve order
                        //將原先桶中的鏈表分為兩個鏈表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            /*
                             * e.hash & oldCap 對hash取模運算,
                             * 雖然數組大小擴大了一倍,
                             * 但是同一個key在新舊table中對應的index卻存在一定聯繫: 
                             * 要麼一致,要麼相差一個 oldCap。
                             */
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

  此處在處理鏈表的時候,如何將鏈表中的節點重新分配到新的哈希表需要做一些解釋。在擴容的時候,將原來的哈希表擴大了一倍,原來屬於同一個桶中的數據會被重新分配,此時取模運算時(a mod b),會注意到,b會擴大兩倍(a mod 2b),此時如果該桶中的某一個數據的哈希值是c1(0<c<b),則它必定還是會落入原來的位置,而如果桶中的某一個數據的哈希值是c2(b<c2<2b),則它會被重新分配到一個新的位置(這個位置是原先的哈希桶位置+舊桶的大小)。

HashMap在多線程的情況下出現的死循環現象

  在某些java版本中擴容機制如果使用鏈表,且再插入時使用尾插法會出現死循環,具體原因可以參考老生常談,HashMap的死循環,在本文中所參考的java版本使用了頭插法的方式將元素添加到鏈表當中,可以避免死循環的出現,但是會出現一部分節點丟失的問題。如圖:

  假設原始的哈希map的某個桶的數據如下,此時線程開始擴容,將桶中的數據分配到lo和hi桶的鏈表中。

   初始時刻線程1和線程2開始運行,線程1在執行完以下代碼后,線程1的時間片運行結束。線程1運行的結果如圖所示

  線程2與線程1同時運行,線程2的時間片未用完,還在繼續執行,根據代碼的分配策略,線程2直到時間片運行結束,出現如圖所示的結果:

   此時CPU的時間片又被分配到了線程1,線程1繼續運行,因為此時A所在的鏈表結構已經發生了變化,只能處理A,B,D三個元素。此時線程1創建的hashmap如圖:

 

 參考資料

  教你初步了解紅黑樹

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

深入正則表達式(3):正則表達式工作引擎流程分析與原理釋義

作為正則的使用者也一樣,不懂正則引擎原理的情況下,同樣可以寫出滿足需求的正則,但是不知道原理,卻很難寫出高效且沒有隱患的正則。所以對於經常使用正則,或是有興趣深入學習正則的人,還是有必要了解一下正則引擎的匹配原理的。

有興趣可以回顧《深入正則表達式(0):正則表達式概述》

正則引擎類型

正則引擎主要可以分為兩大類:一種是DFA(Deterministic Finite Automatons/確定性有限自動機—),一種是NFA(Nondeterministic Finite Automatons/非確定性有限自動機)。總的來說,

  • DFA可以稱為文本主導的正則引擎

  • NFA可以稱為表達式主導的正則引擎

NFA與DFA工作的區別:

我們常常說用正則去匹配文本,這是NFA的思路,DFA本質上其實是用文本去匹配正則

'for tonight's'.match(/to(nite|knite|night)/);
  • 如果是NFA引擎,表達式佔主導地位。在字符串先查找字符串中的t,然後依次匹配,如果是o,則繼續(以此循環)。匹配到to后,到n,就面臨三種選擇,每一種都去嘗試匹配一下(它也不嫌累),第一個分支也是依次匹配,到t這裏停止(nite分到t這裏直接被淘汰);同理,接着第二個分支在k這裏也停止了;終於在第三個分支柳暗花明,找到了自己的歸宿。 NFA 工作方式是以正則表達式為標準,反覆測試字符串,這樣同樣一個字符串有可能被反覆測試了很多次!

  • 如果是DFA引擎呢,文本佔主導地位。從整個字符串第一個字符開始f開始查找t,查找到t后,定位到t,以知其後為o,則去查看正則表達式其相應位置后是否為o,如果是,則繼續(以此循環),再去查正則表達式o后是否為n(此時淘汰knite分支),再后是否為g(淘汰nite分支),這個時候只剩一個分支,直接匹配到終止即可。

只有正則表達式才有分支和範圍,文本僅僅是一個字符流。這帶來什麼樣的後果?就是NFA引擎在匹配失敗的時候,如果有其他的分支或者範圍,它會返回,記住,返回,去嘗試其他的分支而DFA引擎一旦匹配失敗,就結束了,它沒有退路。

這就是它們之間的本質區別。其他的不同都是這個特性衍生出來的。

NFA VS DFA

首先,正則表達式在計算機看來只是一串符號,正則引擎首先肯定要解析它。NFA引擎只需要編譯就好了;而DFA引擎則比較繁瑣,編譯完還不算,還要遍歷出表達式中所有的可能。因為對DFA引擎來說機會只有一次,它必須得提前知道所有的可能,才能匹配出最優的結果。

所以,在編譯階段,NFA引擎比DFA引擎快

 

其次,DFA引擎在匹配途中一遍過,溜得飛起。相反NFA引擎就比較苦逼了,它得不厭其煩的去嘗試每一種可能性,可能一段文本它得不停返回又匹配,重複好多次。當然運氣好的話也是可以一遍過的。

所以,在運行階段,NFA引擎比DFA引擎慢

 

最後,因為NFA引擎是表達式佔主導地位,所以它的表達能力更強,開發者的控制度更高,也就是說開發者更容易寫出性能好又強大的正則來,當然也更容易造成性能的浪費甚至撐爆CPU。DFA引擎下的表達式,只要可能性是一樣的,任何一種寫法都是沒有差別(可能對編譯有細微的差別)的,因為對DFA引擎來說,表達式其實是死的。而NFA引擎下的表達式,高手寫的正則和新手寫的正則,性能可能相差10倍甚至更多。

也正是因為主導權的不同,正則中的很多概念,比如非貪婪模式、反向引用、零寬斷言等只有NFA引擎才有。

所以,在表達能力上,NFA引擎秒殺DFA引擎

 

但是NFA以表達式為主導,因而NFA更容易操縱,因此一般程序員更偏愛NFA引擎!

當今市面上大多數正則引擎都是NFA引擎,應該就是勝在表達能力上。

 

總體來說,兩種引擎的工作方式完全不同,一個(NFA)以表達式為主導,一個(DFA)以文本為主導!兩種引擎各有所長,而真正的引用則取決與你的需要以及所使用的語言。

這兩種引擎都有了很久的歷史(至今二十多年),當中也由這兩種引擎產生了很多變體!

因為NFA引擎比較靈活,很多語言在實現上有細微的差別。所以後來大家弄了一個標準,符合這個標準的正則引擎就叫做POSIX NFA引擎,其餘的就只能叫做傳統型NFA引擎咯。

Deterministic finite automaton,Non-deterministic finite automaton,Traditional NFA,Portable Operating System Interface for uniX NFA

於是POSIX的出台規避了不必要變體的繼續產生。這樣一來,主流的正則引擎又分為3類:DFA,傳統型NFA,POSIX NFA。

正則引擎三國

DFA引擎

DFA引擎在線性時狀態下執行,因為它們不要求回溯(並因此它們永遠不測試相同的字符兩次)。

DFA引擎還可以確保匹配最長的可能的字符串。但是,因為 DFA 引擎只包含有限的狀態,所以它不能匹配具有反向引用的模式;並且因為它不構造显示擴展,所以它不可以捕獲子表達式。

DFN不回溯,所以匹配快速,因而不支持捕獲組,支持反向引用和$number引用

傳統的 NFA引擎

傳統的 NFA 引擎運行所謂的“貪婪的”匹配回溯算法,以指定順序測試正則表達式的所有可能的擴展並接受第一個匹配項。因為傳統的 NFA 構造正則表達式的特定擴展以獲得成功的匹配,所以它可以捕獲子表達式匹配和匹配的反向引用。但是,因為傳統的 NFA 回溯,所以它可以訪問完全相同的狀態多次(如果通過不同的路徑到達該狀態)。因此,在最壞情況下,它的執行速度可能非常慢。因為傳統的 NFA 接受它找到的第一個匹配,所以它還可能會導致其他(可能更長)匹配未被發現

大多數編程語言和工具使用的是傳統型的NFA引擎,它有一些DFA不支持的特性:

  • 捕獲組、反向引用和$number引用方式;

  • 環視(Lookaround,(?<=…)、(?<!…)、(?=…)、(?!…)),或者有的有文章叫做預搜索;

  • 忽略優化量詞(??、*?、+?、{m,n}?、{m,}?),或者有的文章叫做非貪婪模式;

  • 佔有優先量詞(?+、*+、++、{m,n}+、{m,}+,目前僅Java和PCRE支持),固化分組(?>…)。

POSIX NFA引擎

POSIX NFA引擎主要指符合POSIX標準的NFA引擎,與傳統的 NFA 引擎類似,不同的一點在於:提供longest-leftmost匹配,也就是在找到最左側最長匹配之前,它將繼續回溯(可以確保已找到了可能的最長的匹配之前它們將繼續回溯)。因此,POSIX NFA 引擎的速度慢於傳統的 NFA 引擎;並且在使用 POSIX NFA 時,您恐怕不會願意在更改回溯搜索的順序的情況下來支持較短的匹配搜索,而非較長的匹配搜索。

同DFA一樣,非貪婪模式或者說忽略優先量詞對於POSIX NFA同樣是沒有意義的。

三種引擎的使用情況

  • 使用傳統型NFA引擎的程序主要有(主流):

    • Java、Emacs(JavaScript/actionScript)、Perl、PHP、Python、Ruby、.NET語言

    • VI,GNU Emacs,PCRE library,sed;

  • 使用POSIX NFA引擎的程序主要有:mawk,Mortice Kern Systems’ utilities,GNU Emacs(使用時可以明確指定);

  • 使用DFA引擎的程序主要有:awk,egrep,flex,lex,MySQL,Procmail等;

  • 也有使用DFA/NFA混合的引擎:GNU awk,GNU grep/egrep,Tcl。

 

《精通正則表達式》書中說POSIX NFA引擎不支持非貪婪模式,很明顯JavaScript不是POSIX NFA引擎。

'123456'.match(/\d{3,6}/);
// ["123456", index: 0, input: "123456", groups: undefined]
'123456'.match(/\d{3,6}?/);
// ["123", index: 0, input: "123456", groups: undefined]

JavaScript的正則引擎是傳統型NFA引擎。

為什麼POSIX NFA引擎不支持也沒有必要支持非貪婪模式?

回溯

現在我們知道,NFA引擎是用表達式去匹配文本,而表達式又有若干分支和範圍,一個分支或者範圍匹配失敗並不意味着最終匹配失敗,正則引擎會去嘗試下一個分支或者範圍。

正是因為這樣的機制,引申出了NFA引擎的核心特點——回溯。

首先我們要區分備選狀態和回溯。

什麼是備選狀態?就是說這一個分支不行,那我就換一個分支,這個範圍不行,那我就換一個範圍。正則表達式中可以商榷的部分就叫做備選狀態。

備選狀態可以實現模糊匹配,是正則表達能力的一方面。

回溯可不是個好東西。想象一下,面前有兩條路,你選擇了一條,走到盡頭髮現是條死路,你只好原路返回嘗試另一條路。這個原路返回的過程就叫回溯,它在正則中的含義是吐出已經匹配過的文本。

我們來看兩個例子:

'abbbc'.match(/ab{1,3}c/);
// ["abbbc", index: 0, input: "abbbc", groups: undefined]
'abc'.match(/ab{1,3}c/);
// ["abc", index: 0, input: "abc", groups: undefined]

第一個例子,第一次a匹配a成功,接着碰到貪婪匹配,不巧正好是三個b貪婪得逞,最後用c匹配c成功。

正則 文本
/a/ a
/ab{1,3}/ ab
/ab{1,3}/ abb
/ab{1,3}/ abbb
/ab{1,3}c/ abbbc

第二個例子的區別在於文本只有一個b。所以表達式在匹配第一個b成功後繼續嘗試匹配b,然而它見到的只有黃臉婆c。不得已將c吐出來,委屈一下,畢竟貪婪匹配也只是盡量匹配更多嘛,還是要臣服於匹配成功這個目標。最後不負眾望用c匹配c成功。

正則 文本
/a/ a
/ab{1,3}/ ab
/ab{1,3}/ abc
/ab{1,3}/ ab
/ab{1,3}c/ abc

請問,第二個例子發生回溯了嗎?

並沒有。

誒,你這樣就不講道理了。不是把c吐出來了嘛,怎麼就不叫回溯了?

回溯是吐出已經匹配過的文本。匹配過程中造成的匹配失敗不算回溯

為了讓大家更好的理解,我舉一個例子:

你和一個女孩子(或者男孩子)談戀愛,接觸了半個月後發現實在不合適,於是提出分手。這不叫回溯,僅僅是不合適而已。

你和一個女孩子(或者男孩子)談戀愛,這段關係維持了兩年,並且已經同居。但由於某些不可描述的原因,疲憊掙扎之後,兩人最終還是和平分手。這才叫回溯。

雖然都是分手,但你們應該能理解它們的區別吧。

為了讓大家更好的理解,我舉一個例子:

你和一個女孩子(或者男孩子)談戀愛,接觸了半個月後發現實在不合適,於是提出分手。這不叫回溯,僅僅是不合適而已。

你和一個女孩子(或者男孩子)談戀愛,這段關係維持了兩年,並且已經同居。但由於某些不可描述的原因,疲憊掙扎之後,兩人最終還是和平分手。這才叫回溯。

雖然都是分手,但你們應該能理解它們的區別吧。

網絡上有很多文章都認為上面第二個例子發生了回溯。至少根據我查閱的資料,第二個例子發生的情況不能被稱為回溯。當然也有可能我([馬蹄疾]是錯的,歡迎討論。

我們再來看一個真正的回溯例子:

'ababc'.match(/ab{1,3}c/);
// ["abc", index: 2, input: "ababc", groups: undefined]

匹配文本到ab為止,都沒什麼問題。後面既匹配不到b,也匹配不到c。引擎只好將文本ab吐出來,從下一個位置開始匹配。因為上一次是從第一個字符a開始匹配,所以下一個位置當然就是從第二個字符b開始咯。

正則 文本
/a/ a
/ab{1,3}/ ab
/ab{1,3}/ aba
/ab{1,3}/ ab
/ab{1,3}c/ aba
/a/ ab
/a/ aba
/ab{1,3}/ abab
/ab{1,3}/ ababc
/ab{1,3}/ abab
/ab{1,3}c/ ababc

一開始引擎是以為會和最早的ab走完餘生的,然而命運弄人,從此天涯。

這他媽才叫回溯!

還有一個細節。上面例子中的回溯並沒有往回吐呀,吐出來之後不應該往回走嘛,怎麼往後走了?

我們再來看一個例子:

'"abc"def'.match(/".*"/);
// [""abc"", index: 0, input: ""abc"def", groups: undefined]

因為.*是貪婪匹配,所以它把後面的字符都吞進去了。直到發現目標完不成,不得已往回吐,吐到第二個”為止,終於匹配成功。這就好比結了婚還在外面養小三,幾經折騰才發現家庭才是最重要的,自己的行為背離了初衷,於是幡然悔悟。

正則 文本
/”/
/”.*/ “a
/”.*/ “ab
/”.*/ “abc
/”.*/ “abc”
/”.*/ “abc”d
/”.*/ “abc”de
/”.*/ “abc”def
/”.*”/ “abc”def
/”.*”/ “abc”de
/”.*”/ “abc”d
/”.*”/ “abc”

我想說的是,不要被回溯的回字迷惑了。它的本質是把已經吞進去的字符吐出來。至於吐出來之後是往回走還是往後走,是要根據情況而定的。

優化正則表達式

現在我們知道了控制回溯是控制正則表達式性能的關鍵。

控制回溯又可以拆分成兩部分:第一是控製備選狀態的數量,第二是控製備選狀態的順序。

備選狀態的數量當然是核心,然而如果備選狀態雖然多,卻早早的匹配成功了,早匹配早下班,也就沒那麼多糟心事了。

傳統NFA工作流程

許多因素影響正則表達式的效率,首先,正則表達式適配的文本千差萬別,部分匹配時比完全不匹配所用的時間要長。上面提到過,JavaScript是傳統NFA引擎,當然每種瀏覽器的正則表達式引擎也有不同的內部優化。

為了有效地使用正則表達式,重要的是理解它們的工作原理。下面是一個正則表達式處理的基本步驟:

第一步:編譯

當你創建了一個正則表達式對象之後(使用一個正則表達式直接量或者RegExp構造器),瀏覽器檢查你的模板有沒有錯誤,然後將它轉換成一個本機代碼例程,用於執行匹配工作。如果你將正則表達式賦給一個變量,你可以避免重複執行此步驟。

第二步:設置起始位置

當一個正則表達式投入使用時,首先要確定目標字符串中開始搜索的位置。它是字符串的起始位置,或由正則表達式的lastIndex屬性指定,但是當它從第四步返回到這裏的時候(因為嘗試匹配失敗),此位置將位於最後一次嘗試起始位置推后一個字符的位置上。

      瀏覽器優化正則表達式引擎的辦法是,在這一階段中通過早期預測跳過一些不必要的工作。例如,如果一個正則表達式以^開頭,IE 和Chrome通常判斷在字符串起始位置上是否能夠匹配,然後可避免愚蠢地搜索後續位置。另一個例子是匹配第三個字母是x的字符串,一個聰明的辦法是先找到x,然後再將起始位置回溯兩個字符。

第三步:匹配每個正則表達式的字元

      正則表達式一旦找好起始位置,它將一個一個地掃描目標文本和正則表達式模板。當一個特定字元匹配失敗時,正則表達式將試圖回溯到掃描之前的位置上,然後進入正則表達式其他可能的路徑上。

      第四步:匹配成功或失敗

      如果在字符串的當前位置上發現一個完全匹配,那麼正則表達式宣布成功。如果正則表達式的所有可能路徑都嘗試過了,但是沒有成功地匹配,那麼正則表達式引擎回到第二步,從字符串的下一個字符重新嘗試。只有字符串中的每個字符(以及最後一個字符後面的位置)都經歷了這樣的過程之後,還沒有成功匹配,那麼正則表達式就宣布徹底失敗。

      牢記這一過程將有助於您明智地判別那些影響正則表達式性能問題的類型。

 

工具

[ regex101 ]是一個很多人推薦過的工具,可以拆分解釋正則的含義,還可以查看匹配過程,幫助理解正則引擎。如果只能要一個正則工具,那就是它了。

[ regexper ]是一個能讓正則的備選狀態可視化的工具,也有助於理解複雜的正則語法。

 

參考文章:

 https://baike.baidu.com/item/正則表達式

正則表達式工作原理 https://www.cnblogs.com/aaronjs/archive/2012/06/30/2570800.html

一次性搞懂JavaScript正則表達式之引擎 https://juejin.im/post/5becc2aef265da6110369c93

 

轉載本站文章《深入正則表達式(3):正則表達式工作引擎流程分析與原理釋義》,
請註明出處:https://www.zhoulujun.cn/html/theory/algorithm/IntroductionAlgorithms/8430.html

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

Spring Boot 集成 Swagger 構建接口文檔

在應用開發過程中經常需要對其他應用或者客戶端提供 RESTful API 接口,尤其是在版本快速迭代的開發過程中,修改接口的同時還需要同步修改對應的接口文檔,這使我們總是做着重複的工作,並且如果忘記修改接口文檔,就可能造成不必要的麻煩。

為了解決這些問題,Swagger 就孕育而生了,那讓我們先簡單了解下。

Swagger 簡介

Swagger 是一個規範和完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務

總體目標是使客戶端和文件系統作為服務器,以同樣的速度來更新。文件的方法、參數和模型緊密集成到服務器端的代碼中,允許 API 始終保持同步。

下面我們在 Spring Boot 中集成 Swagger 來構建強大的接口文檔。

Spring Boot 集成 Swagger

Spring Boot 集成 Swagger 主要分為以下三步:

  1. 加入 Swagger 依賴
  2. 加入 Swagger 文檔配置
  3. 使用 Swagger 註解編寫 API 文檔

加入依賴

首先創建一個項目,在項目中加入 Swagger 依賴,項目依賴如下所示:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

加入配置

接下來在 config 包下創建一個 Swagger 配置類 Swagger2Configuration,在配置類上加入註解 @EnableSwagger2,表明開啟 Swagger,注入一個 Docket 類來配置一些 API 相關信息,apiInfo() 方法內定義了幾個文檔信息,代碼如下:

@Configuration
@EnableSwagger2
public class Swagger2Configuration {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // swagger 文檔掃描的包
                .apis(RequestHandlerSelectors.basePackage("com.wupx.interfacedoc.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("測試接口列表")
                .description("Swagger2 接口文檔")
                .version("v1.0.0")
                .contact(new Contact("wupx", "https://www.tianheyu.top", "wupx@qq.com"))
                .license("Apache License, Version 2.0")
                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
                .build();
    }
}

列舉其中幾個文檔信息說明下:

  • title:接口文檔的標題
  • description:接口文檔的詳細描述
  • termsOfServiceUrl:一般用於存放公司的地址
  • version:API 文檔的版本號
  • contact:維護人、維護人 URL 以及 email
  • license:許可證
  • licenseUrl:許可證 URL

編寫 API 文檔

domain 包下創建一個 User 實體類,使用 @ApiModel 註解表明這是一個 Swagger 返回的實體,@ApiModelProperty 註解表明幾個實體的屬性,代碼如下(其中 getter/setter 省略不显示):

@ApiModel(value = "用戶", description = "用戶實體類")
public class User {

    @ApiModelProperty(value = "用戶 id", hidden = true)
    private Long id;

    @ApiModelProperty(value = "用戶姓名")
    private String name;

    @ApiModelProperty(value = "用戶年齡")
    private String age;

    // getter/setter
}

最後,在 controller 包下創建一個 UserController 類,提供用戶 API 接口(未使用數據庫),代碼如下:

@RestController
@RequestMapping("/users")
@Api(tags = "用戶管理接口")
public class UserController {

    Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());

    @GetMapping("/")
    @ApiOperation(value = "獲取用戶列表", notes = "獲取用戶列表")
    public List<User> getUserList() {
        return new ArrayList<>(users.values());
    }

    @PostMapping("/")
    @ApiOperation(value = "創建用戶")
    public String addUser(@RequestBody User user) {
        users.put(user.getId(), user);
        return "success";
    }

    @GetMapping("/{id}")
    @ApiOperation(value = "獲取指定 id 的用戶")
    @ApiImplicitParam(name = "id", value = "用戶 id", paramType = "query", dataTypeClass = Long.class, defaultValue = "999", required = true)
    public User getUserById(@PathVariable Long id) {
        return users.get(id);
    }

    @PutMapping("/{id}")
    @ApiOperation(value = "根據 id 更新用戶")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "用戶 id", defaultValue = "1"),
            @ApiImplicitParam(name = "name", value = "用戶姓名", defaultValue = "wupx"),
            @ApiImplicitParam(name = "age", value = "用戶年齡", defaultValue = "18")
    })
    public User updateUserById(@PathVariable Long id, @RequestParam String name, @RequestParam Integer age) {
        User user = users.get(id);
        user.setName(name);
        user.setAge(age);
        return user;
    }

    @DeleteMapping("/{id}")
    @ApiOperation(value = "刪除用戶", notes = "根據 id 刪除用戶")
    @ApiImplicitParam(name = "id", value = "用戶 id", dataTypeClass = Long.class, required = true)
    public String deleteUserById(@PathVariable Long id) {
        users.remove(id);
        return "success";
    }
}

啟動項目,訪問 http://localhost:8080/swagger-ui.html,可以看到我們定義的文檔已經在 Swagger 頁面上显示了,如下圖所示:

到此為止,我們就完成了 Spring Boot 與 Swagger 的集成。

同時 Swagger 除了接口文檔功能外,還提供了接口調試功能,以創建用戶接口為例,單擊創建用戶接口,可以看到接口定義的參數、返回值、響應碼等,單擊 Try it out 按鈕,然後點擊 Execute 就可以發起調用請求、創建用戶,如下圖所示:

註解介紹

由於 Swagger 2 提供了非常多的註解供開發使用,這裏列舉一些比較常用的註解。

@Api

@Api 用在接口文檔資源類上,用於標記當前類為 Swagger 的文檔資源,其中含有幾個常用屬性:

  • value:定義當前接口文檔的名稱。
  • description:用於定義當前接口文檔的介紹。
  • tag:可以使用多個名稱來定義文檔,但若同時存在 tag 屬性和 value 屬性,則 value 屬性會失效。
  • hidden:如果值為 true,就會隱藏文檔。

@ApiOperation

@ApiOperation 用在接口文檔的方法上,主要用來註解接口,其中包含幾個常用屬性:

  • value:對API的簡短描述。
  • note:API的有關細節描述。
  • esponse:接口的返回類型(注意:這裏不是返回實際響應,而是返回對象的實際結果)。
  • hidden:如果值為 true,就會在文檔中隱藏。

@ApiResponse、@ApiResponses

@ApiResponses 和 @ApiResponse 二者配合使用返回 HTTP 狀態碼。@ApiResponses 的 value 值是 @ApiResponse 的集合,多個 @ApiResponse 用逗號分隔,其中 @ApiResponse 包含的屬性如下:

  • code:HTTP狀態碼。
  • message:HTTP狀態信息。
  • responseHeaders:HTTP 響應頭。

@ApiParam

@ApiParam 用於方法的參數,其中包含以下幾個常用屬性:

  • name:參數的名稱。
  • value:參數值。
  • required:如果值為 true,就是必傳字段。
  • defaultValue:參數的默認值。
  • type:參數的類型。
  • hidden:如果值為 true,就隱藏這個參數。

@ApiImplicitParam、@ApiImplicitParams

二者配合使用在 API 方法上,@ApiImplicitParams 的子集是 @ApiImplicitParam 註解,其中 @ApiImplicitParam 註解包含以下幾個參數:

  • name:參數的名稱。
  • value:參數值。
  • required:如果值為 true,就是必傳字段。
  • defaultValue:參數的默認值。
  • dataType:數據的類型。
  • hidden:如果值為 true,就隱藏這個參數。
  • allowMultiple:是否允許重複。

@ResponseHeader

API 文檔的響應頭,如果需要設置響應頭,就將 @ResponseHeader 設置到 @ApiResponseresponseHeaders 參數中。@ResponseHeader 提供了以下幾個參數:

  • name:響應頭名稱。
  • description:響應頭備註。

@ApiModel

設置 API 響應的實體類,用作 API 返回對象。@ApiModel 提供了以下幾個參數:

  • value:實體類名稱。
  • description:實體類描述。
  • subTypes:子類的類型。

@ApiModelProperty

設置 API 響應實體的屬性,其中包含以下幾個參數:

  • name:屬性名稱。
  • value:屬性值。
  • notes:屬性的註釋。
  • dataType:數據的類型。
  • required:如果值為 true,就必須傳入這個字段。
  • hidden:如果值為 true,就隱藏這個字段。
  • readOnly:如果值為 true,字段就是只讀的。
  • allowEmptyValue:如果為 true,就允許為空值。

到此為止,我們就介紹完了 Swagger 提供的主要註解。

總結

Swagger 可以輕鬆地整合到 Spring Boot 中構建出強大的 RESTful API 文檔,可以減少我們編寫接口文檔的工作量,同時接口的說明內容也整合入代碼中,可以讓我們在修改代碼邏輯的同時方便的修改接口文檔說明,另外 Swagger 也提供了頁面測試功能來調試每個 RESTful API。

如果項目中還未使用,不防嘗試一下,會發現效率會提升不少。

本文的完整代碼在 https://github.com/wupeixuan/SpringBoot-Learn 的 interface-doc 目錄下。

最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。

參考

http://swagger.io

https://github.com/wupeixuan/SpringBoot-Learn

《Spring Boot 2 實戰之旅》

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

c#中的值類型和引用類型

值類型和引用類型,是c#比較基礎,也必須掌握的知識點,但是也不是那麼輕易就能掌握,今天跟着老胡一起來看看吧。
 

典型類型

首先我們看看這兩種不同的類型有哪些比較典型的代表。
 

典型值類型

int, long, float, double等原始類型中表示数字的類型都是值類型,表示時間的datatime也是值類型,除此之外我們還可以通過關鍵字struct自定義值類型。
 

典型引用類型

原始類型中,array, list, dictionary, queue, stack和string都是引用類型,除此之外我們通過關鍵字class自定義引用類型。
 

基類

c#中所有的類型都最終繼承自Object,這是沒有疑問的,但是這其中還有些微區別。
 

值類型基類

對於值類型來說,除了最終繼承自Object,還繼承自ValueType,繼承鏈如下

但是請不要誤解,這裏僅僅指的是值類型天然是ValueType,但是不代表值類型能夠這麼聲明

struct Struct1 : ValueType
{

}

這樣是會引起編譯錯誤的,值類型不能繼承任何其他類型,值類型只能實現接口,不能繼承自其它類型。只有引用類型既可以實現接口也能繼承自其它類型。順便說一下,還有一點比較重要的是,ValueType重寫了Object基類的Equals方法和GetHashCode方法,所以當使用Equals比較兩個值類型的時候,系統會比較兩個值類型的各個屬性是否相等,再返回結果,這就是所謂的相等性。與此相對,引用類型在使用Equals的時候,會在後台調用object.ReferenceEquals,換言之,引用類型在比較相等性的時候會考慮同一性
 

引用類型基類

對於引用類型就沒有那麼麻煩,引用類型不會繼承自ValueType。引用類型可以繼承其他類型。
 

在內存中的表現

我們都知道,C#將內存分為了兩部分,一個是Stack,另外一個是Managed Heap。一般來說,用於函數調用進棧,函數返回出棧,用的是Stack,而當創造一個新的實例時,會根據創建的實例屬於值類型還是引用類型決定使用Stack還是Managed Heap。
 

值類型在內存中

當創建一個值類型對象時,c#會在Stack上面創建一塊空間,這塊空間就存放這個值類型對象。
int是一個典型的值類型,如下語句

int age = 10;

會存在於內存中的Stack上面。

如果把值類型的實例賦值給另外一個值類型,那麼效果就是複製一個新的值類型實例。

int myAge = age;

 

引用類型在內存中

與值類型在內存中的表現不一樣,創建一個引用類型的實例,不但會在Stack上面新建一個引用,還會在Heap上面劃分出內存以容納該引用類型實例。用戶在使用的時候通過Stack上面的變量間接引用該實例。

class Author
{
	public string Name{get;set;}
	public int Age{get;set;}
}

Author author = new Author(){Name="deatharthas", Age= 32};

注意看和值類型在內存中的區別,引用類型通過Stack上的變量訪問位於Heap上面的實例。
在賦值的時候,拷貝的僅僅是Stack上面的變量,新拷貝出來的對象和舊的對象指向的是同一塊內存。

Author myAuthor = author;

這個時候,author和myAuthor指向同一塊內存,稱為同一性,通過調用

object.ReferenceEquals(myAuthor, author);

可以得到驗證。
 
但可能有細心的朋友會有疑問了,不是說int是值類型,值類型是存在於Stack上面的嗎?為什麼在author類裏面,它會在Heap裏面呢?贊一個細心!值類型一般存在於Stack上面,但如果某個值類型包含於引用類型,那麼它也會隨着那個引用類型存放在Heap上面。
 

當參數時的行為區別

c#中的參數傳遞默認都是傳值(by value),但是根據所傳遞對象是值類型還是引用類型,它們的行為還是有所區別,現在我們來看看。

值類型當參數

值類型當參數的時候,傳遞到函數內部的是一份值類型的拷貝,所以在函數內部修改這個拷貝不會影響原對象。除非我們在傳遞參數的時候使用了ref或者out。
 

引用類型當參數

如果參數是引用類型,傳遞到函數內部的依然是一份拷貝,但是這個拷貝是其在Stack上面的變量的拷貝,就像上面的賦值那個例子。所以這個時候這份拷貝其實和原對象指向同一塊內存(指向同一性),修改這個對象可以反映到原對象上面。
 

謹慎返回引用類型

編程是一項需要謹慎的工作,有時候我們經常會犯一些錯誤,而這些錯誤又是那麼的不明顯以至於不摔坑幾次,我們根本察覺不了,考慮下面一個例子。

    class People
    {
        public string Name { get; set; }
        public int Age { get; set; }
        private People _Father = null;
        public People Father { get { return _Father; } }
        public People(People father)
        {
            _Father = father;
        }
        public void ShowFather()
        {
            Console.WriteLine("father's name is " + Father.Name + " and his age is " + Father.Age);
        }
    }

    class Program
    {        
        static void Main(string[] args)
        {
            People father = new People(null) { Name = "father", Age = 60 };
            People son = new People(father);
            son.ShowFather();
            Console.ReadLine();
        }
    }

看起來沒什麼問題,對吧?Father沒有提供setter,似乎是安全的。但是我們試試下面的代碼。

	static void Main(string[] args)
        {
            People father = new People(null) { Name = "father", Age = 60 };
            People son = new People(father);
            var f = son.Father;
            f.Name="Changed";
            son.ShowFather();
            Console.ReadLine();
        }

看,發現了什麼,外部改變了本來應該被封裝所保護的Father屬性,封裝被破壞了!
稍微一想我們應該能明白這個道理,Father屬性返回的拷貝的變量和原Father變量指向同一塊實例。要想解決這個問題,我們要麼返回一個值類型,要麼返回一個全新的對象。修改Father屬性如下:

public People Father { get { return new People(_Father._Father) { Name = _Father.Name, Age = _Father.Age }; } }

再次測試,

這次封裝就沒問題了。
 

總結

我們大概知道了值類型和引用類型的區別,包括它們的行為,在內存的居住方式,以及使用引用類型時可能會遇到的暗坑,希望大家通過閱讀這篇文章,能夠加深一些對它們的了解,少走一些彎路。
今天也簡單的提到了比較時的同一性,和預防封裝被破壞所採用的返回一個新的實例拷貝的策略(這個時候適合使用DeepCopy),我們之後有機會再詳細聊。

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

【其他文章推薦】

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

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

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

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

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

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

系統化學習多線程(一)

大綱

————————-學前必讀———————————-

學習不能快速成功,但一定可以快速入門
整體課程思路:
1.實踐為主,理論化偏少
2.課程筆記有完整的案例和代碼,(為了學習效率)再開始之前我會簡單粗暴的介紹知識點案例思路,
有基礎的同學聽了之後可以直接結合筆記寫代碼,
如果沒聽懂再向下看視頻,我會手把手編寫代碼和演示測試結果;
3.重要提示,學編程和學游泳一樣,多實踐學習效率才高,理解才透徹;
4.編碼功底差的建議每個案例代碼寫三遍,至於為什麼…<<賣油翁>>…老祖宗的智慧

————————————————————————-

 1.線程

1.1.什麼是線程

線程(英語:thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程并行執行不同的任務。在Unix System V及SunOS中也被稱為輕量進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱為線程。(來自百度百科)

一個進程可以有很多線程,每條線程并行執行不同的任務。

1.2.多線程hello word

需求:模擬在計算上一邊聽歌一邊打遊戲

三種實現方案如下:

TestDemo

 1 package com.wfd360.thread;
 2 
 3 import com.wfd360.thread.demo01.GameRunnable;
 4 import com.wfd360.thread.demo01.MusicRunnable;
 5 import com.wfd360.thread.demo02.GameThread;
 6 import com.wfd360.thread.demo02.MusicThread;
 7 import org.junit.Test;
 8 
 9 /**
10  * @author 姿勢帝-博客園
11  * @address https://www.cnblogs.com/newAndHui/
12  * @WeChat 851298348
13  * @create 05/03 5:27
14  * @description 需求分析:
15  * 1.模擬一邊打遊戲一邊聽音樂,在控制台打印輸出模擬
16  * 2.把兩個業務封裝成獨立的線程,實現接口Runnable或繼承Thread,通過看源碼你會發現Thread類實現了接口Runnable,使用本質上這兩種方法時一樣的。
17  * 3.Thread類提供兩個方法,線程主題方法run,啟動線程方法start
18  */
19 public class TestDemo {
20     /**
21      * 方式1:實現接口Runnable
22      */
23     @Test
24     public void testRunnable() throws InterruptedException {
25         System.out.println("-------test start-------");
26         // 實例對象
27         MusicRunnable music = new MusicRunnable();
28         GameRunnable game = new GameRunnable();
29         // 創建線程
30         Thread musicThread = new Thread(music);
31         Thread gameThread = new Thread(game);
32         // 啟動線程
33         musicThread.start();
34         gameThread.start();
35         System.out.println("--------等待其他線程執行--------------");
36         Thread.sleep(5 * 1000);
37         System.out.println("-------test end-------");
38     }
39 
40     /**
41      * 方式2:繼承Thread
42      */
43     @Test
44     public void testThread() throws InterruptedException {
45         System.out.println("-------test start-------");
46         // 創建線程
47         MusicThread musicThread = new MusicThread();
48         GameThread gameThread = new GameThread();
49         // 啟動線程
50         musicThread.start();
51         gameThread.start();
52         System.out.println("--------等待其他線程執行--------------");
53         Thread.sleep(5 * 1000);
54         System.out.println("-------test end-------");
55     }
56 
57     /**
58      * 方式3:簡寫,這種寫法一般我們在做模擬測試的使用,在正式代碼中建議不使用,可讀性較差
59      */
60     @Test
61     public void testThreadSimple() throws InterruptedException {
62         System.out.println("-------test start-------");
63         // 創建線程
64         Thread musicThread = new Thread(() -> {
65             for (int i = 0; i < 100; i++) {
66                 System.out.println("=======聽音樂中============" + i);
67             }
68         });
69         Thread gameThread = new Thread(() -> {
70             for (int i = 0; i < 100; i++) {
71                 System.out.println("=======打遊戲中============" + i);
72             }
73         });
74         // 啟動線程
75         musicThread.start();
76         gameThread.start();
77         System.out.println("--------等待其他線程執行--------------");
78         Thread.sleep(5 * 1000);
79         System.out.println("-------test end-------");
80     }
81 }


 實現接口Runnable

 1 package com.wfd360.thread.demo01;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 5:31
 8  * @description
 9  */
10 public class GameRunnable implements Runnable {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("=======打遊戲中============" + i);
15         }
16     }
17 }

GameRunnable

 1 package com.wfd360.thread.demo01;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 5:29
 8  * @description
 9  */
10 public class MusicRunnable implements Runnable {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("=======聽音樂中============"+i);
15         }
16     }
17 }

MusicRunnable

 繼承Thread

 1 package com.wfd360.thread.demo02;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 6:00
 8  * @description
 9  */
10 public class GameThread extends Thread {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("-------遊戲中----------"+i);
15         }
16     }
17 }

GameThread GameThread

 1 package com.wfd360.thread.demo02;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/03 6:00
 8  * @description
 9  */
10 public class MusicThread extends Thread {
11     @Override
12     public void run() {
13         for (int i = 0; i < 100; i++) {
14             System.out.println("-------音樂中----------" + i);
15         }
16     }
17 }


 總結

啟動線程兩種方式:

    1.通過繼承Thread類

    2.實現Runnable接口

 使用哪種方式更好?

區別: 

一個類如果繼承了其他類,就無法在繼承Thread類,在Java中,一個類只能繼承一個類,而一個類如果實現了一個接口,還可以實現其他接口,接口是可以多實現的,所以說

Runable的擴展性更強,但是繼承的方式更簡單,個人建議一般情況下使用Thread;

實現接口Runnable或繼承Thread,通過看源碼你會發現Thread類實現了接口Runnable,使用本質上這兩種方法是一樣的

啟動線程流程:

    創建啟動線程的方式一:繼承Thread類

       1.將業務方法封裝成線程對象,自定義類t extends Thread類; 

       2.覆寫run方法: 覆寫第一步中的run方法;

       3.創建自定義對象t

       4.啟動線程 t.start();

   創建啟動線程方式二:實現Runnable接口

      1.將業務方法封裝成線程對象,自定義類t implements Runnable接口;

      2.實現第一步中的run方法

      3.創建自定義對象t

      4.啟動線程 new Thread(t).start();

1.3.對主線程與創建線程執行順序的理解

問題:
直接寫一個簡單的HelloWorld 程序,有沒有線程?
==>有一個主線程,在垃圾回收的時候,有gc 線程。

 1 package com.wfd360.thread;
 2 
 3 import org.junit.Test;
 4 
 5 /**
 6  * @author 姿勢帝-博客園
 7  * @address https://www.cnblogs.com/newAndHui/
 8  * @WeChat 851298348
 9  * @create 05/04 11:09
10  * @description <p>
11  * 問題:
12  * 直接寫一個簡單的HelloWorld 程序,有沒有線程?
13  * ==>有一個主線程,在垃圾回收的時候,有gc 線程。
14  * 結論:一旦線程啟動起來之後就是獨立的,和創建環境沒有關係;
15  * 啟動線程不能直接調用run方法,必須調用start方法;
16  * </p>
17  */
18 public class TestDemo02 {
19     /**
20      * 如果把創建線程放在循環語句的 下 面,會交替出現嗎
21      * ==>否,因為主線程執行完成后才會啟動hello線程
22      *
23      * @throws Exception
24      */
25     @Test
26     public void test1() throws Exception {
27         System.out.println("---test start-------");
28         // 執行主線程
29         for (int i = 0; i < 100; i++) {
30             System.out.println("-----test1--------" + i);
31         }
32         // 啟動hello線程
33         new HelloThread().start();
34         System.out.println("=======等待執行完成===========");
35         Thread.sleep(5 * 1000);
36         System.out.println("---test end-------");
37     }
38 
39     /**
40      * 如果把創建線程放在循環語句的 上 面,會交替出現嗎
41      * ==>可能會,可能不會,可能出現for循環完之後,線程還沒有啟動完;
42      *
43      * @throws Exception
44      */
45     @Test
46     public void test2() throws Exception {
47         System.out.println("---test start-------");
48         // 啟動hello線程
49         new HelloThread().start();
50         // 執行主線程
51         for (int i = 0; i < 100; i++) {
52             System.out.println("-----test1--------" + i);
53         }
54         System.out.println("=======等待執行完成===========");
55         Thread.sleep(5 * 1000);
56         System.out.println("---test end-------");
57     }
58 
59     /**
60      * 採用內部類的方式定義一個hello線程對象
61      */
62     class HelloThread extends Thread {
63         @Override
64         public void run() {
65             for (int i = 0; i < 100; i++) {
66                 System.out.println("-----HelloThread--------" + i);
67             }
68         }
69     }
70 }

TestDemo02

結論:一旦線程啟動起來之後就是獨立的,和創建環境沒有關係;
啟動線程不能直接調用run方法,必須調用start方法;

 1.4.對sleep方法的理解

package com.wfd360.thread;

/**
 * @author 姿勢帝-博客園
 * @address https://www.cnblogs.com/newAndHui/
 * @WeChat 851298348
 * @create 05/04 11:34
 * @description <p>
 * Thread類的方法:
 * static void sleep(long millis) 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行);
 * </p>
 */
public class TestSleep {
    /**
     * 做一個簡易倒計時,10秒鐘,控制台每一秒輸出一個数字,如10,9,8,7.....0
     */
    public static void main(String[] args) throws Exception {
        System.out.println("---test start-------");
        for (int i = 10; i >= 0; i--) {
            Thread.sleep(1 * 1000);
            System.out.println(i);
        }
        System.out.println("---test end-------");
    }
}

1.5.線程名稱的設置與獲取

繼承方式

簡單需求:使用多線程模擬多窗口售票

 1 package com.wfd360.thread.demo03Ticket;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/04 11:55
 8  * @description <p>
 9  * 模擬多線程售票
10  * </p>
11  */
12 public class TicketThread extends Thread {
13     // 假定票總是100張
14     private static Integer num = 100;
15 
16     @Override
17     public void run() {
18         // 只要有票就一直售票
19         while (num > 0) {
20             System.out.println("正在出售第" + num + "張票");
21             --num;
22         }
23         System.out.println("===售票結束===");
24     }
25 }

TicketThread

test

/**
     * 測試模擬三個窗口售票
     * @throws InterruptedException
     */
    @Test
    public void testTicketThread() throws InterruptedException {
        System.out.println("---test start-------");
        // 模擬多3個窗口售票
        TicketThread ticketThread1 = new TicketThread();
        TicketThread ticketThread2 = new TicketThread();
        TicketThread ticketThread3 = new TicketThread();
        // 啟動線程售票
        ticketThread1.start();
        ticketThread2.start();
        ticketThread3.start();
        System.out.println("======等待售票============");
        Thread.sleep(5 * 1000);
        System.out.println("---test end-------");
    }

結果:

1.在售票過程中不能區分售出的票是那個窗口售出的,解決通過線程名稱判斷

2.有重複售出的票(後面的線程同步解決)

解決第一個問題,設置獲取線程名稱,通過Thread對象裏面自帶的getName,setName方法

 具體代碼

設置線程名稱

 獲取線程名稱

 上面講了繼承的方式獲取線程名稱,那麼實現接口Runnable的方式怎麼獲取設置勒

繼承Thread的方式,可以通過getName的方式獲取當前線程的名稱?
那使用Runnable的方式,能通過getName獲取嘛?

getName方法是Thread類的,但是TicketThread現在並沒有繼承Thread類,而是實現了Runnable接口.

問題:如果實現Runnable接口,怎麼獲取線程名稱?

 思考:TicketThread類裏面的代碼要執行,它肯定存在於某個線程中, 就比如寫個helloword打印語句,是不是也處於一個主線程中,那這裏怎麼獲取線程名稱?
通過動態獲取,當程序正在執行的時候,獲取當前正在執行的線程名稱。怎麼獲取?
在Thread類裏面有個靜態的方法currentThread() 方法,返回當前正在執行的線程引用;

Thread.currentThread().getName

那怎麼設置線程名稱?

Thread類裏面有個name字段,相當於Thread類把它包裝了一下:

通過源碼可以發現,構造方法裏面還有可以傳一個名字:

具體實現代碼如下

 

 總結:

繼承方式設置\獲取線程名稱通過 Thread對象裏面的 setName,getName方法;

實現接口方式設置名稱通過 new Thread(‘線程實例對象’, “線程名稱”),獲取線程名稱通過:Thread.currentThread().getName

1.6.Thread的join方法

void join() 方法 :等待該線程終止
void join(long millis) 方法 :等待該線程終止的時間最長為millis毫秒

需求: 當主線程運行到20的時候(i =20)的時候,讓JoinThread線程加進來直到執行完成,在執行主線程.

 1 package com.wfd360.thread;
 2 
 3 import org.junit.Test;
 4 
 5 /**
 6  * @author 姿勢帝-博客園
 7  * @address https://www.cnblogs.com/newAndHui/
 8  * @WeChat 851298348
 9  * @create 05/04 6:31
10  * @description
11  */
12 public class Test05Join {
13     /**
14      * 需求:
15      * 當主線程for循環到i=20時,等JoinThread線程執行完成后,在執行for循環的線程
16      * @throws InterruptedException
17      */
18     @Test
19     public void testJoinThread() throws InterruptedException {
20         System.out.println("---test start-------");
21         // 開啟線程
22         JoinThread thread = new JoinThread();
23         thread.start();
24         // 循環打印線程
25         for (int i = 0; i < 100; i++) {
26             System.out.println("======testJoinThread=========="+i);
27             Thread.sleep(1);
28             if (i==20){
29                 // 等線程JoinThread執行完成
30                 thread.join();
31             }
32         }
33         System.out.println("=============等待線程執行完成===================");
34         Thread.sleep(10*1000);
35         System.out.println("---test end-------");
36     }
37     
38     class JoinThread extends Thread {
39         @Override
40         public void run() {
41             for (int i = 0; i < 100; i++) {
42                 System.out.println("=====JoinThread=======" + i);
43                 // 模擬處理很多業務耗時1毫秒
44                 try {
45                     Thread.sleep(1);
46                 } catch (InterruptedException e) {
47                     e.printStackTrace();
48                 }
49             }
50         }
51     }
52 }

Test05Join

1.7.線程優先級

直接上代碼

 1 package com.wfd360.thread;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/05 8:17
 8  * @description <p>
 9  * 1.==>線程優先級的理解:
10  * 線程的優先級和生活中類似,高優先級線程的執行優先於低優先級線程;
11  * 並不是絕對的,可能優先級高的線程優先 比 優先級低的線程先執行,只能說,高優先級的線程優先執行的幾率更多;
12  * (比如兩個線程,一個優先級高,一個優先級低,如果一共運行一個小時,優先級高的線程執行遠遠大於優先級低的但是並不是說優先級高的先執行完,
13  * 在執行優先級低的)
14  * 2.==>重新設置線程優先級
15  * int getPriority() 返回線程的優先級。
16  * void setPriority(int newPriority) 更改線程的優先級。Java線程的優先級從1到10級別,值越大優先級越高.
17  * 3.==>線程的默認優先級受創建線程的環境影響,默認值5,自定義線程的默認優先級和創建它的環境的線程優先級一致
18  * </p>
19  */
20 public class Test06Priority {
21     /**
22      * 測試獲取線程優先級,設置線程優先級,驗證線程優先級受創建環境影響
23      * @param args
24      */
25     public static void main(String[] args) {
26         Thread threadMain = Thread.currentThread();
27         // 獲取默認優先級数字
28         System.out.println("main線程默認優先級:" +threadMain.getPriority());// 5
29         // 重新設置默認優先級数字
30         threadMain.setPriority(8);
31         // 再次重新獲取優先級数字
32         System.out.println("main線程修改后的優先級:" +threadMain.getPriority());// 8
33         // 創建一個線程查看優先級
34         Thread thread = new Thread();
35         System.out.println("thread線程的優先級:" +thread.getPriority());// 8 受創建環境影響
36     }
37 }

1.8.後台線程,即守護線程

直接看代碼

 1 package com.wfd360.thread;
 2 
 3 import com.wfd360.thread.demo04Daemon.DaemonThreaad;
 4 
 5 /**
 6  * @author 姿勢帝-博客園
 7  * @address https://www.cnblogs.com/newAndHui/
 8  * @WeChat 851298348
 9  * @create 05/05 9:12
10  * @description <p>
11  * 後台線程,即守護線程
12  * 後台線程:指為其他線程提供服務的線程,也稱為守護線程。JVM的垃圾回收線程就是一個後台線程。
13  * 需求:嘗試把線程標記為後台線程或者標記為(前台)線程;
14  * Thread類提供的方法:
15  * 方法1: void setDaemon(boolean on) 將該線程標記為守護線程或用戶線程,true為後台線程,false為用戶線程(前台線下)
16  * 怎樣測試該線程是否是守護線程?
17  * 方法2:isDaemon()  測試該線程是否為守護線程. true為後台線程,false為用戶線程(前台線下)
18  * <p>
19  * 結論1:活動的線程(已經在執行的線程t.start())不能設置後台線程,即主線程不能設置為後台線程。
20  * 結論2: 自定義線程的默認狀態和環境有關,後台線程中創建的線程默認是後台線程,前台線程中創建的線程為前台線程.
21  * 結論3: 前台線程執行完后,會直接關閉後台線程,即自定義的後台線程不一定能執行完成
22  * </p>
23  */
24 public class Test07Daemon {
25     /**
26      * 測試1
27      * 查看主線程的狀態,嘗試更改
28      * 結論:活動的線程不能設置為後台線程
29      *
30      * @param args
31      */
32     public static void main1(String[] args) {
33         Thread threadMain = Thread.currentThread();
34         System.out.println("是後台線程么:" + threadMain.isDaemon());// false
35         threadMain.setDaemon(true); // 報錯,活動的線程不能設置為後台線程
36         System.out.println("修改后是後台線程么:" + threadMain.isDaemon());
37     }
38 
39     /**
40      * 測試2
41      * 查看主線程中 創建線程的狀態,嘗試更改;
42      *
43      * @param args
44      */
45     public static void main2(String[] args) {
46         Thread thread = new Thread();
47         // false
48         System.out.println("是後台線程么:" + thread.isDaemon());
49         // 修改為後台線程
50         thread.setDaemon(true);
51         System.out.println("修改后是後台線程么:" + thread.isDaemon());
52     }
53 
54     /**
55      * 測試3
56      * 查看主線程中 創建線程的狀態,嘗試更改,讓線程處於活動狀態在修改->報錯;
57      *
58      * @param args
59      */
60     public static void main3(String[] args) {
61         DaemonThread thread = new DaemonThread();
62         // 讓線程處於活躍狀態
63         thread.start();
64         // false
65         System.out.println("是後台線程么:" + thread.isDaemon());
66         // 修改為後台線程,報錯,當前已經是活躍狀態(thread.start())不能修改為後台線程
67         thread.setDaemon(true);
68         System.out.println("修改后是後台線程么:" + thread.isDaemon());
69     }
70 
71     /**
72      * 測試4
73      * 前台線程執行完后,會直接關閉後台線程,即如果後台線程不一定能執行完成
74      * 可以通過修改等待執行時間來觀察DaemonThread線程的數組輸出變化
75      *
76      * @param args
77      */
78     public static void main(String[] args) throws InterruptedException {
79         DaemonThread thread = new DaemonThread();
80         // 修改為後台線程
81         thread.setDaemon(true);
82         // 讓線程處於活躍狀態
83         thread.start();
84         System.out.println("========等待後台線程執行============");
85         Thread.sleep(5 * 1000);
86     }
87 }
 1 package com.wfd360.thread.demo04Daemon;
 2 
 3 /**
 4  * @author 姿勢帝-博客園
 5  * @address https://www.cnblogs.com/newAndHui/
 6  * @WeChat 851298348
 7  * @create 05/05 9:27
 8  * @description
 9  */
10 public class DaemonThread extends Thread {
11     @Override
12     public void run() {
13         for (int i = 0; i < 10; i++) {
14             System.out.println("===="+i);
15             try {
16                 Thread.sleep(1000);
17             } catch (InterruptedException e) {
18                 e.printStackTrace();
19             }
20         }
21     }
22 }

DaemonThread

線程基礎相關的方法定義就先到這裏,下一篇我們將進入線程同步.

https://www.cnblogs.com/newAndHui/p/12831089.html

系統化的在線學習:點擊進入學習

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

【其他文章推薦】

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

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

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

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

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

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

WebService之Spring+CXF整合示例

一、Spring+CXF整合示例

WebService是一種跨編程語言、跨操作系統平台的遠程調用技術,它是指一個應用程序向外界暴露一個能通過Web調用的API接口,我們把調用這個WebService的應用程序稱作客戶端,把提供這個WebService的應用程序稱作服務端。

環境

win10+Spring5.1+cxf3.3.2

下載

  • 官網下載:https://archive.apache.org/dist/cxf/
  • 百度網盤:
    鏈接:https://pan.baidu.com/s/1nsUweTFG_6CcZKaVBCQ7uQ
    提取碼:4qp7

服務端

  • 新建web項目
  • 放入依賴
    apache-cxf-3.3.2\lib中的jar包全部copy至項目WEB-INF\lib目錄下(偷個懶,這些jar包中包含了Spring所需的jar包)
  • web.xml中添加webService的配置攔截
<!--webService  -->
<servlet>
    <servlet-name>CXFService</servlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>CXFService</servlet-name>
    <url-pattern>/webservice/*</url-pattern>
</servlet-mapping>
  • webservice服務接口
    在項目src目錄下新建pms.inface.WebServiceInterface
package pms.inface;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;

@WebService(targetNamespace = "http://spring.webservice.server", name = "WebServiceInterface")
public interface WebServiceInterface {

	@WebMethod
    @WebResult(name = "result", targetNamespace = "http://spring.webservice.server")
	public String sayBye(@WebParam(name = "word", targetNamespace = "http://spring.webservice.server") String word);

}

  • 接口實現類
    在項目src目錄下新建pms.impl.WebServiceImpl
package pms.impl;

import javax.jws.WebService;

import pms.inface.WebServiceInterface;

@WebService
public class WebServiceImpl implements WebServiceInterface{

	@Override
	public String sayBye(String word) {
		return word + "當和這個真實的世界迎面撞上時,你是否找到辦法和自己身上的慾望講和,又該如何理解這個鋪面而來的人生?";
	}

}

  • webservice配置文件
    WEB-INF目錄下新建webservice配置文件cxf-webService.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jaxws="http://cxf.apache.org/jaxws"
	xmlns:cxf="http://cxf.apache.org/core"
	xmlns:http-conf="http://cxf.apache.org/transports/http/configuration"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://cxf.apache.org/jaxws
       http://cxf.apache.org/schemas/jaxws.xsd
       http://cxf.apache.org/core
	   http://cxf.apache.org/schemas/core.xsd
	   http://cxf.apache.org/transports/http/configuration
	   http://cxf.apache.org/schemas/configuration/http-conf.xsd
	   ">
	   
	<import resource="classpath:META-INF/cxf/cxf.xml" />

	<!-- 使用jaxws:server標籤發布WebService服務 ,設置address為訪問地址, 和web.xml文件中配置的CXF配合為一個完整的路徑 -->
	<!-- serviceClass為實現類的接口 serviceBean引用配置好的WebService實現類 -->
	<jaxws:server address="/webServiceInterface"
		serviceClass="pms.inface.WebServiceInterface">
		<jaxws:serviceBean>
			<ref bean="WebServiceImpl" />
		</jaxws:serviceBean>
	</jaxws:server>
	
	<!-- 為所有的WS設置超時時間 ,此時為默認值 連接時間30s,等待回復時間為60s-->	
	<http-conf:conduit name="*.http-conduit">
		<http-conf:client ConnectionTimeout="60000" ReceiveTimeout="120000"/>
	</http-conf:conduit>

</beans>
  • spring配置文件
    WEB-INF目錄下新建spring配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id="WebServiceImpl" class="pms.impl.WebServiceImpl"></bean>
	
	<import resource="cxf-webService.xml" />

</beans>

      在web.xml中配置applicationContext.xml

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
		    /WEB-INF/applicationContext.xml
		</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  • 將項目放至tomcat中啟動
    啟動后訪問地址:localhost:PORT/項目名/webservice/webServiceInterface?wsdl,如下圖所示,webservice接口發布成功

二、SoapUI測試

SoapUI是一個開源測試工具,通過soap/http來檢查、調用、實現Web Service的功能/負載/符合性測試。

下載

  • 百度網盤
    鏈接:https://pan.baidu.com/s/1N2RTqhvrkuzx7YJvmDeY7Q
    提取碼:e1w3

測試

  • 打開SoapUI,新建一個SOAP項目,將剛才的發布地址copyInitial WSDL欄,點擊OK按鈕
  • 發起接口請求

三、客戶端

使用wsdl2java工具生成webservice客戶端代碼

  • 該工具在剛才下載的apache-cxf-3.3.2\bin目錄下
  • 配置環境變量
    設置CXF_HOME,並添加%CXF_HOME %/binpath環境變量。
  • CMD命令行輸入wsdl2java -help,有正常提示說明環境已經正確配置
  • wsdl2java.bat用法:
wsdl2java –p 包名 –d 存放目錄 -all wsdl地址

-p 指定wsdl的命名空間,也就是要生成代碼的包名

-d 指令要生成代碼所在目錄

-client 生成客戶端測試web service的代碼

-server 生成服務器啟動web service代碼

-impl 生成web service的實現代碼,我們在方式一用的就是這個

-ant 生成build.xml文件

-all 生成所有開始端點代碼
  • 生成客戶端代碼
wsdl2java -p pms.inface -d ./ -all http://localhost:8080/spring_webservice_server/webservice/webServiceInterface?wsdl

客戶端調用

  • 新建web項目
  • 放入依賴
    apache-cxf-3.3.2\lib中的jar包全部copy至項目WEB-INF\lib目錄下
  • wsdl2java生成的代碼放至src.pms.inface目錄下
調用方法一:
  • 新建webServiceClientMain測試
package pms;

import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import pms.inface.WebServiceInterface;

public class webServiceClientMain {
	public static void main(String[] args) {
		JaxWsProxyFactoryBean svr = new JaxWsProxyFactoryBean();
		svr.setServiceClass(WebServiceInterface.class);
		svr.setAddress("http://localhost:8080/spring_webservice_server/webservice/webServiceInterface?wsdl");
		WebServiceInterface webServiceInterface = (WebServiceInterface) svr.create();

		System.out.println(webServiceInterface.sayBye("honey,"));
	}
}
  • 運行webServiceClientMain
調用方法二:
  • 在src目錄下新建applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:jaxws="http://cxf.apache.org/jaxws"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context-3.0.xsd
		http://cxf.apache.org/jaxws
		http://cxf.apache.org/schemas/jaxws.xsd">

	<jaxws:client id="webServiceInterface"
		serviceClass="pms.inface.WebServiceInterface"
		address="http://localhost:8080/spring_webservice_server/webservice/webServiceInterface?wsdl" >
	</jaxws:client>	
</beans>
  • 新建webServiceClientTest測試
package pms;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pms.inface.WebServiceInterface;

public class webServiceClientTest {

	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		WebServiceInterface webServiceInterface = context.getBean(WebServiceInterface.class);
		String result = webServiceInterface.sayBye("honey,");
		System.out.println(result);
	}
	
}
  • 運行webServiceClientTest

四、服務端攔截器

  • 需求場景:服務提供方安全驗證,也就是webservice自定義請求頭的實現,服務接口在身份認證過程中的密碼字段滿足SM3(哈希函數算法標準)的加密要求
  • SM3加密所需jar包:commons-lang3-3.9.jarbcprov-jdk15on-1.60.jar,這兩個jar包在剛才下載的apache-cxf-3.3.2\lib下就有
  • 請求頭格式
<security>
	<username></username>
	<password></password>
</auth>
  • src.pms.interceptor下新建WebServiceInInterceptor攔截器攔截請求,解析頭部
package pms.interceptor;

import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.xml.namespace.QName;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.transport.http.AbstractHTTPDestination;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import pms.support.Sm3Utils;
import pms.support.StringUtils;

/**
 * WebService的輸入攔截器
 * @author coisini
 * @date May 2020, 13
 *
 */
public class WebServiceInInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
	
    private static final String USERNAME = "admin";
    private static final String PASSWORD = "P@ssw0rd";
    
    /**
     * 允許訪問的IP
     */
    private static final String ALLOWIP = "127.0.0.1;XXX.XXX.XXX.XXX";

	public WebServiceInInterceptor() {
		/*
		 * 攔截器鏈有多個階段,每個階段都有多個攔截器,攔截器在攔截器鏈的哪個階段起作用,可以在攔截器的構造函數中聲明
		 * RECEIVE 接收階段,傳輸層處理
		 * (PRE/USER/POST)_STREAM 流處理/轉換階段
		 * READ SOAPHeader讀取 
		 * (PRE/USER/POST)_PROTOCOL 協議處理階段,例如JAX-WS的Handler處理 
		 * UNMARSHAL SOAP請求解碼階段 
		 * (PRE/USER/POST)_LOGICAL SOAP請求解碼處理階段 
		 * PRE_INVOKE 調用業務處理之前進入該階段 
		 * INVOKE 調用業務階段 
		 * POST_INVOKE 提交業務處理結果,並觸發輸入連接器
		 */
		super(Phase.PRE_INVOKE);
	}

	/**
	  * 客戶端傳來的 soap 消息先進入攔截器這裏進行處理,客戶端的賬目與密碼消息放在 soap 的消息頭<security></security>中,
	  * 類似如下:
     * <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
     * <soap:Header><security><username>admin</username><password>P@ssw0rd</password></security></soap:Header>
     * <soap:Body></soap:Body></soap:Envelope>
     * 現在只需要解析其中的 <head></head>標籤,如果解析驗證成功,則放行,否則這裏直接拋出異常,
     * 服務端不會再往後運行,客戶端也會跟着拋出異常,得不到正確結果
     *
     * @param message
     * @throws Fault
     */
	@Override
    public void handleMessage(SoapMessage message) throws Fault {
		System.out.println("PRE_INVOKE");
		
		HttpServletRequest request = (HttpServletRequest)message.get(AbstractHTTPDestination.HTTP_REQUEST);
	    String ipAddr=request.getRemoteAddr();
	    System.out.println("客戶端訪問IP----"+ipAddr);
	    
	    if(!ALLOWIP.contains(ipAddr)) {
			throw new Fault(new IllegalArgumentException("非法IP地址"), new QName("0009"));
		}
		
		/**
		 * org.apache.cxf.headers.Header
         * QName :xml 限定名稱,客戶端設置頭信息時,必須與服務器保持一致,否則這裏返回的 header 為null,則永遠通不過的
         */
		Header authHeader = null;
		//獲取驗證頭
		List<Header> headers = message.getHeaders();
		for(Header h:headers){
			if(h.getName().toString().contains("security")){
				authHeader=h;
				break;
			}
		}
		System.out.println("authHeader");
		System.out.println(authHeader);
		
		if(authHeader !=null) {
			Element auth = (Element) authHeader.getObject();
			NodeList childNodes = auth.getChildNodes();
			String username = null,password = null;
			for(int i = 0, len = childNodes.getLength(); i < len; i++){
					Node item = childNodes.item(i);
					if(item.getNodeName().contains("username")){
						username = item.getTextContent();
						System.out.println(username);
					}
					if(item.getNodeName().contains("password")){
						password = item.getTextContent();
						System.out.println(password);
					}
			}
			
			if(StringUtils.isBlank(username) || StringUtils.isBlank(password)) { 
		    	throw new Fault(new IllegalArgumentException("用戶名或密碼不能為空"), new QName("0001")); 
		    }
			
			if(!Sm3Utils.verify(USERNAME, username) || !Sm3Utils.verify(PASSWORD,password)) { 
		    	throw new Fault(new IllegalArgumentException("用戶名或密碼錯誤"), new QName("0008")); 
		    }
		  
		    if (Sm3Utils.verify(USERNAME, username) && Sm3Utils.verify(PASSWORD,password)) { 
		    	System.out.println("webService 服務端自定義攔截器驗證通過...."); 
		    	return;//放行
		    } 
		}else {
			throw new Fault(new IllegalArgumentException("請求頭security不合法"), new QName("0010"));
		}
	}

	// 出現錯誤輸出錯誤信息和棧信息
	public void handleFault(SoapMessage message) {
		Exception exeption = message.getContent(Exception.class);
		System.out.println(exeption.getMessage());
	}
	
}
  • src.pms.support下新建Sm3Utils加密類
package pms.support;

import java.io.UnsupportedEncodingException;
import java.security.Security;
import java.util.Arrays;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;

/**
 * SM3加密
 * @author coisini
 * @date May 2020, 13
 */
public class Sm3Utils {
	 private static final String ENCODING = "UTF-8";
     static {
         Security.addProvider(new BouncyCastleProvider());
     }
	    
    /**
     * sm3算法加密
     * @explain
     * @param paramStr
     * 待加密字符串
     * @return 返回加密后,固定長度=32的16進制字符串
     */
    public static String encrypt(String paramStr){
        // 將返回的hash值轉換成16進制字符串
        String resultHexString = "";
        try {
            // 將字符串轉換成byte數組
            byte[] srcData = paramStr.getBytes(ENCODING);
            // 調用hash()
            byte[] resultHash = hash(srcData);
            // 將返回的hash值轉換成16進制字符串
            resultHexString = ByteUtils.toHexString(resultHash);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return resultHexString;
    }
    
    /**
     * 返回長度=32的byte數組
     * @explain 生成對應的hash值
     * @param srcData
     * @return
     */
    public static byte[] hash(byte[] srcData) {
        SM3Digest digest = new SM3Digest();
        digest.update(srcData, 0, srcData.length);
        byte[] hash = new byte[digest.getDigestSize()];
        digest.doFinal(hash, 0);
        return hash;
    }
    
    /**
     * 通過密鑰進行加密
     * @explain 指定密鑰進行加密
     * @param key
     *            密鑰
     * @param srcData
     *            被加密的byte數組
     * @return
     */
    public static byte[] hmac(byte[] key, byte[] srcData) {
        KeyParameter keyParameter = new KeyParameter(key);
        SM3Digest digest = new SM3Digest();
        HMac mac = new HMac(digest);
        mac.init(keyParameter);
        mac.update(srcData, 0, srcData.length);
        byte[] result = new byte[mac.getMacSize()];
        mac.doFinal(result, 0);
        return result;
    }
    
    /**
     * 判斷源數據與加密數據是否一致
     * @explain 通過驗證原數組和生成的hash數組是否為同一數組,驗證2者是否為同一數據
     * @param srcStr
     *            原字符串
     * @param sm3HexString
     *            16進制字符串
     * @return 校驗結果
     */
    public static boolean verify(String srcStr, String sm3HexString) {
        boolean flag = false;
        try {
            byte[] srcData = srcStr.getBytes(ENCODING);
            byte[] sm3Hash = ByteUtils.fromHexString(sm3HexString);
            byte[] newHash = hash(srcData);
            if (Arrays.equals(newHash, sm3Hash))
                flag = true;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return flag;
    }
    
    public static void main(String[] args) {
        // 測試二:account
        String account = "admin";
        String passoword = "P@ssw0rd";
        String hex = Sm3Utils.encrypt(account);
        System.out.println(hex);//dc1fd00e3eeeb940ff46f457bf97d66ba7fcc36e0b20802383de142860e76ae6
        System.out.println(Sm3Utils.encrypt(passoword));//c2de40449a2019db9936381fa9810c22c8548a8635ed2b7fb3c7ec362e37429d
        //驗證加密后的16進制字符串與加密前的字符串是否相同
        boolean flag =  Sm3Utils.verify(account, hex);
        System.out.println(flag);// true
    }
}
  • StringUtils工具類
package pms.support;

/**
 * 字符串工具類
 * @author coisini
 * @date Nov 27, 2019
 */
public class StringUtils {

	/**
	 * 判空操作
	 * @param value
	 * @return
	 */
	public static boolean isBlank(String value) {
		return value == null || "".equals(value) || "null".equals(value) || "undefined".equals(value);
	}

}
  • cxf-webService.xml添加攔截器配置
<!-- 在此處引用攔截器 -->
<bean id="InInterceptor"
	class="pms.interceptor.WebServiceInInterceptor" >
</bean>

<cxf:bus>
	<cxf:inInterceptors>
		<ref bean="InInterceptor" />
	</cxf:inInterceptors>
</cxf:bus> 
  • SoapUI調用
  • java調用

    服務端攔截器到此結束,由上圖可以看出攔截器配置生效

五、客戶端攔截器

  • src.pms.support下新建AddHeaderInterceptor攔截器攔截請求,添加自定義認證頭部
package pms.support;

import java.util.List;
import javax.xml.namespace.QName;
import org.apache.cxf.binding.soap.SoapHeader;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class AddHeaderInterceptor extends AbstractPhaseInterceptor<SoapMessage>{ 
    
    private String userName; 
    private String password; 
       
    public AddHeaderInterceptor(String userName, String password) { 
        super(Phase.PREPARE_SEND); 
        this.userName = userName; 
        this.password = password;  
    } 
   
    @Override 
    public void handleMessage(SoapMessage msg) throws Fault { 
    	   System.out.println("攔截...");
        
           /**
            * 生成的XML文檔
            * <authHeader>
            *      <userName>admin</userName>
            *      <password>P@ssw0rd</password>
            * </authHeader>
            */ 
        
        	// SoapHeader部分待添加的節點
     		QName qName = new QName("security");
     		Document doc = DOMUtils.createDocument();

     		Element pwdEl = doc.createElement("password");
     		pwdEl.setTextContent(password);
     		Element userEl = doc.createElement("username");
     		userEl.setTextContent(userName);
     		Element root = doc.createElement("security");
     		root.appendChild(userEl);
     		root.appendChild(pwdEl);
     		// 創建SoapHeader內容
     		SoapHeader header = new SoapHeader(qName, root);
     		// 添加SoapHeader內容
     		List<Header> headers = msg.getHeaders();
     		headers.add(header); 
    } 
}
  • java調用,修改webServiceClientMain調用代碼如下
public class webServiceClientMain {
	public static void main(String[] args) {
		JaxWsProxyFactoryBean svr = new JaxWsProxyFactoryBean();
		svr.setServiceClass(WebServiceInterface.class);
		svr.setAddress("http://localhost:8081/spring_webservice_server/webservice/webServiceInterface?wsdl");
		WebServiceInterface webServiceInterface = (WebServiceInterface) svr.create();
		
		// jaxws API 轉到 cxf API 添加日誌攔截器
		org.apache.cxf.endpoint.Client client = org.apache.cxf.frontend.ClientProxy
				.getClient(webServiceInterface);
		org.apache.cxf.endpoint.Endpoint cxfEndpoint = client.getEndpoint();
		//添加自定義的攔截器
		cxfEndpoint.getOutInterceptors().add(new AddHeaderInterceptor("dc1fd00e3eeeb940ff46f457bf97d66ba7fcc36e0b20802383de142860e76ae6", "c2de40449a2019db9936381fa9810c22c8548a8635ed2b7fb3c7ec362e37429d"));
		
		System.out.println(webServiceInterface.sayBye("honey,"));
	}
}

  • SoapUI調用

六、代碼示例

服務端:https://github.com/Maggieq8324/spring_webservice_server.git
客戶端:https://github.com/Maggieq8324/spring_webservice_client.git

.end

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

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

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

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

疫情下的奇景!孟買市區湧入大量紅鶴 數量估創紀錄

摘錄自2020年5月2日自由時報綜合報導

根據《CNN》報導,每年9月至翌年5月都會觀測到紅鶴族群遷徙至孟買覓食,然而,今(2020)年在人類活動大幅下降的狀況下,遷徙至當地的紅鶴數量預估將超過13萬4000隻,創下歷史新高。

孟買自然歷史學會(BNHS)副主任科特(Rahul Khot)表示,在人類社交活動暫停後,當地不僅出現破紀錄數量的紅鶴,牠們選定的棲地也與往常相異,已有族群擴展至以往少見紅鶴蹤跡的濕地。

印度境內陸續傳出野生動物受益於武漢肺炎疫情的消息,不只德里湧入大量猴群,極瀕危的恆河江豚也在多年來首度被觀測到活體行為;顯示出人類活動暫停,讓我們的地球鄰居們產生明顯變化。

生物多樣性
生態保育
國際新聞
印度
孟買
紅鶴
江豚
動物與大環境變遷
武漢肺炎

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

山地大猩猩的家園不平靜 剛果維龍加國家公園12名護管員遭殺害

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

墨西哥穩定電力擺第一 新能源測試喊卡

摘錄自2020年5月4日經濟日報報導

在疫情蔓延下,墨西哥電力系統主管機關宣布,對乾淨能源新計畫的關鍵測試無限期喊卡,另採取措施,以提高全國電力系統的穩定性,但批評者擔心,這項措施將傷害再生能源業者。

 

能源議題
能源轉型
國際新聞
墨西哥
乾淨能源
武漢肺炎
綠電
疫情看氣候與能源
新能源

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價