Unity – Cinemachine實現相機抖動

普通相機抖動腳本較易實現,但在使用cinemachine相機下,其Transform組件不可被代碼修改,那麼Cinemachine的相機抖動如何實現呢?本文結合實際項目,對實現相機抖動的三大步驟進行系統講解:

  • 項目地址:

配置流程

項目背景:一款2D像素動作遊戲,我們操控着Player(必須帶有Collider組件),遊戲相機為Cinemachine 2DCamera(關於其配置方法此處不做贅述,推薦文章在末尾參考處)

1. 在相機上添加監聽腳本

在我們使用的虛擬相機 CM vcam1 上添加組件:AddComponent->CinemachineImpulse Listener

  • CinemachineImpulse Listener監聽腳本內震動信號(方法調用),使得抖動在此相機上發生
    • Channel Mask:通道遮罩,此處最好默認為EveryThing
    • Gain:可獲得震動信號的數目,0為屏蔽,1表示某時段僅能進行一個抖動運動
    • Use 2D Distance:用於2D遊戲,忽略相機Z軸的抖動

2. 在震動信號發生物體上添加腳本

震動信號發生物體(調用震動函數的物體)為Player,因此需要在Player上添加組件:AddComponent->Cinemachine Collision Impulse Source(注:必須掛到含Collider的物體上),然後在Raw Signal右側齒輪->New Noise Settings 添加震動配置器,默認名CM vcam1 Raw Signal

  • Cinemachine Collision Impulse Source:含有抖動函數震動配置器的關鍵腳本
    • Raw Signal:震動配置器,配置震動參數的關鍵部件,我們打開剛上面新建的CM vcam1 Raw Signal,可看到震動方式的各類參數。我們以Position Y,即上下抖動為例,添加Components后可設置其Frequency震頻Amplitude震幅,並且勾選右側方框可將其設置為正弦波
    • Attack:抖動開始的變化曲線及時間
    • Sustain Time:抖動的持續時間
    • Decay:抖動衰退的變化曲線及時間

3. 調用震動方法:

在Player內引用震動核心腳本,並在合適位置調用震動方法

private Cinemachine.CinemachineCollisionImpulseSource MyInpulse;

private void Start()
{
    MyInpulse = GetComponent<Cinemachine.CinemachineCollisionImpulseSource>();
}

private void Update()
{
    //按下右鍵產生相機抖動,抖動方式依照上面CM vcam1 Raw Signal內配置信息
    if (Input.GetMouseButtonDown(1))
        MyInpulse.GenerateImpulse();       
}

至此,我們在遊戲內操控Player,按下右鍵即可實現相機抖動。當然除了上面無參的GenerateImpulse()方法,還有兩個帶參的方法:

//假若使用傳遞velocity的方法,其震動方式為velocity和CM vcam1 Raw Signal的混合
public void GenerateImpulse(Vector3 velocity);
public void GenerateImpulse(Vector3 position, Vector3 velocity);

此外,還有可能出現bug:還尚未調用震動函數,遊戲開始時就自動產生抖動。其產生原因博主尚未在對應腳本內發現,但解決方式為關閉Player上的Cinemachine Collision Impulse Source腳本

總述

Cinemachine中實現相機抖動的基本流程:

  • 在虛擬相機上添加監聽腳本 CinemachineImpulse Listener
  • 在Player上添加震動核心腳本Cinemachine Collision Impulse Source,並添加、設置震動配置器
  • Player腳本合適位置調用震動函數

本例僅介紹了單Position方向上的抖動,讀者可按需配置抖動的Position、Rotation、發生時間、維持時間、衰退時間等,實現自己想要的效果

參考

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

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

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

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

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?

心裏有點樹

why 樹形結構

  • 順序存儲

順序存儲的特點是各個存儲單位在邏輯和物理內存上都是相鄰的,典型的就是代表就是數組,物理地址相鄰因此我們可以通過下標很快的檢索出一個元素

我們想往數組中添加一個元素最快的方式就是往它的尾部添加.如果往頭部添加元素的話,效率就很低,因為需要將從第一個元素開始依次往後移動一位,這樣就能空出第一位的元素,然後才能我們指定的數據插入到第一個的位置上

  • 鏈式存儲

鏈式存儲的特點是,各個節點之間邏輯是相鄰的,但是物理存儲上不相鄰,每一個節點都存放一個指針或者是引用用來指向它的前驅或者後繼節點, 因此我們想插入或者刪除一個元素時速度就會很塊,只需要變動一下指針的指向就行

但是對鏈表來說查找是很慢的, 因此對任意一個節點來書,他只知道自己的下一個節點或者是上一個節點在哪裡,再多的他就不不知道了,因此需要從頭結點開始遍歷…

樹型存儲結構有很多種,比如什麼二叉樹,滿二叉樹,紅黑樹,B樹等, 對於樹形結構來說,它會相對中和鏈式存儲結構和順序存儲結構的優缺點 (其中二叉排序樹最能直接的體會出樹中和鏈式存儲和線性存儲的特性,可以通過右邊的導航先去看看二叉排序樹)

樹的概述

如上圖是一個二叉樹, 當然樹還能有三叉,四叉等等…

  • 根節點: 最頂上的節點 即A
    層: 根節點在第一層 BE在第二層
    高度: 最大的層數
    森林: 多個樹的組合
    權: 節點上的值 如根節點的權是 A
    恭弘=叶 恭弘子節點: 下層上的節點是上一層的恭弘=叶 恭弘子節點
    雙親節點: 上層的節點是下層的節點的雙親節點(單個節點又是爸又是媽)
    路徑: 找到C的路徑是 A-B-C
    度: 就是直接子節點的個數

普通二叉樹

  • 什麼是二叉樹?

顧名思義就是度最大為2的樹就是二叉樹.而且對二叉樹來說,是嚴格區分左子樹和右子樹的,看上圖,雖然兩個樹的根節點都是1,但是他們的左右子樹不同,因此他們並不是相同的樹

  • 什麼是滿二叉樹?

像上圖這樣,所有的恭弘=叶 恭弘子節點都在最後一層,所有的且除了最後一層其他層的節點都有兩個子節點

二叉樹的全部節點計算公式是 2^n-1 , n是層數

  • 什麼是完全二叉樹?

像上圖這樣, 所有的恭弘=叶 恭弘子點都在最後一層或者是倒數第二層, 並且從左往右數是連續的

java&二叉樹

  • 封裝二叉樹節點
public class TreeNode {
    // 權
    private int value;
    // 左節點
    private TreeNode leftNode;
    // 右節點
    private TreeNode rightNode;
}
  • 封裝二叉樹
public class BinaryTree {
    TreeNode root;

    public void setRoot(TreeNode root) {
        this.root = root;
    }

    public TreeNode getRoot() {
        return this.getRoot();
    }
}

遍歷

像這樣一顆二叉樹,通過不同的書序遍歷會得到不同的結果

前中后的順序說的是root節點的順序,前序的話就是先遍歷父節點, 中序就是左父右 後續就是左右父

  • 前序遍歷
 public void frontShow() {
        System.out.println(this.value);
        if (leftNode != null)
            leftNode.frontShow();

        if (rightNode != null)
            rightNode.frontShow();
    }
  • 中序遍歷
    public void middleShow() {
        if (leftNode != null)
            leftNode.middleShow();

        System.out.println(value);

        if (rightNode != null)
            rightNode.middleShow();
    }
  • 後續遍歷
    public void backShow() {
        if (leftNode != null)
            leftNode.backShow();

        if (rightNode != null)
            rightNode.backShow();

        System.out.println(value);
    }

查找

其實有了上面三種遍歷的方式, 查找自然存在三種, 一遍遍歷一遍查找

    public TreeNode frontSeach(int num) {
        TreeNode node = null;
        // 當前節點不為空,返回當前節點
        if (num == this.value) {
            return this;
        } else {
            // 查找左節點
            if (leftNode != null) {
                node = leftNode.frontSeach(num);
            }
            if (node != null)
                return node;
            // 查找右節點
            if (rightNode != null)
                node = rightNode.frontSeach(num);
        }
        return node;
    }

刪除節點

刪除節點也是, 不考慮特別複雜的情況, 刪除節點就有兩種情況, 第一種要刪除的節點就是根節點, 那麼讓根節點=null就ok, 第二種情況要刪除的節點不是根節點,就處理它的左右節點, 左右節點還不是需要刪除的元素的話那麼就得遞歸循環這個過程

   // 先判斷是否是根節點,在調用如下方法
    
   public void deleteNode(int i) {
        TreeNode parent = this;
        // 處理左樹
        if (parent.leftNode!=null&&parent.leftNode.value==i){
            parent.leftNode=null;
            return;
        }
        // 處理左樹
        if (parent.rightNode!=null&&parent.rightNode.value==i){
            parent.rightNode=null;
            return;
        }
        // 遞歸-重置父節點
        parent=leftNode;
        if (parent!=null)
            parent.deleteNode(i);
        // 遞歸-重置父節點
        parent=rightNode;
        if (parent!=null)
            parent.deleteNode(i);

    }

順序存儲二叉樹

文章一開始剛說了, 順序存儲的數據結構的典型代表就是數組, 就像這樣

[1,2,3,4,5,6,7]

什麼是順序存儲的二叉樹呢? 其實就是將上面的數組看成了一顆樹,就像下圖這樣

數組轉換成二叉樹是有規律的, 這個規律就體現在他們的 下標的關聯上, 比如我們想找2節點的左子節點的下標就是 2*n -1 = 3 , 於是我們從數組中下標為3的位置取出4來

  • 第n個元素的左子節點是 2n-1
  • 第n個元素的右子節點是 2n-2
  • 第n個元素的父節點是 (n-1)/2

  • 遍歷順序存儲的二叉樹

    public void frontShow(int start){
        if (data==null||data.length==0){
            return;
        }
        // 遍歷當前節點
        System.out.println(data[start]);
        // 遍歷左樹
        if (2*start+1<data.length)
            frontShow(2*start+1);
        // 遍歷右樹
        if (2*start+2<data.length)
            frontShow(2*start+2);
    }

線索二叉樹

假設我們有下面的二叉樹, 然後我們可以使用中序遍歷它, 中序遍歷的結果是 4,2,5,1,3,6 但是很快我們就發現了兩個問題, 啥問題呢?

  • 問題1: 雖然可以正確的遍歷出 4,2,5,1,3,6 , 但是當我們遍歷到2時, 我們是不知道2的前一個是誰的,(哪怕我們剛才遍歷到了它的前一個節點就是4)

  • 問題2: node4,5,6,3的左右節點的引用存在空閑的情況

針對這個現狀做出了改進就是線索化二叉樹, 它可以充分利用各個節點中剩餘的node這個現狀…線索化后如下圖

  • 如果這個節點的右節點為空,我們就讓它讓它指向自己的後繼節點, 例如上圖的紅線
  • 如何節點的左節點為空, 就讓這個空閑的節點指向它的前驅節點,例如上圖的藍色線

這樣的話, 就實現了任意獲取出一個節點我們都能直接的得知它的前驅節點后後繼節點到底是誰

java&中序化二叉樹;

思路: 按照原來中序遍歷樹的思路,對樹進行中序遍歷,一路遞歸到4這個節點, 檢查到它的左節點為空,就將他的左節點指向它的前驅節點, 可是4本來就是最前的節點,故4這個節點的左節點自然指向了null

然後看它的右節點也為空,於是將他的右節點指向它的後繼節點, 可是這時依然沒獲取到2節點的引用怎麼辦呢? 於是先找個變量將4節點臨時存起來, 再往後遞歸,等遞歸到2節點時,取出臨時變量的4節點, 4節點.setRightNode(2節點)

然後重複這個過程

    // 臨時保存上一個節點
    private TreeNode preNode;

    // 中序線索化二叉樹
    void threadNode(TreeNode node) {
        if (node == null)
            return;

        // 處理左邊
        threadNode(node.getLeftNode());

        // 左節點為空,說明沒有左子節點, 讓這個空出的左節點指向它的上一個節點
        if (node.getLeftNode() == null) {
            // 指向上一個節點
            node.setLeftNode(preNode);
            // 標識節點的類型
            node.setLeftType(1);
        }

        // 處理前驅節點的右指針
        // 比如現在遍歷到了1, 1的上一個節點是5, 5的右邊空着了, 於是讓5的有節點指向1
        if (preNode != null && preNode.getRightNode() == null) {
            preNode.setRightNode(node);
            preNode.setRightType(1);
        }

        // 每次遞歸調用一次這個方法就更新前驅節點
        preNode = node;
        // 處理右邊
        threadNode(node.getRightNode());
    }

遍歷二叉樹

    public void threadIterator() {
        TreeNode node = root;
        while (node != null) {
            // 循環找
            while (node.getLeftType() == 0)
                node = node.getLeftNode();
            // 打印當前節點
            System.out.println(node.getValue());
            // 如果當前的節點的右type=1說明它有指針指向自己的前一個節點
            // 比如現在位置是4, 通過下面的代碼可以讓node=2
            while (node.getRightType() == 1) {
                node = node.getRightNode();
                System.out.println(node.getValue());
            }

            // 替換遍歷的節點, 可以讓 node從2指向 5, 或者從3指向1
            node = node.getRightNode();
        }

    }

赫夫曼樹(最優二叉樹)

定義: 什麼是赫夫曼樹

赫夫曼樹又稱為最優二叉樹

定義: 在N個帶權的恭弘=叶 恭弘子節點的所組成的所有二叉樹中,如果你能找出那個帶權路徑最小的二叉樹,他就是赫夫曼樹

一說起來赫夫曼樹,其實我們可以只關心它的恭弘=叶 恭弘子節點, 權, 路徑這三個要素

  • 什麼是恭弘=叶 恭弘子節點的帶權路徑?

所謂權,其實就是節點的值, 比如上圖中node4的權是8 , node5的權是6 ,node3的權是1, 而且我們只關心恭弘=叶 恭弘子節點的權

啥是帶權路徑呢? 比如上圖中 node4的帶權路徑是 1-2-4

  • 樹的帶權路徑長度(weight path length) 簡稱 WPL

其實就是這個樹所有的恭弘=叶 恭弘子節點的帶權路徑長度之和,

計算左樹的WPL =2*8+2*6+1*1 = 29

計算左樹的WPL =2*1+2*6+1*8 = 22

總結: 權值越大的節點,離根節點越近的節點是最優二叉樹

### 實戰: 將數組轉換為赫夫曼樹

  • 思路:

假設我們現在已經有了數組 [3,5,7,8,11,14,23,29], 如何將這個數組轉換成赫夫曼樹呢?

取出這裏最小的node3 和 倒數第二小的node5 ,構建成新的樹, 新樹的根節點是 node3,5的權值之和, 將構建完成的樹放回到原數組中

重複這個過程, 將最小的node7,node8取出,構建新樹, 同樣新樹的權重就是 node7,8的權重之和, 再將構建完成的樹放回到原數組中

如此往複,最終得到的樹就是huffman樹

  • java實現:

封裝TreeNode, 看上面的過程可以看到,需要比較權重的大小,因此重寫它的compareTo方法

public class TreeNode implements Comparable{
    // 權
    private int value;
    private TreeNode leftNode;
    private TreeNode rightNode;

    @Override
    public int compareTo(Object o) {
        TreeNode node = (TreeNode) o;
        return this.value-node.value;
    }

構建赫夫曼樹, 思路就是上圖的過程, 將數組中的各個元素轉換成Node. 然後存放在List容器中,每輪構建新樹時需要排序, 當集合中僅剩下一個節點,也就是根節點時完成樹的構建

    // 創建赫夫曼樹
    private static TreeNode buildHuffmanTree(int[] arr) {
        // 創建一個集合,存放將arr轉換成的二叉樹
        ArrayList<TreeNode> list = new ArrayList<>();
        for (int i : arr) {
            list.add(new TreeNode(i));
        }
        // 開始循環, 當集合中只剩下一棵樹時
        while (list.size() > 1) {
            // 排序
            Collections.sort(list);
            // 取出權值最小的數
            TreeNode leftNode = list.get(list.size() - 1);
            // 取出權值次要小的數
            TreeNode rightNode = list.get(list.size() - 2);
            // 移除取出的兩棵樹
            list.remove(leftNode);
            list.remove(rightNode);

            // 創建新的樹根節點
            TreeNode parentNode = new TreeNode(leftNode.getValue() + rightNode.getValue(), leftNode, rightNode);
            // 將新樹放到原樹的集合中
            list.add(parentNode);
        }
        return list.get(0);
    }

實戰: 赫夫曼樹與數據壓縮

通過上面的介紹我們能直觀的看出來,赫夫曼樹很顯眼的特徵就是它是各個節點能組成的樹中,那顆WPL,帶權路徑長度最短的樹, 利用這條性質常用在數據壓縮領域, 即我們將現有的數據構建成一個赫夫曼樹, 其中出現次數越多的字符,就越靠近根節點, 經過這樣的處理, 就能用最短的方式表示出原有字符

假設我們有這條消息can you can a can as a canner can a can.

數據對計算機來說不過是0-1這樣的数字, 我們看看將上面的字符轉換成01這樣的二進制數它長什麼樣子

1. 將原字符串的每一個char強轉換成 byte == ASCII
99 97 110 32 121 111 117 32 99 97 110 32 97 32 99 97 110 32 97 115 32 97 32 99 97 110 110 101 114 32 99 97 110 32 97 32 99 97 110
    
2. 將byte toBinaryString 轉換成01串如下:
1100011110000111011101000001111001110111111101011
0000011000111100001110111010000011000011000001100
0111100001110111010000011000011110011100000110000
1100000110001111000011101110100000110001111000011
1011101101110110010111100101000001100011110000111
011101000001100001100000110001111000011101110101110

也就是說,如果我們不對其進行壓縮時, 它將會轉換成上面那一大坨在網絡上進行傳輸

使用赫夫曼進行編碼:

思路: 我們將can you can a can as a canner can a can. 中的每一個符號,包括 點 空格,全部封裝進TreeNode

TreeNode中屬性如下: 包含權重: 也就是字符出現的次數, 包含data: 字符本身

public class TreeNode implements Comparable{
    // 存放權重就是字符出現的次數
    private int weight;
    // 存放英文數值
    private Byte data; //
    private TreeNode leftNode;
    private TreeNode rightNode;

封裝完成后, 按照權重的大小倒序排序,各個節點長成這樣:

a:11  :11   n:8   c:7   o:1  .:1  y:1   e:1  u:1  s:1  r:1  

將赫夫曼樹畫出來長這樣:

特徵,我們讓左側的路徑上的值是0, 右邊是1. 因此通過這個赫夫曼樹其實我們可以得到一張赫夫曼編碼錶,

比如像下面這樣:

n: 00
 : 01
a: 10
c: 111
// 每一個字符的編碼就是從根節點到它的路徑

有了這樣編碼錶, 下一步就是對數據進行編碼, 怎麼編碼呢? 不就是做一下替換嗎? 我們現在開始循環遍歷一開始的字符串, 挨個取出裏面的字符, 比如我們取出第一個字符是c, 拿着c來查詢這個表發現,c的編碼是111,於是我們將c替換成111, 遍歷到第二個字符是a, 拿着a查詢表,發現a的值是10, 於是我們將a替換成10, 重複這個過程, 最終我們得到的01串明顯比原來短很多

怎麼完成解碼呢? 解碼也不複雜, 前提也是我們得獲取到huffman編碼錶, 使用前綴匹配法, 比如我們現在接收到了

1111000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

使用前綴就是先取出1 去查查編碼錶有沒有這個數? 有的話就返回對應的字符, 沒有的話就用11再去匹配

大家可以看看上面的那顆霍夫曼樹, 所有的data都在恭弘=叶 恭弘子節點上,所以使用前綴匹配完全可以,絕對不會出現重複的情況

  • 使用java實現這個過程

思路概覽:

  1. 將原生的字節數組轉化成一個個的TreeNode
  2. 取出所有的TreeNode封裝成赫夫曼樹
  3. 通過赫夫曼樹踢去出赫夫曼編碼錶
  4. 使用這個編碼錶進行編碼
  5. 解碼

  private static byte[] huffmanZip(byte[] bytes) {
        // 先統計每個byte出現的次數,放入集合中
        List<TreeNode> treeNodes = buildNodes(bytes);
        // 創建赫夫曼樹
        TreeNode node = createHuffmanTree(treeNodes);
        // 創建huffman編碼錶
        Map<Byte, String> codes = createHuffmanCodeTable(node);
        // 編碼, 將每一個byte替換成huffman編碼錶中的V
        byte[] encodeBytes = encodeHuffmanByte(bytes, codes);
        
        // 使用huffman編碼進行解碼
        byte[] decodeBytes = decode(encodeBytes);
        return decodeBytes;
    }

將原生的byte數組,封裝成一個個的TreeNode節點,保存在一個容器中,並且記錄下這個節點出現的次數, 因此我們需要將出現次數多的節點靠近根節點

    /**
     * 將byte轉換成node集合
     *
     * @param bytes
     * @return
     */
    private static List<TreeNode> buildNodes(byte[] bytes) {
        ArrayList<TreeNode> list = new ArrayList<>();
        HashMap<Byte, Integer> countMap = new HashMap<>();
        // 統計每一個節點的出現的次數
        for (byte aByte : bytes) {
            Integer integer = countMap.get(aByte);
            if (integer == null) {
                countMap.put(aByte, 1);
            } else {
                countMap.put(aByte, integer + 1);
            }
        }
        // 將k-v轉化成node
        countMap.forEach((k, v) -> {
            list.add(new TreeNode(v, k));
        });
        return list;
    }

構建赫夫曼樹

  /**
     * 創建huffman樹
     *
     * @param treeNodes
     * @return
     */
    private static TreeNode createHuffmanTree(List<TreeNode> treeNodes) {
        // 開始循環, 當集合中只剩下一棵樹時
        while (treeNodes.size() > 1) {
            // 排序
            Collections.sort(treeNodes);
            // 取出權值最小的數
            TreeNode leftNode = treeNodes.get(treeNodes.size() - 1);
            // 取出權值次要小的數
            TreeNode rightNode = treeNodes.get(treeNodes.size() - 2);
            // 移除取出的兩棵樹
            treeNodes.remove(leftNode);
            treeNodes.remove(rightNode);

            // 創建新的樹根節點
            TreeNode parentNode = new TreeNode(leftNode.getWeight() + rightNode.getWeight(), leftNode, rightNode);
            // 將新樹放到原樹的集合中
            treeNodes.add(parentNode);
        }
        return treeNodes.get(0);
    }

從赫夫曼樹中提取出編碼錶, 思路: 下面是完了個遞歸, 我們規定好左樹是0,右邊是1, 通過一個SpringBuilder, 每次迭代都記錄下原來走過的路徑,當判斷到它的data不為空時,說明他就是恭弘=叶 恭弘子節點,立即保存這個節點曾經走過的路徑,保存在哪裡呢? 保存在一個map中, Key就是byte value就是走過的路徑

  static StringBuilder stringBuilder = new StringBuilder();
  static Map<Byte, String> huffCode = new HashMap<>();

    /**
     * 創建huffman便編碼錶
     *
     * @param node
     * @return
     */
    private static Map<Byte, String> createHuffmanCodeTable(TreeNode node) {
        if (node == null)
            return null;
        getCodes(node.getLeftNode(), "0", stringBuilder);
        getCodes(node.getRightNode(), "1", stringBuilder);
        return huffCode;
    }

    /**
     * 根據node, 獲取編碼
     *
     * @param node
     * @param code
     * @param stringBuilder
     */
    private static void getCodes(TreeNode node, String code, StringBuilder stringBuilder) {
        StringBuilder sb = new StringBuilder(stringBuilder);
        sb.append(code);
        // 如果節點的data為空,說明根本不是恭弘=叶 恭弘子節點,接着遞歸
        if (node.getData() == null) {
            getCodes(node.getLeftNode(), "0", sb);
            getCodes(node.getRightNode(), "1", sb);
        } else {
            // 如果是恭弘=叶 恭弘子節點,就記錄它的data和路徑
            huffCode.put(node.getData(), sb.toString());
        }
    }

根據赫夫曼編碼錶進行編碼:

思路:

舉個例子: 比如,原byte數組中的一個需要編碼的字節是a

a的ASCII==97

97正常轉成二進制的01串就是 0110 0001

但是現在我們有了編碼錶,就能根據97從編碼錶中取出編碼: 10

換句話說,上面 0110 0001 和 10 地位相同

若干個需要編碼的數append在一起,於是我們就有了一個比原來短一些的01串, 但是問題來了,到這裏就結束了嗎? 我們是將這些01串轉換成String, 在getBytes()返回出去嗎? 其實不是的,因為我們還需要進行解碼,你想想解碼不得編碼map中往外取值? 取值不得有key? 我們如果在這裏將這個01串的byte數組直接返回出去了,再按照什麼樣的方式將這個byte[]轉換成String串呢? ,因為我們要從這個String串中解析出key

然後這裏我們進行約定, 將現在得到的01串按照每8位為一組轉換成int數, 再將這個int強轉成byte, 解碼的時候我們就知道了.就按照8位一組進行解碼. 解析出來數組再轉換成01串,我們就重新拿到了這個編碼后的01串,它是個String串

每遇到8個0或者1,就將它強轉成Int, 再強轉成type, 經過這樣的轉換可能會出現負數,因此01串的最前面有個符號位,1表示負數

比如說: 如果你打印一下面代碼中的encodeByte,你會發現打印的第一個數是-23, 這個-23被保存在新創建的byte數組的第一個位置上, 後續解碼時,就從這個byte數組中的第一個位置上獲取出這個-23, 將它轉換成01二進制串

怎麼轉換呢? 比如不是-23, 而是-1
真值 1
原碼:1,0001
補碼: 2^(4+1) +1 = 100000 + (-1) = 1,1111
我們獲取到的結果就是1111
 /**
     * 進行編碼
     *
     * @param bytes
     * @param codes
     * @return
     */
    private static byte[] encodeHuffmanByte(byte[] bytes, Map<Byte, String> codes) {
        StringBuilder builder = new StringBuilder();
        for (byte aByte : bytes) {
            builder.append(codes.get(aByte));
        }

        // 將這些byte按照每8位一組進行編碼
        int length = 0;
        if (builder.length() % 8 == 0) {
            length = builder.length() / 8;
        } else {
            length = builder.length() / 8 + 1;
        }
        // 用於存儲壓縮后的byte
        byte[] resultByte = new byte[length];
        // 記錄新byte的位置
        int index = 0;
        // 遍歷新得到的串
        for (int i = 0; i < builder.length(); i += 8) {
            String str = null;
            if (i + 8 > builder.length()) {
                str = builder.substring(i);
            } else {
                str = builder.substring(i, i + 8);
            }
            // 將八位的二進制轉換成byte
            // 這裏出現負數了....  涉及到補碼的問題
            byte encodeByte = (byte) Integer.parseInt(str, 2);
            // 存儲起來
            resultByte[index] = encodeByte;
            index++;
        }
        return resultByte;
    }

解碼: 前面我們知道了,約定是按照8位轉換成的int 再轉換成type[] , 現在按照這個約定,反向轉換出我們一開始的01串

/**
     * 按照指定的赫夫曼編碼錶進行解碼
     *
     * @param encodeBytes
     * @return
     */
    private static byte[] decode(byte[] encodeBytes) {
        List<Byte> list = new ArrayList();
        StringBuilder builder = new StringBuilder();
        for (byte encodeByte : encodeBytes) {
            // 判斷是否是最後一個,如果是最後一次不用用0補全, 因此最後一位本來就不夠8位
            boolean flag = encodeByte == encodeBytes[encodeBytes.length - 1];
            String s = byteToBitStr(!flag, encodeByte);
            builder.append(s);
        }
        // 調換編碼錶的k-v
        Map<String, Byte> map = new HashMap<>();
        huffCode.forEach((k, v) -> {
            map.put(v, k);
        });
        // 處理字符串
        for (int i = 0; i < builder.length(); ) {
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag){
                String key = builder.substring(i,i+count);
                b=map.get(key);
                if (b==null){
                    count++;
                }else {
                    flag=false;
                }
            }
            list.add(b);
            i+=count;
        }

        // 將list轉數組
        byte[] bytes = new byte[list.size()];
        int i=0;
        for (Byte aByte : list) {
            bytes[i]=aByte;
            i++;
        }
        return bytes;
    }

    /**
     * 將byte轉換成二進制的String
     *
     * @param b
     * @return
     */
    public static String byteToBitStr(boolean flag, byte b) {
        /**
         * 目標: 全部保留八位.正數前面就補零, 負數前面補1
         * 為什麼選256呢?  因為我們前面約定好了, 按照8位進行分隔的
         * 256的二進製表示是  1 0000 0000
         * 假設我們現在是 1
         * 計算              1 0000 0000
         *               或  0 0000 0001
         *              ----------------------
         *                   1 0000 0001
         *                   結果截取8位就是 0000 0001
         *
         * 假設我們現在是   -1
         * 轉換成二進制:    1111 1111 1111 1111 1111 1111 1111 1111
         *
         * 計算                            1 0000 0000
         * 或  1111 1111 1111 1111 1111 1111 1111 1111
         *              ----------------------
         *                        1 1111 1111
         *                   結果截取8位就是 1111 1111
         *
         *
         */
        int temp = b;
        if (flag) {
            temp |= 256;
        }
        String str = Integer.toBinaryString(temp);
        if (flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }

    }

二叉排序樹

二叉排序樹, 又叫二叉搜索樹 , BST (Binary Search Tree)

  • 線性存儲和鏈式存儲的優缺點

比如我們有一個數組 [7,3,10,12,5,1,9]

雖然我們可以直接取出下標為幾的元素,但是卻不能直接取出值為幾的元素, 比如,我們如果想取出值為9的元素的話,就得先去遍歷這個數組, 然後挨個看看當前位置的數是不是9 , 就這個例子來說我們得找7次

假設我們手裡的數組已經是一個有序數組了 [1,3,5,7,9,11,12]

我們可以通過二分法快速的查找到想要的元素,但是對它依然是數組,如果想往第一個位置上插入元素還是需要把從第一個位置開始的元素,依次往後挪. 才能空出第一個位置,把新值放進去

假設我們將這一行數轉換成鏈式存儲, 確實添加, 刪除變的異常方便, 但是查找還是慢, 不管是查詢誰, 都得從第一個開始往後遍歷

  • 我們的主角: 二叉搜索樹

二叉排序樹有如下的特點:

  • 對於二叉排序樹中的任意一個非恭弘=叶 恭弘子節點都要求他的左節點小於自己, 右節點大於自己
  • 空樹也是二叉排序樹

將上面的無序的數組轉換成二叉排序樹長成下圖這樣

如果我們按照中序遍歷的話結果是: 1 3 5 7 9 11 12 , 正好是從小到大完成排序

再看他的特徵: 如果我們想查找12 , 很簡單 7-10-12 , 如果我們想插入也很簡單,它有鏈表的特性

java&二叉排序樹

封裝Node和Tree

// tree
public class BinarySortTree {
    Node root;
}

// node
public class Node {
    private int value;
    private Node leftNode;
    private Node rightNode;
}

構建一顆二叉排序樹, 思路是啥呢? 如果沒有根節點的話,直接返回,如果存在根節點, 就調用根節點的方法,將新的node添加到根節點上, 假設我們現在遍歷到的節點是NodeA. 新添加的節點是NodeB, 既然想添加就得比較一下NodeA和NodeB的值的大小, 將如果NodeB的值小於NodeA,就添加在NodeA的右邊, 反之就添加在NodeA的左邊

-----------BinarySortTree.class--------------- 
/**
     * 向二叉排序樹中添加節點
     */
    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

-------------Node.class------------
/**
     * 添加節點
     *
     * @param node
     */
    public void add(Node node) {
        if (node == null)
            return;
        //判斷需要添加的節點的值比傳遞進來的節點的值大還是小
        // 添加的節點小於當前節點的值
        if (node.value < this.value) {
            if (this.leftNode == null) {
                this.leftNode = node;
            } else {
                this.leftNode.add(node);
            }
        } else {
            if (this.rightNode == null) {
                this.rightNode = node;
            } else {
                this.rightNode.add(node);
            }
        }
    }

刪除一個節點

刪除一節點如如下幾種情況, 但是無論是哪種情況,我們都的保存當前節點的父節點, 通過他的父節點對應節點=null實現節點的刪除

情況1: 如圖

這是最好處理的情況, 就是說需要刪除的元素就是單個的子節點

情況2: 如圖

這種情況也不麻煩,我們讓當前比如我們想上刪除上圖中的3號節點, 我們首先保存下node3的父節點 node7, 刪除node3時發現node3有一個子節點,於是我們讓 node7 的 leftNode = node3

情況3: 如圖

比如我們想刪除7, 但是7這個節點還有一個子樹 按照中序遍歷這個樹的順序是 1,3,5,7,9,11,13, 想刪除7的話,其實

  1. 臨時存儲node9
  2. 刪除node9
  3. 用臨時存儲的node9替換node7

如果node9還有右節點怎麼辦呢?

  1. 臨時保存node9
  2. 刪除node9
  3. 讓node9的右節點替換node9
  4. 讓臨時存儲的node9替換node7
/**
     * 刪除一個節點
     *
     * @param value
     * @return
     */
    public void delete(int value) {
        if (root == null) {
            return;
        } else {
            // 找到這個節點
            Node node = midleSearch(value);
            if (node == null)
                return;
            // 找到他的父節點
            Node parentNode = searchParent(value);

            // todo 當前節點是恭弘=叶 恭弘子節點
            if (node.getLeftNode() == null && node.getRightNode() == null) {
                if (parentNode.getLeftNode().getValue() == value) {
                    parentNode.setLeftNode(null);
                } else {
                    parentNode.setRightNode(null);
                }
                // todo 要刪除的節點存在兩個子節點
            } else if (node.getLeftNode() != null && node.getRightNode() != null) {
                // 假設就是刪除7
                //1. 找到右子樹中最小的節點,保存它的值,然後刪除他
                int minValue = deleteMin(node.getRightNode());
                //2.替換被刪除的節點值
                node.setValue(minValue);

            } else { // todo 要刪除的節點有一個左子節點或者是右子節點
                // 左邊有節點
                if (node.getLeftNode() != null) {
                    // 要刪除的節點是父節點的左節點
                    if (parentNode.getLeftNode().getValue() == value) {
                        parentNode.setLeftNode(node.getLeftNode());
                    } else {// 要刪除的節點是父節點的右節點
                        parentNode.setRightNode(node.getLeftNode());
                    }
                } else { // 右邊有節點
                    // 要刪除的節點是父節點的右節點
                    if (parentNode.getLeftNode().getValue() == value) {
                        parentNode.setLeftNode(node.getRightNode());
                    } else {// 要刪除的節點是父節點的右節點
                        parentNode.setRightNode(node.getRightNode());
                    }
                }
            }
        }
    }

 /**
     * 刪除並保存以當前點為根節點的樹的最小值節點
     * @param node
     * @return
     */
    private int deleteMin(Node node) {
        // 情況1: 值最小的節點沒有右節點
        // 情況2: 值最小的節點存在右節點
        // 但是下面我們使用delete,原來考慮到了
        while(node.getLeftNode()!=null){
            node=node.getLeftNode();
        }
        delete(node.getValue());
        return node.getValue();
    }

    /**
     * 搜索父節點
     *
     * @param value
     * @return
     */
    public Node searchParent(int value) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(value);
        }
    }

缺點

二叉排序樹其實對節點權是有要求的, 比如我們的數組就是[1,2,3,4] 那麼畫成平衡二叉樹的話長下面這樣

它不僅沒有二叉排序樹的優點,而且還不如單鏈表的速度快

AVL樹(平衡二叉樹)

定義: 什麼是平衡二叉樹

平衡二叉樹的出現就是為了 解決上面二叉排序樹[1,2,3,4,5,6]這樣成單條鏈的略勢的情況,它要求,每個樹的左子樹和右子樹的高度之差不超過1, 如果不滿足這種情況了,馬上馬對各個節點進行調整,這樣做保證了二叉排序樹的優勢

如何調整

  • 情況1: 對於node1來說, 它的左邊深度0 , 右邊的深度2 , 於是我們將它調整成右邊的樣子
  • 情況2: 在1234的情況下, 添加node5,導致node2不平衡, 進行如下的調整
  • 情況3: 在12345的基礎上添加node6,導致node4不平衡, 對node4進行調整, 其實就和情況1相同了
  • 情況4: 在1234567的情況下,進行添加8. 打破了node5的平衡, 因此進行旋轉

一個通用的旋轉規律

看這個典型的有旋轉的例子

node4的出現,使用node8的平衡被打破, 因此我們需要進行調整, 按照下面的步驟進行調整

下面說的this是根節點node8, 按照下面的步驟在紙上畫一畫就ok

  1. 創建新node, 使新node.value = this.value
  2. 新節點的rightNode = this.rightNode
  3. 新節點的leftNode = this.leftNode.rightNode
  4. this.value = this.LeftNode.value
  5. this.leftNode = this.leftNode .leftNode
  6. this.leftNode = 新創建的node

需要注意的情況:

新添加6使得node8不再平衡,但是如果你按照上面的步驟進行旋轉的話,會得到右邊的結果, 但是右邊的結果中對於node4還是不平衡的,因此需要預處理一下

再進行右旋轉時,提前進行檢驗一下,當前節點的左子樹是否存在右邊比左邊高的情況, 如果右邊比較高的話,就先將這個子樹往左旋轉, 再以node8為根,整體往右旋轉

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

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

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

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

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

小三通物流營運型態?

※快速運回,大陸空運推薦?

Prometheus監控有所思:多標籤埋點及Mbean

  使用 grafana+prometheus+jmx 作為普通的監控手段,是比較有用的。我之前的文章介紹了相應的實現辦法。

  但是,按照之前的實現,我們更多的只能是監控 單值型的數據,如請求量,tps 等等,對於複雜組合型的指標卻不容易監控。

  這種情況一般帶有一定的業務屬性,比如想監控mq中的每個topic的消費情況,每類產品的實時訂單情況等等。當然,對於看過完整的 prometheus 的監控數據的同學來說,會覺得很正常,因為你會看到如下的數據:

# HELP java_lang_MemoryPool_PeakUsage_max java.lang.management.MemoryUsage (java.lang<type=MemoryPool, name=Metaspace><PeakUsage>max)
# TYPE java_lang_MemoryPool_PeakUsage_max untyped
java_lang_MemoryPool_PeakUsage_max{name="Metaspace",} -1.0
java_lang_MemoryPool_PeakUsage_max{name="PS Old Gen",} 1.415053312E9
java_lang_MemoryPool_PeakUsage_max{name="PS Eden Space",} 6.96778752E8
java_lang_MemoryPool_PeakUsage_max{name="Code Cache",} 2.5165824E8
java_lang_MemoryPool_PeakUsage_max{name="Compressed Class Space",} 1.073741824E9
java_lang_MemoryPool_PeakUsage_max{name="PS Survivor Space",} 5242880.0

  這裏面的 name 就是普通標籤嘛,同理於其他埋點咯。應該是可以實現的。

  是的,prometheus 是方便實現這玩意的,但是我們之前不是使用 jmx_exportor 作為導出工具嘛,使用的埋點組件是 io.dropwizard.metrics:metrics-core 。

  而它則是重在單值的監控,所以,用它我們是實現不了帶指標的數據的監控了。

  那怎麼辦呢?三個辦法!

1. 直接替換原有的 metrics-core 組件為 prometheus 的client 組件,因為官方是支持這種操作的;
2. 使用 prometheus-client 組件與 metrics-core 組件配合,各自使用各自的功能;
3. 自行實現帶標籤的埋點,這可能是基於 MBean 的;

 

  以上這幾種方案,各有優劣。方案1可能改動太大,而且可能功能不兼容不可行; 方案2可能存在整合不了或者功能衝突情況,當然如果能整合,絕對是最好的; 方案3實現複雜度就高了,比如監控值維護、線程安全、MBean數據吐出方式等等。

  好吧,不管怎麼樣,我們還是都看看吧。

 

一、 使用 prometheus-client 埋點實現帶標籤的監控

  1. 引入 pom 依賴

        <dependency>
            <groupId>io.prometheus</groupId>
            <artifactId>simpleclient</artifactId>
            <version>0.8.0</version>
        </dependency>
        <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient_hotspot</artifactId>
                <version>0.8.0</version>
        </dependency>
        <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient_servlet</artifactId>
                <version>0.8.0</version>
        </dependency>

  2. 框架註冊監控

        @Configuration
        public class PrometheusConfig {
            @Bean
            public ServletRegistrationBean servletRegistrationBean(){
                // 將埋點指標吐出到 /metrics 節點
                return new ServletRegistrationBean(new MetricsServlet(), "/metrics");
            }
        }

  3. 業務埋點數據

        // 註冊指標實例
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  4. 獲取埋點數據信息

        curl http://localhost:8080/metrics
        # 對外暴露http接口調用,結果如下
        # HELP jmx_test_abc_ffff counter usage.
        # TYPE jmx_test_abc_ffff counter
        jmx_test_abc_ffff{topic="bbb",} 1.0
        jmx_test_abc_ffff{topic="2",} 2.0
        jmx_test_abc_ffff{topic="test",} 1.0

  可以看出,效果咱們是實現了。但是,對於已經運行的東西,要改這玩意可能不是那麼友好。主要有以下幾點:

    1. 暴露數據方式變更,原來由javaagent進行統一處理的數據,現在可能由於應用端口的不一,導致收集的配置會變更,不一定符合運維場景;
    2. 需要將原來的埋點進行替換;

 

二、 prometheus-client 與 metrics-core 混合埋點

  不處理以前的監控,將新監控帶標籤數據吐入到 jmx_exportor 中。

  我們試着使用如上的埋點方式:

        // 註冊指標實例
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  好像數據是不會進入的到 jmx_exportor 的。這也不奇怪,畢竟咱們也不了解其原理,難道想靠運氣取勝??

  細去查看 metrics-core 組件的埋點實現方案,發現其是向 MBean 中吐入數據,從而被 jmx_exportor 抓取的。

        // com.codahale.metrics.jmx.JmxReporter.JmxListener#onCounterAdded
        @Override
        public void onCounterAdded(String name, Counter counter) {
            try {
                if (filter.matches(name, counter)) {
                    final ObjectName objectName = createName("counters", name);
                    registerMBean(new JmxCounter(counter, objectName), objectName);
                }
            } catch (InstanceAlreadyExistsException e) {
                LOGGER.debug("Unable to register counter", e);
            } catch (JMException e) {
                LOGGER.warn("Unable to register counter", e);
            }
        }
        // 向 mBeanServer 註冊監控實例
        // 默認情況下 mBeanServer = ManagementFactory.getPlatformMBeanServer();
        private void registerMBean(Object mBean, ObjectName objectName) throws InstanceAlreadyExistsException, JMException {
            ObjectInstance objectInstance = mBeanServer.registerMBean(mBean, objectName);
            if (objectInstance != null) {
                // the websphere mbeanserver rewrites the objectname to include
                // cell, node & server info
                // make sure we capture the new objectName for unregistration
                registered.put(objectName, objectInstance.getObjectName());
            } else {
                registered.put(objectName, objectName);
            }
        }

  而 prometheus-client 則是通過 CollectorRegistry.defaultRegistry 進行註冊實例的。

    // io.prometheus.client.SimpleCollector.Builder#register()
    /**
     * Create and register the Collector with the default registry.
     */
    public C register() {
      return register(CollectorRegistry.defaultRegistry);
    }
    /**
     * Create and register the Collector with the given registry.
     */
    public C register(CollectorRegistry registry) {
      C sc = create();
      registry.register(sc);
      return sc;
    }

  所以,好像原理上來講是不同的。至於到底為什麼不能監控到數據,那還不好說。至少,你可以學習 metrics-core 使用 MBean 的形式將數據導出。這是我們下一個方案要討論的事。

  這裏我可以給到一個最終簡單又不失巧合的方式,實現兩個監控組件的兼容,同時向 jmx_exportor 進行導出。如下:

  1. 引入 javaagent 依賴包

        <!-- javaagent 包,與 外部使用的 jmx_exportor 一致 -->
        <dependency>
            <groupId>io.prometheus.jmx</groupId>
            <artifactId>jmx_prometheus_javaagent</artifactId>
            <version>0.12.0</version>
        </dependency>

  2. 使用 agent 的工具類進行埋點

  因為 javaagent 裏面提供一套完整的 client 工具包,所以,我們可以使用。

        // 註冊指標實例
        // 將 io.prometheus.client.Counter 包替換為 io.prometheus.jmx.shaded.io.prometheus.client.Counter
        io.prometheus.client.Counter c = io.prometheus.client.Counter.build()
                .name("jmx_test_abc_ffff")
                .labelNames("topic")
                .help("topic counter usage.")
                .register();
        public void incTopicMetric(String topic) {
            // c.labels("test").inc();  // for test
        }

  3. 原樣使用 jmx_exportor 就可以導出監控數據了

  為什麼換一個包這樣就可以了?

  因為 jmx_exportor 也是通過註冊 CollectorRegistry.defaultRegistry 來進行收集數據的,我們只要保持與其實例一致,就可以做到在同一個jvm內共享數據了。

 

三、 基於 MBean自行實現帶標籤的埋點

// 測試類
public class PrometheusMbeanMetricsMain {
    private static ConcurrentHashMap<String, AtomicInteger> topicContainer = new ConcurrentHashMap<>();
    private static MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

    public static void main(String[] args) throws Exception {
        // 模擬某個topic
        String commingTopic = "test_topic";
        AtomicInteger myTopic1Counter = getMetricCounter(commingTopic);
        System.out.println("jmx started!");
        while(true){
            System.out.println("---");
            // 計數增加
            myTopic1Counter.incrementAndGet();
            Thread.sleep(10000);
        }
    }

    private static AtomicInteger getMetricCounter(String topic) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        AtomicInteger myTopic1Counter = topicContainer.get(topic);
        if(myTopic1Counter == null) {
            myTopic1Counter = new AtomicInteger(0);
            Hashtable<String, String> tab = new Hashtable<>();
            tab.put("topic", topic);
            // 佔位符,雖然不知道什麼意思,但是感覺很厲害的樣子
            tab.put("_", "_value");
            ObjectName objectName = new ObjectName("mydomain_test", tab);
            // 註冊監控實例 到 MBeanServer 中
            ObjectInstance objectInstance = mBeanServer.registerMBean(new JmxCounter(myTopic1Counter, objectName), objectName);
        }
        return myTopic1Counter;
    }
}
// JmxCounter, MBean 要求: 1. 接口必須定義成Public的;  2. 接口命名規範符合要求, 即接口名叫 XYZMBean ,那麼實現名就必須一定是XYZ;
// DynamicMBean
public interface JmxCounterMBean {
    public Object getCount() throws Exception;
}
public class JmxCounter implements JmxCounterMBean {
    private AtomicInteger metric;
    private ObjectName objectName;

    public JmxCounter(AtomicInteger metric, ObjectName objectName) {
        this.objectName = objectName;
        this.metric = metric;
    }

    @Override
    public Object getCount() throws Exception {
        // 返回監控結果
        return metric.get();
    }

}

  最後,見證奇迹的時刻。結果如下:

# HELP mydomain_test_value_Count Attribute exposed for management (mydomain_test<_=_value, topic=b_topic><>Count)
# TYPE mydomain_test_value_Count untyped
mydomain_test_value_Count{topic="b_topic",} 1.0
mydomain_test_value_Count{topic="a_topic",} 88.0

  很明顯,這是一個糟糕的實現,不要學他。僅為了演示效果。

  所以,總結下來,自然是使用方案2了。兩個組件兼容,實現簡單,性能也不錯。如果只是為了使用,到此就可以了。不過你得明白,以上方案有取巧的成分在。

 

四、 原理: jmx_exportor 是如何獲取數據的?

  jmx_exportor 也是可以通過 http_server 暴露數據。

    // io.prometheus.client.exporter.HTTPServer
    /**
     * Start a HTTP server serving Prometheus metrics from the given registry.
     */
    public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
        server = HttpServer.create();
        server.bind(addr, 3);
        // 使用 HTTPMetricHandler 處理請求
        HttpHandler mHandler = new HTTPMetricHandler(registry);
        // 綁定到 /metrics 地址上
        server.createContext("/", mHandler);
        server.createContext("/metrics", mHandler);
        executorService = Executors.newFixedThreadPool(5, DaemonThreadFactory.defaultThreadFactory(daemon));
        server.setExecutor(executorService);
        start(daemon);
    }    
    /**
     * Start a HTTP server by making sure that its background thread inherit proper daemon flag.
     */
    private void start(boolean daemon) {
        if (daemon == Thread.currentThread().isDaemon()) {
            server.start();
        } else {
            FutureTask<Void> startTask = new FutureTask<Void>(new Runnable() {
                @Override
                public void run() {
                    server.start();
                }
            }, null);
            DaemonThreadFactory.defaultThreadFactory(daemon).newThread(startTask).start();
            try {
                startTask.get();
            } catch (ExecutionException e) {
                throw new RuntimeException("Unexpected exception on starting HTTPSever", e);
            } catch (InterruptedException e) {
                // This is possible only if the current tread has been interrupted,
                // but in real use cases this should not happen.
                // In any case, there is nothing to do, except to propagate interrupted flag.
                Thread.currentThread().interrupt();
            }
        }
    }

  所以,可以主要邏輯是 HTTPMetricHandler 處理。來看看。

        // io.prometheus.client.exporter.HTTPServer.HTTPMetricHandler#handle
        public void handle(HttpExchange t) throws IOException {
            String query = t.getRequestURI().getRawQuery();

            ByteArrayOutputStream response = this.response.get();
            response.reset();
            OutputStreamWriter osw = new OutputStreamWriter(response);
            // 主要由該 TextFormat 進行格式化輸出
            // registry.filteredMetricFamilySamples() 進行數據收集
            TextFormat.write004(osw,
                    registry.filteredMetricFamilySamples(parseQuery(query)));
            osw.flush();
            osw.close();
            response.flush();
            response.close();

            t.getResponseHeaders().set("Content-Type",
                    TextFormat.CONTENT_TYPE_004);
            if (shouldUseCompression(t)) {
                t.getResponseHeaders().set("Content-Encoding", "gzip");
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
                final GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody());
                response.writeTo(os);
                os.close();
            } else {
                t.getResponseHeaders().set("Content-Length",
                        String.valueOf(response.size()));
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.size());
                // 寫向客戶端
                response.writeTo(t.getResponseBody());
            }
            t.close();
        }

    }

 

五、 原理: jmx_exportor 是如何獲取Mbean 的數據的?

  jmx_exportor 有一個 JmxScraper, 專門用於處理 MBean 的值。

    // io.prometheus.jmx.JmxScraper#doScrape
    /**
      * Get a list of mbeans on host_port and scrape their values.
      *
      * Values are passed to the receiver in a single thread.
      */
    public void doScrape() throws Exception {
        MBeanServerConnection beanConn;
        JMXConnector jmxc = null;
        // 默認直接獲取本地的 jmx 信息
        // 即是通過共享 ManagementFactory.getPlatformMBeanServer() 變量來實現通信的
        if (jmxUrl.isEmpty()) {
          beanConn = ManagementFactory.getPlatformMBeanServer();
        } else {
          Map<String, Object> environment = new HashMap<String, Object>();
          if (username != null && username.length() != 0 && password != null && password.length() != 0) {
            String[] credent = new String[] {username, password};
            environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credent);
          }
          if (ssl) {
              environment.put(Context.SECURITY_PROTOCOL, "ssl");
              SslRMIClientSocketFactory clientSocketFactory = new SslRMIClientSocketFactory();
              environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
              environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
          }
          // 如果是遠程獲取,則會通過 rmi 進行遠程通信獲取
          jmxc = JMXConnectorFactory.connect(new JMXServiceURL(jmxUrl), environment);
          beanConn = jmxc.getMBeanServerConnection();
        }
        try {
            // Query MBean names, see #89 for reasons queryMBeans() is used instead of queryNames()
            Set<ObjectName> mBeanNames = new HashSet<ObjectName>();
            for (ObjectName name : whitelistObjectNames) {
                for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
                    mBeanNames.add(instance.getObjectName());
                }
            }

            for (ObjectName name : blacklistObjectNames) {
                for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
                    mBeanNames.remove(instance.getObjectName());
                }
            }

            // Now that we have *only* the whitelisted mBeans, remove any old ones from the cache:
            jmxMBeanPropertyCache.onlyKeepMBeans(mBeanNames);

            for (ObjectName objectName : mBeanNames) {
                long start = System.nanoTime();
                scrapeBean(beanConn, objectName);
                logger.fine("TIME: " + (System.nanoTime() - start) + " ns for " + objectName.toString());
            }
        } finally {
          if (jmxc != null) {
            jmxc.close();
          }
        }
    }
    
    // io.prometheus.jmx.JmxScraper#scrapeBean
    private void scrapeBean(MBeanServerConnection beanConn, ObjectName mbeanName) {
        MBeanInfo info;
        try {
          info = beanConn.getMBeanInfo(mbeanName);
        } catch (IOException e) {
          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
          return;
        } catch (JMException e) {
          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
          return;
        }
        MBeanAttributeInfo[] attrInfos = info.getAttributes();

        Map<String, MBeanAttributeInfo> name2AttrInfo = new LinkedHashMap<String, MBeanAttributeInfo>();
        for (int idx = 0; idx < attrInfos.length; ++idx) {
            MBeanAttributeInfo attr = attrInfos[idx];
            if (!attr.isReadable()) {
                logScrape(mbeanName, attr, "not readable");
                continue;
            }
            name2AttrInfo.put(attr.getName(), attr);
        }
        final AttributeList attributes;
        try {
            // 通過 MBean 調用對象,獲取所有屬性值,略去不說
            attributes = beanConn.getAttributes(mbeanName, name2AttrInfo.keySet().toArray(new String[0]));
        } catch (Exception e) {
            logScrape(mbeanName, name2AttrInfo.keySet(), "Fail: " + e);
            return;
        }
        for (Attribute attribute : attributes.asList()) {
            MBeanAttributeInfo attr = name2AttrInfo.get(attribute.getName());
            logScrape(mbeanName, attr, "process");
            // 處理單個key的屬性值, 如 topic=aaa,ip=1 將會進行再次循環處理
            processBeanValue(
                    mbeanName.getDomain(),
                    // 獲取有效的屬性列表, 我們可以簡單看一下過濾規則, 如下文
                    jmxMBeanPropertyCache.getKeyPropertyList(mbeanName),
                    new LinkedList<String>(),
                    attr.getName(),
                    attr.getType(),
                    attr.getDescription(),
                    attribute.getValue()
            );
        }
    }
    // 處理每個 mBean 的屬性,寫入到 receiver 中
    // io.prometheus.jmx.JmxScraper#processBeanValue
    /**
     * Recursive function for exporting the values of an mBean.
     * JMX is a very open technology, without any prescribed way of declaring mBeans
     * so this function tries to do a best-effort pass of getting the values/names
     * out in a way it can be processed elsewhere easily.
     */
    private void processBeanValue(
            String domain,
            LinkedHashMap<String, String> beanProperties,
            LinkedList<String> attrKeys,
            String attrName,
            String attrType,
            String attrDescription,
            Object value) {
        if (value == null) {
            logScrape(domain + beanProperties + attrName, "null");
        } 
        // 單值情況,数字型,字符串型,可以處理
        else if (value instanceof Number || value instanceof String || value instanceof Boolean) {
            logScrape(domain + beanProperties + attrName, value.toString());
            // 解析出的數據存入 receiver 中,可以是 jmx, 或者 控制台
            this.receiver.recordBean(
                    domain,
                    beanProperties,
                    attrKeys,
                    attrName,
                    attrType,
                    attrDescription,
                    value);
        } 
        // 多值型情況
        else if (value instanceof CompositeData) {
            logScrape(domain + beanProperties + attrName, "compositedata");
            CompositeData composite = (CompositeData) value;
            CompositeType type = composite.getCompositeType();
            attrKeys = new LinkedList<String>(attrKeys);
            attrKeys.add(attrName);
            for(String key : type.keySet()) {
                String typ = type.getType(key).getTypeName();
                Object valu = composite.get(key);
                processBeanValue(
                        domain,
                        beanProperties,
                        attrKeys,
                        key,
                        typ,
                        type.getDescription(),
                        valu);
            }
        } 
        // 更複雜型對象
        else if (value instanceof TabularData) {
            // I don't pretend to have a good understanding of TabularData.
            // The real world usage doesn't appear to match how they were
            // meant to be used according to the docs. I've only seen them
            // used as 'key' 'value' pairs even when 'value' is itself a
            // CompositeData of multiple values.
            logScrape(domain + beanProperties + attrName, "tabulardata");
            TabularData tds = (TabularData) value;
            TabularType tt = tds.getTabularType();

            List<String> rowKeys = tt.getIndexNames();

            CompositeType type = tt.getRowType();
            Set<String> valueKeys = new TreeSet<String>(type.keySet());
            valueKeys.removeAll(rowKeys);

            LinkedList<String> extendedAttrKeys = new LinkedList<String>(attrKeys);
            extendedAttrKeys.add(attrName);
            for (Object valu : tds.values()) {
                if (valu instanceof CompositeData) {
                    CompositeData composite = (CompositeData) valu;
                    LinkedHashMap<String, String> l2s = new LinkedHashMap<String, String>(beanProperties);
                    for (String idx : rowKeys) {
                        Object obj = composite.get(idx);
                        if (obj != null) {
                            // Nested tabulardata will repeat the 'key' label, so
                            // append a suffix to distinguish each.
                            while (l2s.containsKey(idx)) {
                              idx = idx + "_";
                            }
                            l2s.put(idx, obj.toString());
                        }
                    }
                    for(String valueIdx : valueKeys) {
                        LinkedList<String> attrNames = extendedAttrKeys;
                        String typ = type.getType(valueIdx).getTypeName();
                        String name = valueIdx;
                        if (valueIdx.toLowerCase().equals("value")) {
                            // Skip appending 'value' to the name
                            attrNames = attrKeys;
                            name = attrName;
                        } 
                        processBeanValue(
                            domain,
                            l2s,
                            attrNames,
                            name,
                            typ,
                            type.getDescription(),
                            composite.get(valueIdx));
                    }
                } else {
                    logScrape(domain, "not a correct tabulardata format");
                }
            }
        } else if (value.getClass().isArray()) {
            logScrape(domain, "arrays are unsupported");
        } else {
            // 多半會返回不支持的對象然後得不到jmx監控值
            // mydomain_test{3=3, topic=aaa} java.util.Hashtable is not exported
            logScrape(domain + beanProperties, attrType + " is not exported");
        }
    }
    
    // 我們看下prometheus 對 mbeanName 的轉換操作,會將各種特殊字符轉換為 屬性列表
    // io.prometheus.jmx.JmxMBeanPropertyCache#getKeyPropertyList
    public LinkedHashMap<String, String> getKeyPropertyList(ObjectName mbeanName) {
        LinkedHashMap<String, String> keyProperties = keyPropertiesPerBean.get(mbeanName);
        if (keyProperties == null) {
            keyProperties = new LinkedHashMap<String, String>();
            // 轉化為 string 格式
            String properties = mbeanName.getKeyPropertyListString();
            // 此處為 prometheus 認識的格式,已經匹配上了
            Matcher match = PROPERTY_PATTERN.matcher(properties);
            while (match.lookingAt()) {
                keyProperties.put(match.group(1), match.group(2));
                properties = properties.substring(match.end());
                if (properties.startsWith(",")) {
                    properties = properties.substring(1);
                }
                match.reset(properties);
            }
            keyPropertiesPerBean.put(mbeanName, keyProperties);
        }
        return keyProperties;
    }
        // io.prometheus.jmx.JmxMBeanPropertyCache#PROPERTY_PATTERN
        private static final Pattern PROPERTY_PATTERN = Pattern.compile(
            "([^,=:\\*\\?]+)" + // Name - non-empty, anything but comma, equals, colon, star, or question mark
                    "=" +  // Equals
                    "(" + // Either
                    "\"" + // Quoted
                    "(?:" + // A possibly empty sequence of
                    "[^\\\\\"]*" + // Greedily match anything but backslash or quote
                    "(?:\\\\.)?" + // Greedily see if we can match an escaped sequence
                    ")*" +
                    "\"" +
                    "|" + // Or
                    "[^,=:\"]*" + // Unquoted - can be empty, anything but comma, equals, colon, or quote
                    ")");

 

六、 原理: jmx_exportor 為什麼輸出的格式是這樣的?

  prometheus 的數據格式如下,如何從埋點數據轉換?

# HELP mydomain_test_value_Count Attribute exposed for management (mydomain_test<_=_value, topic=b_topic><>Count)
# TYPE mydomain_test_value_Count untyped
mydomain_test_value_Count{topic="b_topic",} 1.0
mydomain_test_value_Count{topic="a_topic",} 132.0

  是一個輸出格式問題,也是一協議問題。

  // io.prometheus.client.exporter.common.TextFormat#write004
  public static void write004(Writer writer, Enumeration<Collector.MetricFamilySamples> mfs) throws IOException {
    /* See http://prometheus.io/docs/instrumenting/exposition_formats/
     * for the output format specification. */
    while(mfs.hasMoreElements()) {
      Collector.MetricFamilySamples metricFamilySamples = mfs.nextElement();
      writer.write("# HELP ");
      writer.write(metricFamilySamples.name);
      writer.write(' ');
      writeEscapedHelp(writer, metricFamilySamples.help);
      writer.write('\n');

      writer.write("# TYPE ");
      writer.write(metricFamilySamples.name);
      writer.write(' ');
      writer.write(typeString(metricFamilySamples.type));
      writer.write('\n');

      for (Collector.MetricFamilySamples.Sample sample: metricFamilySamples.samples) {
        writer.write(sample.name);
        // 帶 labelNames 的,依次輸出對應的標籤
        if (sample.labelNames.size() > 0) {
          writer.write('{');
          for (int i = 0; i < sample.labelNames.size(); ++i) {
            writer.write(sample.labelNames.get(i));
            writer.write("=\"");
            writeEscapedLabelValue(writer, sample.labelValues.get(i));
            writer.write("\",");
          }
          writer.write('}');
        }
        writer.write(' ');
        writer.write(Collector.doubleToGoString(sample.value));
        if (sample.timestampMs != null){
          writer.write(' ');
          writer.write(sample.timestampMs.toString());
        }
        writer.write('\n');
      }
    }
  }

 

  done.

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

花了幾個小時總結了一些容易出錯的 Java 知識點!

本文已經收錄自 (61k+Star!【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。歡迎 Star!歡迎騷擾!)
原文地址:https://javaguide.cn/2019/08/20/java/java%E5%9F%BA%E7%A1%80/Java%E7%96%91%E9%9A%BE%E7%82%B9/

1. 基礎

1.1. 正確使用 equals 方法

Object的equals方法容易拋空指針異常,應使用常量或確定有值的對象來調用 equals。

舉個例子:

// 不能使用一個值為null的引用類型變量來調用非靜態方法,否則會拋出異常
String str = null;
if (str.equals("SnailClimb")) {
  ...
} else {
  ..
}

運行上面的程序會拋出空指針異常,但是我們把第二行的條件判斷語句改為下面這樣的話,就不會拋出空指針異常,else 語句塊得到執行。:

"SnailClimb".equals(str);// false 

不過更推薦使用 java.util.Objects#equals(JDK7 引入的工具類)。

Objects.equals(null,"SnailClimb");// false

我們看一下java.util.Objects#equals的源碼就知道原因了。

public static boolean equals(Object a, Object b) {
        // 可以避免空指針異常。如果a==null的話此時a.equals(b)就不會得到執行,避免出現空指針異常。
        return (a == b) || (a != null && a.equals(b));
    }

注意:

Reference:

  • 每種原始類型都有默認值一樣,如int默認值為 0,boolean 的默認值為 false,null 是任何引用類型的默認值,不嚴格的說是所有 Object 類型的默認值。
  • 可以使用 == 或者 != 操作來比較null值,但是不能使用其他算法或者邏輯操作。在Java中null == null將返回true。
  • 不能使用一個值為null的引用類型變量來調用非靜態方法,否則會拋出異常

1.2. 整型包裝類值的比較

所有整型包裝類對象值的比較必須使用equals方法。

先看下面這個例子:

Integer x = 3;
Integer y = 3;
System.out.println(x == y);// true
Integer a = new Integer(3);
Integer b = new Integer(3);
System.out.println(a == b);//false
System.out.println(a.equals(b));//true

當使用自動裝箱方式創建一個Integer對象時,當數值在-128 ~127時,會將創建的 Integer 對象緩存起來,當下次再出現該數值時,直接從緩存中取出對應的Integer對象。所以上述代碼中,x和y引用的是相同的Integer對象。

注意:如果你的IDE(IDEA/Eclipse)上安裝了阿里巴巴的p3c插件,這個插件如果檢測到你用 ==的話會報錯提示,推薦安裝一個這個插件,很不錯。

1.3. BigDecimal

1.3.1. BigDecimal 的用處

《阿里巴巴Java開發手冊》中提到:浮點數之間的等值判斷,基本數據類型不能用==來比較,包裝數據類型不能用 equals 來判斷。 具體原理和浮點數的編碼方式有關,這裏就不多提了,我們下面直接上實例:

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false

具有基本數學知識的我們很清楚的知道輸出並不是我們想要的結果(精度丟失),我們如何解決這個問題呢?一種很常用的方法是:使用使用 BigDecimal 來定義浮點數的值,再進行浮點數的運算操作。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true 

1.3.2. BigDecimal 的大小比較

a.compareTo(b) : 返回 -1 表示小於,0 表示 等於, 1表示 大於。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));// 1

1.3.3. BigDecimal 保留幾位小數

通過 setScale方法設置保留幾位小數以及保留規則。保留規則有挺多種,不需要記,IDEA會提示。

BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);// 1.255

1.3.4. BigDecimal 的使用注意事項

注意:我們在使用BigDecimal時,為了防止精度丟失,推薦使用它的 BigDecimal(String) 構造方法來創建對象。《阿里巴巴Java開發手冊》對這部分內容也有提到如下圖所示。

1.3.5. 總結

BigDecimal 主要用來操作(大)浮點數,BigInteger 主要用來操作大整數(超過 long 類型)。

BigDecimal 的實現利用到了 BigInteger, 所不同的是 BigDecimal 加入了小數位的概念

1.4. 基本數據類型與包裝數據類型的使用標準

Reference:《阿里巴巴Java開發手冊》

  • 【強制】所有的 POJO 類屬性必須使用包裝數據類型。
  • 【強制】RPC 方法的返回值和參數必須使用包裝數據類型。
  • 【推薦】所有的局部變量使用基本數據類型。

比如我們如果自定義了一個Student類,其中有一個屬性是成績score,如果用Integer而不用int定義,一次考試,學生可能沒考,值是null,也可能考了,但考了0分,值是0,這兩個表達的狀態明顯不一樣.

說明 :POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何 NPE 問題,或者入庫檢查,都由使用者來保證。

正例 : 數據庫的查詢結果可能是 null,因為自動拆箱,用基本數據類型接收有 NPE 風險。

反例 : 比如显示成交總額漲跌情況,即正負 x%,x 為基本數據類型,調用的 RPC 服務,調用不成功時,返回的是默認值,頁面显示為 0%,這是不合理的,應該显示成中劃線。所以包裝數據類型的 null 值,能夠表示額外的信息,如:遠程調用失敗,異常退出。

2. 集合

2.1. Arrays.asList()使用指南

最近使用Arrays.asList()遇到了一些坑,然後在網上看到這篇文章: 感覺挺不錯的,但是還不是特別全面。所以,自己對於這塊小知識點進行了簡單的總結。

2.1.1. 簡介

Arrays.asList()在平時開發中還是比較常見的,我們可以使用它將一個數組轉換為一個List集合。

String[] myArray = { "Apple", "Banana", "Orange" }; 
List<String> myList = Arrays.asList(myArray);
//上面兩個語句等價於下面一條語句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");

JDK 源碼對於這個方法的說明:

/**
 *返回由指定數組支持的固定大小的列表。此方法作為基於數組和基於集合的API之間的橋樑,與           Collection.toArray()結合使用。返回的List是可序列化並實現RandomAccess接口。
 */ 
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

2.1.2. 《阿里巴巴Java 開發手冊》對其的描述

Arrays.asList()將數組轉換為集合后,底層其實還是數組,《阿里巴巴Java 開發手冊》對於這個方法有如下描述:

2.1.3. 使用時的注意事項總結

傳遞的數組必須是對象數組,而不是基本類型。

Arrays.asList()是泛型方法,傳入的對象必須是對象數組。

int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//數組地址值
System.out.println(myList.get(1));//報錯:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[0]);//1

當傳入一個原生數據類型數組時,Arrays.asList() 的真正得到的參數就不是數組中的元素,而是數組對象本身!此時List 的唯一元素就是這個數組,這也就解釋了上面的代碼。

我們使用包裝類型數組就可以解決這個問題。

Integer[] myArray = { 1, 2, 3 };

使用集合的修改方法:add()remove()clear()會拋出異常。

List myList = Arrays.asList(1, 2, 3);
myList.add(4);//運行時報錯:UnsupportedOperationException
myList.remove(1);//運行時報錯:UnsupportedOperationException
myList.clear();//運行時報錯:UnsupportedOperationException

Arrays.asList() 方法返回的並不是 java.util.ArrayList ,而是 java.util.Arrays 的一個內部類,這個內部類並沒有實現集合的修改方法或者說並沒有重寫這些方法。

List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList

下圖是java.util.Arrays$ArrayList的簡易源碼,我們可以看到這個類重寫的方法有哪些。

  private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        ...

        @Override
        public E get(int index) {
          ...
        }

        @Override
        public E set(int index, E element) {
          ...
        }

        @Override
        public int indexOf(Object o) {
          ...
        }

        @Override
        public boolean contains(Object o) {
           ...
        }

        @Override
        public void forEach(Consumer<? super E> action) {
          ...
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
          ...
        }

        @Override
        public void sort(Comparator<? super E> c) {
          ...
        }
    }

我們再看一下java.util.AbstractListremove()方法,這樣我們就明白為啥會拋出UnsupportedOperationException

public E remove(int index) {
    throw new UnsupportedOperationException();
}

2.1.4. 如何正確的將數組轉換為ArrayList?

stackoverflow:https://dwz.cn/vcBkTiTW

1. 自己動手實現(教育目的)

//JDK1.5+
static <T> List<T> arrayToList(final T[] array) {
  final List<T> l = new ArrayList<T>(array.length);

  for (final T s : array) {
    l.add(s);
  }
  return (l);
}
Integer [] myArray = { 1, 2, 3 };
System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList

2. 最簡便的方法(推薦)

List list = new ArrayList<>(Arrays.asList("a", "b", "c"))

3. 使用 Java8 的Stream(推薦)

Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本類型也可以實現轉換(依賴boxed的裝箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

4. 使用 Guava(推薦)

對於不可變集合,你可以使用類及其與工廠方法:(參數不能為空)

List<String> il = ImmutableList.of("string", "elements");  // from varargs
List<String> il = ImmutableList.copyOf(aStringArray);      // from array

對於可變集合,你可以使用類及其工廠方法:

List<String> l1 = Lists.newArrayList(anotherListOrCollection);    // from collection
List<String> l2 = Lists.newArrayList(aStringArray);               // from array
List<String> l3 = Lists.newArrayList("or", "string", "elements"); // from varargs

5. 使用 Apache Commons Collections

List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str);

2.2. Collection.toArray()方法使用的坑&如何反轉數組

該方法是一個泛型方法:<T> T[] toArray(T[] a); 如果toArray方法中沒有傳遞任何參數的話返回的是Object類型數組。

String [] s= new String[]{
    "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//沒有指定類型的話會報錯

由於JVM優化,new String[0]作為Collection.toArray()方法的參數現在使用更好,new String[0]就是起一個模板的作用,指定了返回數組的類型,0是為了節省空間,因為它只是為了說明返回的類型。詳見:

2.3. 不要在 foreach 循環里進行元素的 remove/add 操作

如果要進行remove操作,可以調用迭代器的 remove方法而不是集合類的 remove 方法。因為如果列表在任何時間從結構上修改創建迭代器之後,以任何方式除非通過迭代器自身remove/add方法,迭代器都將拋出一個ConcurrentModificationException,這就是單線程狀態下產生的 fail-fast 機制

fail-fast 機制 :多個線程對 fail-fast 集合進行修改的時,可能會拋出ConcurrentModificationException,單線程下也會出現這種情況,上面已經提到過。

java.util包下面的所有的集合類都是fail-fast的,而java.util.concurrent包下面的所有的類都是fail-safe的。

開源項目推薦

作者的其他開源項目推薦:

  1. :【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。
  2. : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一起維護)。
  3. : 我覺得技術人員應該有的一些好習慣!
  4. :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。

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

【其他文章推薦】

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

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

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

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

SpringBoot 整合NoSql

通用配置

maven依賴

添加Spring-Web和Spring-Security依賴,使用Spring-Security是因為使用SpringBoot的Redis依賴時,必須添加Spring-Security。在新版本SpringBoot才會這樣。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

properties配置

8080端口指定一下,因為下面雙開服務器這個配置必須在這裏显示加上。

server.port=8080

測試類

@RestController
public class HelloController {

    @Value("${server.port}")
    Integer port;

    @GetMapping("/set")
    public String set(HttpSession session) {
        session.setAttribute("name", "johnson");
        return String.valueOf(port);
    }

    @GetMapping("/get")
    public String get(HttpSession session) {
        return (String)session.getAttribute("name") + port;
    }
}

整合Redis

maven依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

連接redis必須要密碼,否則連接不上,所以你的redis服務器必須設置密碼

spring.redis.host=127.0.0.1
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=123456

啟動后,瀏覽器打開localhost:8080,賬號默認為user,密碼在控制台打印出來了,可以去看看。頁面如下:

Redis下的Session共享

當我們開啟兩個或多個Tomcat時,如何在這兩個Tomcat服務中共享Session呢,而Spring直接扔個依賴給你,安裝這個依賴就好了。
???????????? execute me!?
就是這麼簡單,添加spring-session-data-redis依賴就好了,如下:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

測試Session共享

使用maven使用package指令打包出來出來后(IDEA的Maven工具有package按鈕,點一下就好),在target目錄下可以看到你打包好的jar包,就像這樣:

進入到tartget目錄后,打開兩個命令窗口, 分別輸入以下命令:

java -jar sessionhare-0.0.1-SNAPSHOT.jar  --server.port=8080 //窗口1命令
java -jar sessionhare-0.0.1-SNAPSHOT.jar  --server.port=8081 //窗口2命令

然後打開頁面localhost:8080,賬號默認為user,密碼可以在8080的控制台看到,登錄成功后,
再打開頁面localhost:8081,你會發現不需要再次登錄啦,Session共享成功!

Nginx的負載均衡

安裝Nginx可以參考我之前的文章
如果是Ubuntu或者其他類型的系統,就依賴項不同,安裝方式還是一樣的。

nginx配置

nginx配置在路徑在/usr/local/nginx/conf/nginx.conf, 修改配置如下:
http模塊下修改。

    upstream colablog.cn {
        server 127.0.0.1:8080 weight=1;
        server 127.0.0.1:8081 weight=1;
    }
    server {
        listen      80;
        server_name localhost;
        
        location / {
           proxy_pass http://colablog.cn;
           proxy_redirect default;
        }
    }

修改完nginx配置後記得要重新加載一下配置文件,修改配置文件后必須重新指定配置文件,否則啟動會報錯。

sudo /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf //重新指定配置文件
sudo /usr/local/nginx/sbin/nginx -s reload //重新啟動nginx

把剛才項目打包出來的jar包扔到Linux服務器上,讓程序在服務器後台運行,使用如下命令:

$ nohup java -jar sessionhare-0.0.1-SNAPSHOT.jar --server.port=8080 > 8080.log &
$ nohup java -jar sessionhare-0.0.1-SNAPSHOT.jar --server.port=8081 > 8081.log &

打開你Linux服務器的ip地址就可以看到了。在瀏覽器打開你的虛擬機ip/set你的虛擬機ip/get,重複打開幾次就會發現訪問不同的端口。

MongoDb

整合MongoDb就像整合Redis那麼簡單,依賴和配置文件搞一下就行了

maven依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

properties配置

spring.data.mongodb.host=127.0.0.1
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.username=johnson
spring.data.mongodb.password=123456
spring.data.mongodb.port=27017
spring.data.mongodb.database=johnson

這樣就已經配置好了,不過我們還是測試一下吧。

測試

Book實體類

public class Book {

    private Integer id;

    private String name;

    private String author;

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", author='" + author + '\'' +
                '}';
    }

    public Integer getId() { return id; }

    public void setId(Integer id) { this.id = id; }

    public String getName() { return name; }

    public void setName(String name) { this.name = name; }

    public String getAuthor() { return author; }

    public void setAuthor(String author) { this.author = author; }
}

dao接口

public interface BookDao extends MongoRepository<Book, Integer> {
    List<Book> findBookByNameContaining(String name);
}

測試類

@SpringBootTest
class MongoApplicationTests {
    @Autowired
    BookDao dao;

    @Test
    void contextLoads() {
        Book book = new Book();
        book.setName("colablog");
        book.setId(1);
        book.setAuthor("johnson");
        dao.insert(book);
    }

    @Test
    public void getList() {
        List<Book> all = dao.findAll();
        System.out.println(all);
        List<Book> cola = dao.findBookByNameContaining("cola");
        System.out.println(cola);
    }

    @Autowired
    MongoTemplate template;

    @Test
    public void test1() {
        Book book = new Book();
        book.setName("colablog2");
        book.setId(2);
        book.setAuthor("johnson2");
        template.insert(book);

        List<Book> all = template.findAll(Book.class);
        System.out.println(all);
    }
}

總結

文章主要是根據總結了視頻第六章內容,
代碼貼的有點多,因為測試用例的關係,真是抱歉,因為有測試用例才能證明程序能走通。
好了,感謝各位的閱讀,文章若有不足之處或更好的建議,請在下方留言,Thanks(・ω・)ノ。

個人博客網址: https://colablog.cn/

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

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

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

彰化搬家費用,南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

越南市場出售活體老鼠野鳥 蒼蠅到處飛環境髒亂

摘錄自2020年03月16日聯合新聞網報導

自從武漢肺炎(COVID-19)爆發至今已三個月,而疫情的起源被指向中國湖北省武漢的華南海鮮市場,當中販賣的各式野味到處混雜,直至疫情爆發後商家才停止販售,整個市場遭封閉消毒。雖然近期野味交易在中國明令禁止,但在越南市場上仍持續進行,有民眾目擊市場內販售著活體鳥類和老鼠,現場環境髒亂,蒼蠅圍繞著野味打轉。

根據英國「太陽報」報導,越南隆安省一處市場仍在販售活體野味,遭販售的鳥類被關在籠子內,腳被綁成一束,翅膀遭折斷,且眼睛遭縫合,痛苦地在市場喊叫,目擊者表示「賣家縫住牠們的眼睛,用膠帶黏住喙,折斷翅膀,拔出羽毛,並使用一個微型儲氣瓶快速地將牠們煮熟」,而他也指出市場內還販售烏龜、蛇、老鼠和水獺等動物,現場環境相當髒亂。

生活環境
國際新聞
越南
野味
活體動物
老鼠

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

未完成恐攻應變設施 日本川內核電廠成停機首例

摘錄自2020年03月16日中央廣播電台報導

位於日本鹿兒島縣的九州電力公司川內核電廠一號機,因未能在期限內完成「恐攻應變設施」,今(16日)下午將停止反應爐運轉,寫下運轉中的核電廠因不符新規範標準停機首例。

日本放送協會(NHK)報導,日本原子力規制委員會基於新規範標準,要求核電廠在提出建設因應恐攻或航機衝撞等設施的工程計畫獲同意後,必須在五年內完成興建。九州電力公司因為無法在規定期限內完成川內核電廠一號機恐攻應變設施,預計下午停止運轉。

日本原子力規制委員會曾因為沒有規定核電廠提出因應海嘯評估與對策的期限,結果造成福島第一核電廠發生核子事故憾事。基於這樣的教訓,對因應恐攻的應變設施不僅設有期限,也不同意延長,嚴格要求核電廠不符規範就必須停機。

核能
能源議題
國際新聞
日本
核電廠
核電廠反應爐

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

研究:極地冰蓋以1990年代六倍速融化 每年將有4億人受洪災影響

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

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

【其他文章推薦】

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

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

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

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

年幼江豚大嶼山擱淺 本年第14宗

摘錄自2020年03月15日星島日報報導

香港海洋公園保育基金昨(14日)接報通知跟進在大嶼山水口一宗鯨豚擱淺個案,為本年度的第14宗。

該擱淺的年幼江豚,性別有待確定,身長133厘米。其屍身已嚴重腐爛。個案並沒有表面傷痕,保育基金已把江豚帶回海洋公園化驗。

香港海洋公園保育基金呼籲公眾,若發現懷疑鯨豚擱淺個案,可致電 1823 熱線,並提供發現擱淺的時間、地點、相片(如有),讓行動組能盡快到場跟進。

海洋
生態保育
國際新聞
香港
江豚
擱淺死亡

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

【其他文章推薦】

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

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

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

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?

現代轉攻新能源汽車 計畫2020年之前推26款新車

日前,據海外媒體報導,現代汽車集團將在2020年之前推出26款新能源汽車,其中包括旗下現代和起亞兩大品牌。現代汽車集團此舉意在搶佔新能源汽車市場的領先地位,爭取在2020年達到僅次於豐田的行業第二水準。

現代汽車集團計畫推出的26款新車型包括12款混動車型、6款插電式混動車型、2款純電動車型以及2款氫燃料電池車型等。其中,起亞將推出包括Niro混動版、凱尊(參配、圖片、詢價) 混動版、SoulEV在內的11款新車,而現代則會推出插電式混動版、純電動版IONIQ與豐田普銳斯一決高下。據現代汽車集團預估,到2020年現代汽車集團在新能源汽車市場的年銷量有望達到30萬台。

為了節省新能源汽車的開發成本,現代汽車集團相關負責人透露,旗下新能源車型將採用共用零部件策略。雖然每款車的動力系統在動力輸出上可能有所不同,但電動機直徑、核心部件等將會遵循統一化標準。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!