分類
發燒車訊

推薦一種通過刷leetcode來增強技術功底的方法

背景

如果前人認為這個一種學習提高或者檢驗能力的成功實踐。而自己目前又沒有更好的方法,那就不妨試一試。

而不管作為面試官還是被面試者,編碼題最近越來越流行。而兩種角色都需要思考的問題是希望考察什麼能力,通過什麼題目,需要達到怎樣的程度可以說明面試者具有了這樣的能力。

而要找到上面這些問題的答案,比較好的方式除了看一些理論性文章和接受培訓之外,自己動手刷一刷leetcode切身實踐一下不失為一個不錯的方式。而既然要花精力去做這件事情,那就需要解決一個問題:我從中可以獲得什麼提高。以下是個人的一些經驗和感受。

 

收益

對底層有了更深入的了解和思考

leetcode一些常見題也是平時工作中常用的一些底層數據結構的實現方法。 

先舉個大家使用比較多的算法:LRU(最近最少使用),在Java的實現中實現特別簡單。只是使用了LinkedHashMap這種數據結構。而如果看一些開源包里LRU的實現,發現也是這樣實現的。實際上動手實現一遍,LRU就再也不會忘了。

再舉個數據結構的例子:字典樹又叫前綴樹。它是搜索和推薦的基礎。標準點的定義是:

字典樹又稱單詞查找樹,Tire樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

因為之前做過搜索引擎,一直也對這塊很有興趣,所以對它底層知識的補充對個人而言,感覺深度有增加。

 

  

養成評估時空開銷的習慣

我刷leetcode必看官方解答里估算的時間和空間複雜度。這也是作為一個架構師的必備基本能力。

數組、哈希這些因為數據的位置不需要進行查找,只需要算數計算就可以得到,所以它們的時間複雜度是O(1)。

鏈表如果直接在頭部或者尾部插入,因為不需要查找,所以時間複雜度也是O(1),但是定位的話因為涉及查找,按遍歷查找來算是log(n)。所以對於jdk1.7之前,hashmap底層採用的是數組+鏈表的數據結構。所以如果不算哈希衝突和擴容的話,獲取和插入數據的時間複雜度是O(1);如果出現了哈希衝突,則插入因為是頭部插入,時間複雜度還是O(1);獲取時間複雜度因為涉及先查找,所以是O(n),這個n代表衝突的數量。

對於在有序數據中進行查找,因為可採用二分查找等優化,時間複雜度可降到log(n).

對於遞歸調用,如果遞歸方法內進行2次調用。對於層數n來說,時間複雜度是2的n次方。舉個例子就是一個數等於前面兩個數之和。當然,如果是前面3個數之和,不進行優化的情況下時間複雜度就是3的n次方。

對於一個n*m的二維數組等需要進行嵌套循環遍歷的,時間複雜度是O(n*m),有個特殊情況是n*m,這時候時間複雜度是n的平方。

對於全排列的情況,時間複雜度是O(n!)。

 

代碼簡化的方法

 

我習慣的一種學習方法是先做題,有了一定自己的總結和思考之後,再看書學習別人的總結思考方法。對於刷leetcode相關性高,也比較受認可的書是《Cracking the Coding interview(6th)》,中文版翻譯是《程序員面試金典》。這本書對於面試官和面試者來說讀了都會有一定的收穫。

 

我讀了這本書,對我印象最深的是介紹了兩種代碼優化的方法:BUD和BCR。

 

BUD 

BUD是瓶頸、不必要工作、重複工作 三個詞組首字母的縮寫。

 

作者提出拿到一道編程題,可先嘗試用暴力解法將題目寫出來,之後找到解法的性能瓶頸,針對瓶頸進行優化,之後在去掉不必要的工作,最後去掉重複的工作。

這個經典的編程優化方法不只可應用於編程,還可應用於整個程序的優化,也是最常規的優化方法。

 

BCR

BCR是Best Conceivable Runtime的縮寫,意思是想知道自己可以優化到什麼程度,先估算可達到的最優情況。

比如:在一個無序數組中,查找兩個兩個相同的數。直覺來說如果找到這兩個數,最起碼需要將每個數都遍歷一遍,所以可達到的最優情況是O(n),無論怎麼優化,都不可能比這個更好。所以這就是優化的上限。

這本書里還介紹了其他的優化方法如:使用額外數據結構、通過構建測試用例、根據題目的限制和提示來尋找線索,大家看這本書的時候可以了解下。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

分類
發燒車訊

作為一個Java開發你用過Jib嗎

1. 前言

JibGoogle開發的可以直接構建 Java應用的DockerOCI鏡像的類庫,以MavenGradle插件形式提供。它最騷操作的是可以在沒有Docker守護程序的情況下構建,也就是說,您不必在計算機上安裝docker守護程序!儘管Spring Boot 2.3.0.RELEASE已經推出了構建鏡像的功能,胖哥還是忍不住要試試Jib

其實最騷的還是名字。

2. Docker構建流程和Jib的構建流程

沒有對比就沒有傷害。我們還是要對比一下這兩者的構建流程。

Docker構建流程需要我們先把項目打成Jar然後編寫Dockerfile,然後使用Docker構建功能進行構建鏡像、運行容器。流程如下:

而Jib是這樣構建的:

作為一個Java開發者,不用再關心各種無關的命令和操作,只需要專註於Java,而且高效穩定以及可復用的增量構建。為什麼Jib能這麼快而高效?

傳統上,將Java應用程序與應用程序Jar一起構建為單個圖像層,而Jib的構建策略將Java應用程序分為多層,以進行更細化的增量構建。更改代碼時,僅重建更改,而不重建整個應用程序。

3. Jib構建Spring Boot應用

接下來我將演示如何將Spring Boot 應用打成鏡像並上傳到Dockerhub倉庫。

Maven工程為例,我們只需要在pom.xml中引入Jib Maven 插件。默認情況下Jib會把我們打好的鏡像上傳到Googlegcr.io倉庫,實際中我們會把打好的鏡像上傳到私有倉庫,所以我們要加一些個性化配置。這裏我以dockerhub倉庫為例添加一些個性化配置:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>2.4.0</version>
    <configuration>
        <!-- 相當於 Dockerfile 中的 FROM -->
        <from>
            <image>amazoncorretto:8</image>
        </from>
        <to>
            <!--構建鏡像名稱,這裏我使用maven中定義的項目名稱-->
            <image>daxus/${project.name}</image>
            <!--私有倉庫的賬號密碼-->
            <auth>
                <username>felordcn</username>
                <password>yourpassword</password>
            </auth>
            <!--Docker 鏡像的 tag 這裏使用maven定義的版本號-->
            <tags>
                <tag>
                    ${project.version}
                </tag>
            </tags>
        </to>
    </configuration>
</plugin>

然後在項目根目錄執行mvn clean compile jib:build就可以了。

其實也可以簡單引入Jib插件:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>2.4.0</version>
</plugin>

只不過我們的命令會更複雜一些,需要指定一些必要的參數,例如:

mvn clean compile jib:build \
    -Djib.to.image=myregistry/myimage:latest \
    -Djib.to.auth.username=$USERNAME \
    -Djib.to.auth.password=$PASSWORD

更多的定製命令可參考官方文檔:

https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#extended-usage

4. 總結

Jib使用起來非常簡單,讓開發人員以Java的風格來完成Docker鏡像的構建,能夠大大改善編程的體驗。多多關注:碼農小胖哥 獲取更多有用的編程乾貨教程。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

分類
發燒車訊

聊聊算法–堆的構建和調整

先提個問題,完全二叉樹/滿二叉樹,區別?前者是指每一層都是緊湊靠左排列,最後一層可能未排滿,後者是一種特殊的完全二叉樹,

每層都是滿的,即節點總數和深度滿足N=(2^n) -1。堆Heap,一堆蘋果,為了賣相好,越好看的越往上放,就是大頂堆;為了蘋果堆

的穩定,質量越小越往上放,就是小頂堆;堆首先是完全二叉樹,但只確保父節點和子節點大小邏輯,不關心左右子節點的大小關係,

通常是一個可以被看做一棵樹的數組對象,是個很常見的結構,比如BST對象,都與堆有關係,今天就說下這個重要的數據結構和應用。

 

作者原創文章,謝絕一切轉載,違者必究!

本文只發表在”公眾號”和”博客園”,其他均屬複製粘貼!如果覺得排版不清晰,請查看公眾號文章。 

 

準備:

Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4

難度: 新手–戰士–老兵–大師

目標:

1.堆的構建和調整算法

1 優先級隊列

為理解堆的原理,先看優先級隊列,它是一種數據結構,插入或者刪除元素的時候,元素會自動排序,(優先級不是狹義的數值大小,

但為了通俗理解,這裏以字母序為例),通常使用數組存儲,我們可以按照下圖進行轉換,序號 0 不用:

優先級隊列的實現(Java版):

public class PriorityQueue<Key extends Character> {
    /** 存儲元素的數組 */
    private Key[] keys;
    private int N = 0;

    public PriorityQueue(int capacity){
        // 下標0不用,多分配一個單位
        keys = (Key[]) new Character[capacity + 1];
    }

    public Key max(){
        return keys[1];
    }

    public void insert(Key e){
        N ++;
        keys[N] = e;
        swim(N);
    }
    public Key delMax(){
        Key max = keys[1];
        swap(1,N);
        keys[N] = null;
        N --;
        // 讓第一個元素下沉到合適的位置
        sink(1);
        return max;
    }
    /** 上浮第k個元素*/
    private void swim(int k){
        // 比父節點小,即進行交換,直到根
        while (k > 1 && less(parent(k),k)){
            swap(k,parent(k));
            k = parent(k);
        }
    }
    /** 下沉第 k 個元素*/
    private void sink(int k){
        while(k < N){
            int small = left(k);
            if (right(k) < N && less(right(k),left(k))){
                small = right(k);
            }
            if (less(k,small)){
                swap(k,small);
                k = small;
            }
        }
    }
    private void swap(int i,int j){
        Key temp = keys[i];
        keys[i] = keys[j];
        keys[j] = temp;
    }
    /** 元素i和j大小比較*/
    private boolean less(int i,int j){
//   'a' - 'b' = -1 ;
        return keys[i].compareTo(keys[j]) > 0;
    }
    /** 元素i的父節點*/
    private int parent(int i){
        return i/2;
    }
    /** 元素i的左子節點*/
    private int left(int i){
        return i * 2;
    }
    /** 元素i的右子節點*/
    private int right(int i){
        return i * 2 + 1;
    }
}
 

以上代碼解析:

1 swim 上浮,對於元素k,是否需要上浮,僅需與其父節點比較,大於父節點則交換,迭代直到根節點;

2 sink 下沉,對於元素k,是否需要下沉,需先比較其左右子節點,找出左右子節點中較小者,較小者若比父節點大,則交換,迭代直到末尾元素;

3 insert 插入,先將元素放到數組末尾位置,再對其進行上浮操作,直到合適位置;

4 delMax 刪除最大值,大根堆,故第一個元素最大,先將首末元素交換,再刪除末尾元素,再對首元素下沉操作,直到合適位置;

總結:以上只是Java簡化版,java.util.PriorityQueue 是JDK原版,客官可自行研究。但設計還是非常有技巧的,值得思考一番,假設 insert 插入

到首位,會導致數組大量元素移動。delMax 若直接刪除首位最大值,則需要進一步判斷左右子節點大小,並進行先子節點上浮再首元素下沉操作。

        有了這個堆結構,就可以進行堆排序了,將待排數全部加入此堆結構,然後依次取出,即成有序序列了!

2 堆排序

如要求不使用上述堆數據結構。思路(升序為例):將數組構建為一個大頂堆,首元素即為數組最大值,首尾元素交換;排除末尾元素后調整大頂堆,

則新的首元素即為次最大值,交換首尾並再排除末尾元素;如此循環,最後的數組即為升序排列

public class HeapSort02 {
    public static void main(String []args){
        int []arr = {2,1,8,6,4,7,3,0,9,5};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int []arr){
        int len = arr.length;
        // 創建一個大頂堆
        for(int i = (int) Math.ceil(len/2 - 1); i >= 0; i--){
            //從第一個非恭弘=叶 恭弘子結點從下至上,從右至左調整結構
            adjustHeap(arr,i,len);
        }
        // 交換首尾元素,並重新調整大頂堆
        for(int j = len-1;j > 0;j--){
            swap(arr,0,j);
            adjustHeap(arr,0,j);
        }
    }

    /** 迭代寫法*/
    public static void adjustHeap(int []arr,int i,int length){
        int temp = arr[i];
        for (int k = 2*i + 1; k < length; k=k*2 + 1) {
        // 注意這裏的k + 1 < length
            // 如果右子節點大於左子節點,則比較對象為右子節點
            if (k + 1 < length && arr[k] < arr[k+1]){
                k++;
            }
            if (arr[k] > temp){
                // 不進行值交換
                arr[i] = arr[k];
                i = k;
            }
            else{
                break;
            }
        }
        arr[i] = temp;
    }

    /** 遞歸寫法*/
    private static void adjustHeap2(int[] arr, int i, int len){
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int maxIndex = i;
        // 注意這裏的 left < len
        if (left < len && arr[left] > arr[maxIndex]){
            maxIndex = left;
        }
        if (right < len && arr[right] > arr[maxIndex]){
            maxIndex = right;
        }
        if (maxIndex != i){
            swap(arr,i,maxIndex);
            adjustHeap2(arr,maxIndex,len);
        }
    }

    /** 交換元素 */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
 

以上代碼解析:

1完全二叉樹結構中,如果根節點順序號為 0,總節點數為 N,則最末節點的父節點即為最後一個非恭弘=叶 恭弘子節點,順序號為 ceil(N/2 -1),

2 adjustHeap2 為啥使用三個參數,不用中間的參數可以?使用三個參數,是為了進行遞歸調用,因為遞歸肯定是縮小計算規模,而這裏的形參arr和len是固定不變的;

3 adjustHeap是非遞歸寫法,不用中間的參數可以?調用一在“構建大頂堆”處,可寫為函數體內初始化 i,並形成雙重 for 循環;調用二在“重新調整大頂堆”處,

    可見中間參數為 0,可直接去掉。故回答是可以!但需要調整寫法,且影響該方法復用,這裏直接寫為三個形參的函數更為優雅而已。

4非遞歸寫法理解:類似插入排序思想(依次移動並找到合適的位置再插入),先將 arr[i] 取出,然後此節點和左右子樹進行比較,如子樹更大則子節點上升一層,使

    用for循環迭代到最終位置,並進行賦值;

 

以 i=0 為例:

5遞歸方式理解:定位目標元素的左右子樹,若子樹值更大,則進行值交換,且因為子樹發生了變化,故需要對子樹進行遞歸處理;

3 前K個最大的數

在N個數中找出前K個最大的數: 思路:從N個數中取出前K個數,形成一個數組[K],將該數組調整為一個小頂堆,則可知堆頂為K個數中最小值,

然後依次將剩餘 N-K 個數與堆頂比較,若大於,則替換掉並調整堆,直到所有元素加入完畢,堆中元素即為目標集合。

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[100];
        for (int i = 0; i < 100; i++) {
            arr[i] = i + 1;
        }
        // 前10個最大的數
        int k = 10;
        // 構造小頂堆
        for (int i = (int) Math.ceil(k/2 - 1); i >= 0; i--) {
            adjustHeap(arr,i,k);
        }
        // 依次比較剩餘元素
        for (int i = 10; i < arr.length; i++) {
            if (arr[i] > arr[0]){
                swap(arr,0,i);
                adjustHeap(arr,0,k);
            }
        }
        // 輸出結果
        for (int i = 0; i < 10; i++) {
            System.out.print(arr[i]+"-");
        }
    }

    /** 非迭代寫法 ,對arr[i]進行調整 */
    private static void adjustHeap(int[] arr,int i,int length){
        int temp = arr[i];
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            // 因第一次循環中可能越界,故需要 k+1 < length
            if (k + 1 < length && arr[k] > arr[k + 1]){
                k++;
            }
            if (arr[k] < temp){
                arr[i] = arr[k];
                i = k;
            }
            else {
                break;
            }
        }
        arr[i] = temp;
    }
    /** 遞歸寫法 */
    private static void adjustHeap2(int[] arr,int i,int length){
        int left = i * 2 + 1;
        int right = i * 2 + 2;
        int samller = i;
        if (left < length && arr[left] > arr[samller]){
            samller = right;
        }
        if (right < length && arr[right] > arr[samller]){
            samller = right;
        }
        if (samller != i){
            swap(arr,i,samller);
            adjustHeap2(arr,samller,length);
        }
    }

    /** 交換元素 */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
 

以上代碼解析:按照”初始化—構建小頂堆—比較調整—輸出結果”執行。注意for循環中,因第一次循環中未使用for語句條件判斷,可能越界,故需要 k+1 < length

輸出結果如下:

請看官思考,如果需求變為找出N個數中找出前K個最小的數,該如何實現? 建議動腦且動手的寫一遍!因為魔鬼在細節!

全文完!

我近期其他文章:

  • 1 Dubbo學習系列之十九(Apollo配置中心)
  • 2 聊聊算法——二分查找算法深度分析
  • 3 DevOps系列——Jenkins/Gitlab自動打包部署
  • 4 DevOps系列——Jenkins私服
  • 5 DevOps系列——Gitlab私服

    只寫原創,敬請關注 

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

為什麼建議你使用枚舉?

枚舉是 JDK 1.5 新增的數據類型,使用枚舉我們可以很好的描述一些特定的業務場景,比如一年中的春、夏、秋、冬,還有每周的周一到周天,還有各種顏色,以及可以用它來描述一些狀態信息,比如錯誤碼等。

枚舉類型不止存在在 Java 語言中,在其它語言中也都能找到它的身影,例如 C# 和 Python 等,但我發現在實際的項目中使用枚舉的人很少,所以本文就來聊一聊枚舉的相關內容,好讓朋友們對枚舉有一個大概的印象,這樣在編程時起碼還能想到有“枚舉”這樣一個類型。

本文的結構目錄如下:

枚舉的 7 種使用方法

很多人不使用枚舉的一個重要的原因是對枚舉不夠熟悉,那麼我們就先從枚舉的 7 種使用方法說起。

用法一:常量

在 JDK 1.5 之前,我們定義常量都是 public static final... ,但有了枚舉,我們就可以把這些常量定義成一個枚舉類了,實現代碼如下:

public enum ColorEnum {  
  RED, GREEN, BLANK, YELLOW  
} 

用法二:switch

將枚舉用在 switch 判斷中,使得代碼可讀性更高了,實現代碼如下:

enum ColorEnum {
    GREEN, YELLOW, RED
}
public class ColorTest {
    ColorEnum color = ColorEnum.RED;

    public void change() {
        switch (color) {
            case RED:
                color = ColorEnum.GREEN;
                break;
            case YELLOW:
                color = ColorEnum.RED;
                break;
            case GREEN:
                color = ColorEnum.YELLOW;
                break;
        }
    }
}

用法三:枚舉中增加方法

我們可以在枚舉中增加一些方法,讓枚舉具備更多的特性,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ErrorCodeEnum errorCode = ErrorCodeEnum.SUCCESS;
        System.out.println("狀態碼:" + errorCode.code() + 
                           " 狀態信息:" + errorCode.msg());
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    PARAM_ERROR(1001, "parameter error"),
    SYS_ERROR(1003, "system error"),
    NAMESPACE_NOT_FOUND(2001, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }

    public static ErrorCodeEnum getErrorCode(int code) {
        for (ErrorCodeEnum it : ErrorCodeEnum.values()) {
            if (it.code() == code) {
                return it;
            }
        }
        return UNKNOWN_ERROR;
    }
}

以上程序的執行結果為:

狀態碼:1000 狀態信息:success

用法四:覆蓋枚舉方法

我們可以覆蓋一些枚舉中的方法用於實現自己的業務,比如我們可以覆蓋 toString() 方法,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        System.out.println(colorEnum.toString());
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    //  成員變量
    private String name;
    private int index;

    //  構造方法
    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    //覆蓋方法
    @Override
    public String toString() {
        return this.index + ":" + this.name;
    }
}

以上程序的執行結果為:

1:紅色

用法五:實現接口

枚舉類可以用來實現接口,但不能用於繼承類,因為枚舉默認繼承了 java.lang.Enum 類,在 Java 語言中允許實現多接口,但不能繼承多個父類,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        colorEnum.print();
        System.out.println("顏色:" + colorEnum.getInfo());
    }
}

interface Behaviour {
    void print();

    String getInfo();
}

enum ColorEnum implements Behaviour {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }

    @Override
    public String getInfo() {
        return this.name;
    }
}

以上程序的執行結果為:

1:紅色

顏色:紅色

用法六:在接口中組織枚舉類

我們可以在一個接口中創建多個枚舉類,用它可以很好的實現“多態”,也就是說我們可以將擁有相同特性,但又有細微實現差別的枚舉類聚集在一個接口中,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        // 賦值第一個枚舉類
        ColorInterface colorEnum = ColorInterface.ColorEnum.RED;
        System.out.println(colorEnum);
        // 賦值第二個枚舉類
        colorEnum = ColorInterface.NewColorEnum.NEW_RED;
        System.out.println(colorEnum);
    }
}

interface ColorInterface {
    enum ColorEnum implements ColorInterface {
        GREEN, YELLOW, RED
    }
    enum NewColorEnum implements ColorInterface {
        NEW_GREEN, NEW_YELLOW, NEW_RED
    }
}

以上程序的執行結果為:

RED

NEW_RED

用法七:使用枚舉集合

在 Java 語言中和枚舉類相關的,還有兩個枚舉集合類 java.util.EnumSetjava.util.EnumMap,使用它們可以實現更多的功能。

使用 EnumSet 可以保證元素不重複,並且能獲取指定範圍內的元素,示例代碼如下:

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

public class EnumTest {
    public static void main(String[] args) {
        List<ColorEnum> list = new ArrayList<ColorEnum>();
        list.add(ColorEnum.RED);
        list.add(ColorEnum.RED);  // 重複元素
        list.add(ColorEnum.YELLOW);
        list.add(ColorEnum.GREEN);
        // 去掉重複數據
        EnumSet<ColorEnum> enumSet = EnumSet.copyOf(list);
        System.out.println("去重:" + enumSet);

        // 獲取指定範圍的枚舉(獲取所有的失敗狀態)
        EnumSet<ErrorCodeEnum> errorCodeEnums = EnumSet.range(ErrorCodeEnum.ERROR, ErrorCodeEnum.UNKNOWN_ERROR);
        System.out.println("所有失敗狀態:" + errorCodeEnums);
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    ERROR(2001, "parameter error"),
    SYS_ERROR(2002, "system error"),
    NAMESPACE_NOT_FOUND(2003, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }
}

以上程序的執行結果為:

去重:[RED, GREEN, YELLOW]

所有失敗狀態:[ERROR, SYS_ERROR, NAMESPACE_NOT_FOUND, NODE_NOT_EXIST, NODE_ALREADY_EXIST, UNKNOWN_ERROR]

EnumMapHashMap 類似,不過它是一個專門為枚舉設計的 Map 集合,相比 HashMap 來說它的性能更高,因為它內部放棄使用鏈表和紅黑樹的結構,採用數組作為數據存儲的結構。

EnumMap 基本使用示例如下:

import java.util.EnumMap;

public class EnumTest {
    public static void main(String[] args) {
        EnumMap<ColorEnum, String> enumMap = new EnumMap<>(ColorEnum.class);
        enumMap.put(ColorEnum.RED, "紅色");
        enumMap.put(ColorEnum.GREEN, "綠色");
        enumMap.put(ColorEnum.BLANK, "白色");
        enumMap.put(ColorEnum.YELLOW, "黃色");
        System.out.println(ColorEnum.RED + ":" + enumMap.get(ColorEnum.RED));
    }
}

enum ColorEnum {
    RED, GREEN, BLANK, YELLOW;
}

以上程序的執行結果為:

RED:紅色

使用注意事項

阿里《Java開發手冊》對枚舉的相關規定如下,我們在使用時需要稍微注意一下。

【強制】所有的枚舉類型字段必須要有註釋,說明每個數據項的用途。

【參考】枚舉類名帶上 Enum 後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。正例:枚舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。

假如不使用枚舉

在枚舉沒有誕生之前,也就是 JDK 1.5 版本之前,我們通常會使用 int 常量來表示枚舉,實現代碼如下:

public static final int COLOR_RED = 1;
public static final int COLOR_BLUE = 2;
public static final int COLOR_GREEN = 3;

但是使用 int 類型可能存在兩個問題:

第一, int 類型本身並不具備安全性,假如某個程序員在定義 int 時少些了一個 final 關鍵字,那麼就會存在被其他人修改的風險,而反觀枚舉類,它“天然”就是一個常量類,不存在被修改的風險(原因詳見下半部分);

第二,使用 int 類型的語義不夠明確,比如我們在控制台打印時如果只輸出 1…2…3 這樣的数字,我們肯定不知道它代表的是什麼含義。

那有人就說了,那就使用常量字符唄,這總不會還不知道語義吧?實現示例代碼如下:

public static final String COLOR_RED = "RED";
public static final String COLOR_BLUE = "BLUE";
public static final String COLOR_GREEN = "GREEN";

但是這樣同樣存在一個問題,有些初級程序員會不按套路出牌,他們可能會直接使用字符串的值進行比較,而不是直接使用枚舉的字段,實現示例代碼如下:

public class EnumTest {
    public static final String COLOR_RED = "RED";
    public static final String COLOR_BLUE = "BLUE";
    public static final String COLOR_GREEN = "GREEN";
    public static void main(String[] args) {
        String color = "BLUE";
        if ("BLUE".equals(color)) {
            System.out.println("藍色");
        }
    }
}

這樣當我們修改了枚舉中的值,那程序就涼涼了。

枚舉使用場景

枚舉的常見使用場景是單例,它的完整實現代碼如下:

public class Singleton {
    // 枚舉類型是線程安全的,並且只會裝載一次
    private enum SingletonEnum {
        INSTANCE;
        // 聲明單例對象
        private final Singleton instance;
        // 實例化
        SingletonEnum() {
            instance = new Singleton();
        }
        private Singleton getInstance() {
            return instance;
        }
    }
    // 獲取實例(單例對象)
    public static Singleton getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }
    private Singleton() {
    }
    // 類方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}

因為枚舉只會在類加載時裝載一次,所以它是線程安全的,這也是《Effective Java》作者極力推薦使用枚舉來實現單例的主要原因。

知識擴展

枚舉為什麼是線程安全的?

這一點要從枚舉最終生成的字節碼說起,首先我們先來定義一個簡單的枚舉類:

public enum ColorEnumTest {
    RED, GREEN, BLANK, YELLOW;
}

然後我們再將上面的那段代碼編譯為字節碼,具體內容如下:

public final class ColorEnumTest extends java.lang.Enum<ColorEnumTest> {
  public static final ColorEnumTest RED;
  public static final ColorEnumTest GREEN;
  public static final ColorEnumTest BLANK;
  public static final ColorEnumTest YELLOW;
  public static ColorEnumTest[] values();
  public static ColorEnumTest valueOf(java.lang.String);
  static {};
}

從上述結果可以看出枚舉類最終會被編譯為被 final 修飾的普通類,它的所有屬性也都會被 staticfinal 關鍵字修飾,所以枚舉類在項目啟動時就會被 JVM 加載並初始化,而這個執行過程是線程安全的,所以枚舉類也是線程安全的類。

小貼士:代碼反編譯的過程是先用 javac 命令將 java 代碼編譯字節碼(.class),再使用 javap 命令查看編譯的字節碼。

枚舉比較小技巧

我們在枚舉比較時使用 == 就夠了,因為枚舉類是在程序加載時就創建了(它並不是 new 出來的),並且枚舉類不允許在外部直接使用 new 關鍵字來創建枚舉實例,所以我們在使用枚舉類時本質上只有一個對象,因此在枚舉比較時使用 == 就夠了。

並且我們在查看枚舉的 equlas() 源碼會發現,它的內部其實還是直接調用了 == 方法,源碼如下:

public final boolean equals(Object other) {
    return this==other;
}

總結

本文我們介紹了枚舉類的 7 種使用方法:常量、switch、枚舉中添加方法、覆蓋枚舉方法、實現接口、在接口中組織枚舉類和使用枚舉集合等,然後講了如果不使用枚舉類使用 int 類型和 String 類型存在的一些弊端:語義不夠清晰、容易被修改、存在被誤用的風險,所以我們在適合的環境下應該盡量使用枚舉類。並且我們還講了枚舉類的使用場景——單例,以及枚舉類為什麼是安全的,最後我們講了枚舉比較的小技巧,希望本文對你有幫助。

查看 & 鳴謝

https://www.iteye.com/blog/softbeta-1185573

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

分類
發燒車訊

與特斯拉 Mddel X 比拼 奧迪 2018 年左右將推純電動車

國外媒體美國汽車新聞網報導,奧迪將推出全新的純電動 SUV 車型與特斯拉 Model X 車型競爭,但奧迪認為插電混合動力車型才是目前新能源汽車市場最理想的車型。   奧迪將在 2018 年左右發布一款續航里程可達到 498 公里的純電動 SUV,將採用全新的造型設計理念,基於第二代 MLB 平台研發,並藉鑑新一代奧迪 Q5 車型的部分技術與設計。奧迪全新的純電動 SUV 車型將可在 20 分鐘內完成 80% 的充電,續航里程超過特斯拉 Model X 車型,對其構成不小的威脅。   不過,奧迪執行長斯泰德(Rupert Stadler)近日表示在未來的 10 至 15 年內,插電混合動力汽車將是消費者選擇新能源汽車時的首選車型。插電混合動力車型使用一台汽油引擎或柴油引擎和一台電動機聯合驅動,有一定的純電動續航里程,十分適合消費者在市區中駕駛。但是斯泰德同時表示隨著充電網絡建設的繼續,消費者未來對電動車的接受程度將增加。      

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

【【其他文章推薦】

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

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

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

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

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

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

分類
發燒車訊

特斯拉將公布儲能電池計畫 F-貿聯可望受惠

特斯拉傳將於 4 月 30 日公布儲能電池計畫,市場推測,該計畫將包括住宅與公共規模市場,業界傳出,本來就是特斯拉重要夥伴的 F-貿聯,有機會獲新電力線訂單。而今年在特斯拉需求持續強勁,與 Type C 等外接式擴充介面的需求爆發下,法人預期,該公司今年營運將逐季走高,全年營收看增 15%。   F-貿聯首季營收 19.04 億元,年增 10%,創下歷年同期新高。而特斯拉傳出將於下周公布最新的儲能電池計畫,據市場傳言表示,貿聯本來就是特斯拉在電動車電池模組用線與超級充電樁的線束的主力供應商,未來不排除大型公共規模用儲能電池,將與現有充電站結合。而貿聯有機會順勢切入取得新產品,並以公用市場應用的電力線為主,近日有望已小量出貨。   法人預估,F-貿聯今年除來自特斯拉的需求成長外,另在其他車用佈局方面,包括全地形車大客戶全車線束、擴大歐美客戶於倒車雷達等新產品的開發;另外針對美國官方對公營單位節能環保的需求,也配合客戶開發出得以改裝現有車為電動車的配備裝置,隨未來該商業模式成型,貢獻將逐步擴大。  

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

分類
發燒車訊

(七) SpringBoot起飛之路-整合SpringSecurity(Mybatis、JDBC、內存)

興趣的朋友可以去了解一下前五篇,你的贊就是對我最大的支持,感謝大家!

(一) SpringBoot起飛之路-HelloWorld

(二) SpringBoot起飛之路-入門原理分析

(三) SpringBoot起飛之路-YAML配置小結(入門必知必會)

(四) SpringBoot起飛之路-靜態資源處理

(五) SpringBoot起飛之路-Thymeleaf模板引擎

(六) SpringBoot起飛之路-整合JdbcTemplate-Druid-MyBatis

說明:

  • 這一篇的目的還是整合,也就是一個具體的實操體驗,原理性的沒涉及到,我本身也沒有深入研究過,就不獻醜了

  • SpringBoot 起飛之路 系列文章的源碼,均同步上傳到 github 了,有需要的小夥伴,隨意去 down

    • https://github.com/ideal-20/Springboot-Study-Code
  • 才疏學淺,就會點淺薄的知識,大家權當一篇工具文來看啦,不喜勿憤哈 ~

(一) 初識 Spring Security

(1) 引言

權限以及安全問題,雖然並不是一個影響到程序、項目運行的必須條件,但是卻是開發中的一項重要考慮因素,例如某些資源我們不想被訪問到或者我們某些方法想要滿足指定身份才可以訪問,我們可以使用 AOP 或者過濾器來實現要求,但是實際上,如果代碼涉及的邏輯比較多以後,代碼是極其繁瑣,冗餘的,而有很多開發框架,例如 Spring Security,Shiro,已經為我們提供了這種功能,我們只需要知道如何正確配置以及使用它了

(2) 基本介紹

先看一下官網的介紹

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一個功能強大且高度可定製的身份驗證和訪問控制框架。它是保護基於spring的應用程序的實際標準。

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security是一個框架,側重於為Java應用程序提供身份驗證和授權。與所有Spring項目一樣,Spring安全性的真正強大之處在於它很容易擴展以滿足定製需求

簡單的說,Spring Security 就是一個控制訪問權限,強大且完善的框架

Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分,同時它們也是 Spring Security 提供的核心功能

用戶認證:用戶認證就是指這個用戶身份是否合法,一般我們的用戶認證就是通過校驗用戶名密碼,來判斷用戶身份的合法性,確定身份合法后,用戶就可以訪問該系統

用戶授權:如果不同的用戶需要有不同等級的權限,就涉及到用戶授權,用戶授權就是對用戶能訪問的資源,所能執行的操作進行控制,根據不同用戶角色來劃分不同的權限

(二) 靜態頁面導入 And 環境搭建

(1) 關於靜態頁面

A:頁面介紹

頁面是我自己臨時弄得,有需要的朋友可以去我 GitHub:ideal-20 下載源碼,簡單說明一下這個頁面

做一個靜態頁面如果嫌麻煩,也可以單純的自己創建一些簡單的頁面,寫幾個標題文字,能體現出當前是哪個頁面就好了

我代碼中用的這些頁面,就是拿開源的前端組件框架進行了一點的美化,然後方便講解一些功能,頁面模板主要是配合 Thymeleaf

1、目錄結構

├── index.html                        // 首頁
├── images                            // 首頁圖片,僅美觀,無實際作用
├── css                               // 上線項目文件,放在服務器即可正常訪問
├── js                                // 項目截圖
├── views                             // 總子頁面文件夾,權限驗證的關鍵頁面
│   ├── login.html					  // 自製登錄頁面(用來替代 Spring Security 默認的 )
│   ├── L-A							  // L-A 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-B							  // L-B 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-C							  // L-C 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html

B:導入到項目

主要就是把基本一些鏈接,引入什麼的先替換成 Thymeleaf 的標籤格式,這裏語法用的不是特別多,即使對於 Thymeleaf 不是很熟悉也是很容易看懂的,當然如果仍然感覺有點吃力,可以單純的做成 html,將就一下,或者去看一下我以前的文章哈,裏面有關於 Thymeleaf 入門的講解

css、image、js 放到 resources –> static 下 ,views 和 index.html 放到 resources –> templates下

(2) 環境搭建

A:引入依賴

這一部分引入也好,初始化項目的時候,勾選好自動生成也好,只要依賴正常導入了即可

  • 引入 Spring Security 模塊
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

關鍵的依賴主要就是上面這個啟動器,但是還有一些就是常規或者補充的了,例如 web、thymeleaf、devtools

thymeleaf-extras-springsecurity5 這個後面講解中會提到,是用來配合 Thymeleaf 整合 Spring Security 的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

B:頁面跳轉 Controller

因為我們用了模板,頁面的跳轉就需要交給 Controller 了,很簡單,首先是首頁的,當然關於頁面這個就無所謂了,我隨便跳轉到了我的博客,接着還有一個登錄頁面的跳轉

有一個小 Tip 需要提一下,因為 L-A、L-B、L-C 文件夾下都有3個頁面 a.html 、b.html 、c.html,所以可以利用 @PathVariable 寫一個較為通用的跳轉方法

@Controller
public class PageController {

    @RequestMapping({"/", "index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/about")
    public String toAboutPage() {
        return "redirect:http://www.ideal-20.cn";
    }

    @RequestMapping("/toLoginPage")
    public String toLoginPage() {
        return "views/login";
    }

    @RequestMapping("/levelA/{name}")
    public String toLevelAPage(@PathVariable("name") String name) {
        return "views/L-A/" + name;
    }

    @RequestMapping("/levelB/{name}")
    public String toLevelBPage(@PathVariable("name") String name) {
        return "views/L-B/" + name;
    }

    @RequestMapping("/levelC/{name}")
    public String toLevelCPage(@PathVariable("name") String name) {
        return "views/L-C/" + name;
    }
}

C:環境搭建最終效果

  • 為了貼圖方便,我把頁面拉窄了一點
  • 首頁右上角應該為登錄的鏈接,這裡是因為,我運行的是已經寫好的代碼,不登錄頁面例如 L-A-a 等模塊就显示不出來,所以拿一個定義好的管理員身份登陸了
  • 關於如何使其自動切換显示登陸還是登錄后信息,在後面會講解

1、首頁

2、子頁面

L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是一樣的,只是文字有一點變化

3、登陸頁面

(三) 整合 Spring Security (內存中)

這一部分,為了簡化一些,容易理解一些,沒有從帶數據的場景出發(因為涉及代碼少一些,所以講解會多一點),而是直接將一些身份等等寫死了,寫到了內存中,方便理解,接着會在下一個標題中給出含有數據庫的寫法(講解會少一些,重點只說一些與前一種的不同點)

(1) 配置授權內容

A:源碼了解用戶授權方式

可以去官網看一下,官網有提供給我們一些樣例,其中有一個關於配置類的小樣例,也就是下面這個,我們通過這個例子,展開分析

https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#jc-custom-dsls

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .apply(customDsl())
                .flag(true)
                .and()
            ...;
    }
}

1、創建 config –> SecurityConfig 配置類

  • 創建一個配置類,像官網中一樣,繼承 WebSecurityConfigurerAdapter
  • 類上添加 @EnableWebSecurity 註解,代表開啟WebSecurity模式
  • 重寫 configure(HttpSecurity http) 方法
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

既然是重寫,那麼我們可以點進去,看一下父類中關於 configure(HttpSecurity http) 方法的源碼註釋,它有很多有用的信息

我摘選出這麼兩小段,第一段的意思就是 ,我們想要使用 HttpSecurity ,要通過重寫,不能通過 super 調用,否則會有覆蓋問題,第二段就是給出了一個默認的配置方式

* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();

2、按照源碼的註釋分析

我們先按照剛才看到的註釋寫出來,首先能看到,它是支持一個鏈式調用的

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and().formLogin()
            .and().httpBasic();
}
  • 通過字面意思也很好理解,authorizeRequests 是關於請求授權的,所以要涉及到關於請求授權(允許指定身份用戶訪問不同權限的資源)的問題就需要調用了

  • 其次,anyRequest().authenticated() 也就是說所有HTTP請求都需要被認證

  • 接着看,通過 and() 連接了一些新的內容,例如選擇表單登錄還是 HTTPBasic 的方式(這裏認證的過程就是讓你輸入用戶名密碼,檢測你的身份,兩種方式表單或者那種彈窗)

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證,通常需要配合HTTPS來保證信息傳輸的安全

給大家演示一下:

  • 如果不指定一種認證方式 .and().formLogin() 或者 .and().httpBasic() 訪問任何頁面都會提示 403 禁止訪問的錯誤
  • 指定 .and().formLogin() 認證,彈出一個表單頁面(自帶的,和自己創建的沒關係)
  • 指定 .and().httpBasic(); 認證,彈出一個窗口進行 HTTPBasic 認證

B:自定製用戶授權

1、先看源碼註釋

默認配置,設定了所有 HTTP 請求 都需要進行認證,所以我們在訪問首頁等的時候也會被攔截,但是實際情況下,有一些頁面是可以被任何人訪問的,例如首頁,或者自定義的登陸的等頁面,這時候需要用自己定義一些用戶授權的規則

在 WebSecurityConfigurerAdapter 的 formLogin() 註釋附近,又看到了一個有意思的內容

注:&quot 代表引號

* 		http
* 			.authorizeRequests(authorizeRequests ->
* 				authorizeRequests
* 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
* 			)

這就是我們想要找的,自定義的配置,通過一個一個 antMatchers 進行匹配,通過 hasRole 來規定其合法的身份,也就是說只有滿足這個身份的用戶才能訪問前面規定的路徑資源

Matchers 前面的 ant 前綴代表着,他可以用 ant 風格的路徑表達式(舉例的時候就能看懂了)

通配符 說明
? 匹配任何單字符
* 匹配0或者任意數量的字符
** 匹配0或者更多的目錄

補充: 如果想用正則表達式的方式,可以用這個方法 .regexMatchers()

當然了,有很多情況下,你想要讓任何人都可以訪問某個路徑,例如首頁,permitAll() 方法 就可以達到這種效果,在這裏補充一些常用的方法

  • permitAll() :允許任何訪問

  • denyAll():拒絕所有訪問

  • anonymous():允許匿名用戶訪問

  • authenticated() :允許認證的用戶進行訪問

  • hasRole(String) :如果用戶具備給定角色(用戶組)的話,就允許訪問/

  • hasAnyRole(String…) :如果用戶具有給定角色(用戶組)中的一個的話,允許訪問.

  • rememberMe() :如果用戶是通過Remember-me功能認證的,就允許訪問

  • fullyAuthenticated():如果用戶是完整認證的話(不是通過Remember-me功能認證的),就允許訪問

  • hasAuthority(String):如果用戶具備給定權限的話就允許訪問

  • hasAnyAuthority(String…) :如果用戶具備給定權限中的某一個的話,就允許訪問

  • hasIpAddress(String) :如果請求來自給定ip地址的話,就允許訪問.

  • not() :對其他訪問結果求反

說明:hasAnyAuthority(“ROLE_ADMIN”) 和 hasRole(“ADMIN”) 的區別就是,後者會自動使用 它會自動使用 “ROLE_” 前綴

2、我們來定製一下用戶授權

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
        	.antMatchers("/").permitAll()
        	.antMatchers("/levelA/**").hasRole("vip1")
        	.antMatchers("/levelB/**").hasRole("vip2")
        	.antMatchers("/levelC/**").hasRole("vip3")
        	.and().formLogin();
}

我們上面代碼的意思就是,當訪問 /levelA/ /levelB/ /levelC/ 這三個路徑下面的任意文件(這裡有 a/b/c.html)都需要認證,身份分別是對應 vip1、vip2、vip3,而其他頁面,就可以隨便訪問了

很顯然,雖然說規定了授權的內容,也就是哪些權限的用戶,可以訪問哪些資源,但是我們由於並沒有配置用戶的信息(合法的或者非法的),所以自然,前面的登錄頁面,都是會直接報錯的,下面我們來分析一下,如何進行認證

(2) 配置認證內容

A:源碼了解用戶認證方式

剛才的授權部分,我們重寫了 configure(HttpSecurity http) 方法,我們繼續看看重寫方法中,有沒有可能幫助我們驗證身份,進行用戶認證的方法,我們首先來看這個方法 configure(AuthenticationManagerBuilder auth)

先去看一下源碼的註釋(此部分的格式,我稍微修改了一下,方便觀看):

這是其中他局舉的一個例子,其實這個就是我們想要的,看註釋也可以看出來,他就是用來在內存中啟用基於用戶名的身份驗證的

* protected void configure(AuthenticationManagerBuilder auth) {
*  auth
*  // enable in memory based authentication with a user named
*  // &quot;user&quot; and &quot;admin&quot;
*  		.inMemoryAuthentication()
*   		.withUser(&quot;user&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;).and()
*        	.withUser(&quot;admin&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;, &quot;ADMIN&quot;);
* }

照貓畫虎,我們也先這麼做

B:自定製用戶認證

代碼如下:

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("admin")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2", "vip3")
            .and()
            .withUser("ideal-20")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2")
            .and()
            .withUser("jack")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1");
}

我們就是照着例子打的,但是,其中我們又加入了編碼的問題,它要求必須進行編碼,否則會報錯,官方推薦的是bcrypt加密方式,我們這裏就用這種,當然自己用常見的 MD5 等等都是可以的,可以自己寫一個工具類

到這裏,測試一下,實際上就可以按照身份的不同,從而擁有訪問不同路徑資源你的權限了,主要的功能已經實現了,下面補充一些,更加友好的功能,例如登錄註銷按鈕的显示,以及記住密碼等等

(3) 註銷問題

1、註銷配置

當然了,前面因為已經有很多配置了,所以可以通過 .and() 進行連接,例如 .and().xxx,或者像下面給出的,單獨再寫一個 http.xxx

@Override
protected void configure(HttpSecurity http) throws Exception {
   ......
    // 註銷配置
	http.logout().logoutSuccessUrl("/")
}

上面短短一句的代碼, logout() 代表開啟了註銷的配置,logoutSuccessUrl(“/”),代表註銷成功后,返回的頁面,我們令其註銷后回到首頁

前台的頁面中,我已經給出了註銷的按鈕的代碼,當然這不是固定的,不同的 ui 框架,不同的模板引擎都是不一樣的,但是路徑是 /logout

<a class="item" th:href="@{/logout}">
  <i class="address card icon"></i> 註銷
</a>

(4) 根據身份權限显示組件

A:登錄、註銷的显示

還有這樣一種問題,右上角,未登錄的時候,應該显示登陸按鈕,登錄后,應該显示用戶信息,以及註銷等等,這一部分,主要是頁面這邊的問題

显示的條件其實很簡單,就是判斷是否認證了,認證了就取出一些值,沒認證就显示登陸

1、這時,我們就需要引入一個 Thymeleaf 配合 Spring Security 的一個依賴 (當然了如果是別的技術,就不一樣了)

地址如下:

https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

2、導入命名空間

引入這個文件的目的,就是為了在頁面寫權限判斷等相關的內容的時候可以有提示

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

3、修改導航欄邏輯

<!--登錄註銷-->
<div class="right menu">

  <!--如果未登錄-->
  <div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLoginPage}">
      <i class="address card icon"></i> 登錄
    </a>
  </div>

  <!--如果已登錄-->
  <div sec:authorize="isAuthenticated()">
    <a class="item">
      <i class="address card icon"></i>
      用戶名:<span sec:authentication="principal.username"></span>
      <!--角色:<span sec:authentication="principal.authorities"></span>-->
    </a>
  </div>

  <div sec:authorize="isAuthenticated()">
    <a class="item" th:href="@{/logout}">
      <i class="address card icon"></i> 註銷
    </a>
  </div>
</div>

B:組件面板的显示

上面的代碼,解決了導航欄的問題,但是例如我們首頁中,一些板塊,對於不同的用戶的显示也是不同的嗎

正如上面的例子,沒有登錄的用戶,是不能訪問了 /levelA/、 /levelB/、 /levelC/ 下面的任何文件的,只有登錄的用戶,根據權限的大小,才能訪問某一個,或者所有

而我們首頁部分的三個面板就是用來显示這三塊的鏈接,對於沒有足夠身份的人,實際上显示這個面板就已經是多餘了,當然,你可以選擇显示,但是如果想要根據身份显示面板怎麼做呢?

關鍵就是在 div 中添加了這樣一句權限的代碼,沒有這個指定的身份,這個面板就不會显示sec:authorize="hasRole('vip1')"

<div class="column" sec:authorize="hasRole('vip1')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelA/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/b}">L-A-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/c}">L-A-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelB/a}">L-B-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/b}">L-B-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/c}">L-B-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelC/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/b}">L-C-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/c}">L-C-c</a>
    </div>
  </div>
</div>

演示一下:

(5) 記住用戶

如果重啟瀏覽器后,就需要重新登錄,對於一部分用戶來說,他們認為是麻煩的,所以很多網站登錄時都提供記住用戶這種選項

1、一個簡單的配置就可以達到目的,這種情況下,默認的登陸頁面,就會多出一個記住用戶的單選框

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//記住用戶
    http.rememberMe();
}

2、但是如果,登陸頁面是自定義(下面講)的怎麼辦呢?,其實只要修改為如下配置即可,

//定製記住我的參數!
http.rememberMe().rememberMeParameter("remember");

上面的 remember 對應 input 中的 name 屬性值

<input type="checkbox" name="remember"/>
<label>記住密碼</label>

3、它做了哪些事情呢?

可以打開頁面的控制台看一下,實際上配置后,用戶選擇記住密碼后,會自動幫我們增加一個 cookie 叫做 remember-me,過期時間為 14 天,當註銷的時候,這個 cookie 就會被刪除了

(6) 定製登錄頁面

1、配置

自帶的登陸頁面確實,還是比較丑的,版本更低一些的,更是不美觀,如果想要使用自己定製的登陸頁面,可以加入下面的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	// 登陸表單提交請求
    http.formLogin()
	.usernameParameter("username")
	.passwordParameter("password")
	.loginPage("/toLoginPage")
	.loginProcessingUrl("/login")
}
  • .loginPage("/toLoginPage") 就是說,當你訪問一些需要用戶權限認證的頁面時,就會發起這個請求,到你的登錄頁面
  • .loginProcessingUrl("/login") 就是表單中,真正要提交請求的一個路徑
  • 其餘兩個就是關於用戶名和密碼的一個獲取,其值和頁面中用戶名密碼的 name 屬性值一致

2、頁面跳轉

前面我們就提過這個,回顧一下

@RequestMapping("/toLoginPage")
public String toLoginPage() {
    return "views/login";
}

3、自定義登錄頁面的表單提交 action 設置

<form id="login" class="ui fluid form segment" th:action="@{/login}" method="post">
	......
</form>

(7) 關閉csrf

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
	http.csrf().disable();
}

(四) 整合 Spring Security (JDBC)

因為配置內存中的用戶還是相對簡單一些的,所以一些細節也都說了一下,基於上面的基礎,來看一下 如何用 JDBC 實現上面的功能,當然了這部分只能算補充,基本不會這麼用的,下面的整合 MyBatis 才是常用的()

(1) 創建表以及數據

這裏創建了三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(2) 創建實體

我使用了 lombok,不過自己寫 get set 構造方法 也是一樣的

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer uid;
    private String username;
    private String password;
    private String roles;
}

(3) 配置授權內容

這部分沒什麼區別

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //註銷
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
            .and().csrf().disable();
}

(4) 配置認證內容

A:配置數據庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

server:
  port: 8082

B:具體配置

以幾個注意的地方:

  • 查詢語句都是通過 username 查詢

  • usersByUsernameQuery()方法里的參數一定要有一個 true 的查詢結果,所以我直接在查詢語句中寫了一個 true

  • MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候簡單點

  • DataSource dataSource 要在前面注入進來(選擇 sql 的)

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("select username,password,true from user where username = ?")
            .authoritiesByUsernameQuery("select username,roles from user where username = ?")
            .passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence rawPassword) {
                    return MD5Util.MD5EncodeUtf8((String) rawPassword);
                }

                @Override
                public boolean matches(CharSequence rawPassword, String encodedPassword) {
                    return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
                }
            });
}

C:MD5工具類

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

D:修改頁面

到這裏,JDBC 的整合方式就成功了,至於前面的頁面只需要根據我們自己設計的權限進行修改,別的地方和前面內存中的方式是一樣的

<div class="ui stackable three column grid">
  <div class="column" sec:authorize="hasAnyRole('USER','ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelA/a}">L-A-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/b}">L-A-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/c}">L-A-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasAnyRole('ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelB/a}">L-B-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/b}">L-B-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/c}">L-B-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasRole('SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelC/a}">L-C-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/b}">L-C-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/c}">L-C-c</a>
      </div>
    </div>
  </div>
  <!-- <div class="column"></div> -->
</div>

(五) 整合 Spring Security (MyBatis)

因為這部分內容是比較常用的,所以,我盡可能給的完善一些

(1) 添加依賴

像 lombok、commons-lang3 都不是必須的,都是可以使用原生的一些手段替代的,寫到那裡我會提的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

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

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

(2) 創建表

和 JDBC 部分用同樣的表

三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(3) 整合 MyBatis

在進行 Spring Security 的配置前,最好先把 MyBatis 先整合好,這樣等會只考慮 Spring Security 的問題就可以了

說明:這部分我盡可能簡化了,例如連接池就用默認的,如果這部分感覺還是有點問題,可以參考一下我前幾篇,關於整合 MyBatis 的文章

A:配置數據庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: cn.ideal.pojo

server:
  port: 8081

B:配置 Mapper 以及 XML

UserMapper

@Mapper
public interface UserMapper {
    User queryUserByUserName(String username);
}

mapper/UserMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.ideal.mapper.UserMapper">
    <select id="queryUserByUserName" parameterType="String" resultType="cn.ideal.pojo.User">
         select * from user where username = #{username}
    </select>
</mapper>

這裏就不演示測試了,是沒有問題的

(4) 配置授權內容

這部分沒什麼好說的,和前面的都一樣,解釋在內存中配置用戶時已經詳細說過了

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //註銷
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
            .and().csrf().disable();
}

(5) 配置認證內容

A:創建 UserService

創建一個類,實現 UserDetailsService,其實主要就是為了 loadUserByname 方法,在這個類中,我們可以注入 mapper 等等,去查用戶,如果查不到,就還留在這個頁面,如果查到了,做出一定邏輯后(例如判空等等),就會把用戶信息封裝到 Spring Security 自己的的 User類中去,Spring Security 拿前台的數據和它比較,做出操作,例如認證成功或者錯誤

注意:

  • StringUtils 是 commons.lang3 下的,使用需要導包,我們用了一個判空功能,不想用的話,用原生的是一個道理,這不是重點
  • 注意區分自己的 User 和 Spring Security 的 User
@Service
public class UserService<T extends User> implements UserDetailsService{

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.queryUserByUserName(username);
        if (username == null){
            throw  new UsernameNotFoundException("用戶名不存在");
        }

        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        String role = user.getRoles();
        if (StringUtils.isNotBlank(role)){
            authorityList.add(new SimpleGrantedAuthority(role.trim()));
        }
        return new org.springframework.security.core.userdetails
            .User(user.getUsername(),user.getPassword(),authorityList);
    }
}

B:修改配置類

這裏也很熟悉,我們調用就可以調用 userDetailsService 了,同樣還需要指定編碼相關的內容 實例化 PasswordEncoder,就需要重寫 encode、 matches

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
        @Override
        public String encode(CharSequence rawPassword) {
            return MD5Util.MD5EncodeUtf8((String) rawPassword);
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
        }
    });
}

C:MD5 工具類補充

其實上面已經給出了,但是怕大家看起來不方便,這裏再貼一下

MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候可以簡單點

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

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

【【其他文章推薦】

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

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

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

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

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

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

分類
發燒車訊

最新的一波Vue實戰技巧,不用則已,一用驚人

Vue中,不同的選項有不同的合併策略,比如 data,props,methods是同名屬性覆蓋合併,其他直接合併,而生命周期鈎子函數則是將同名的函數放到一個數組中,在調用的時候依次調用

Vue中,提供了一個api, Vue.config.optionMergeStrategies,可以通過這個api去自定義選項的合併策略。

在代碼中打印

console.log(Vue.config.optionMergeStrategies)

  

 通過合併策略自定義生命周期函數

背景

最近客戶給領導反饋,我們的系統用一段時間,瀏覽器就變得有點卡,不知道為什麼。問題出來了,本來想甩鍋到後端,但是瀏覽器問題,沒法甩鍋啊,那就排查吧。

後來發現頁面有許多定時器,ajax輪詢還有動畫,打開一個瀏覽器頁簽沒法問題,打開多了,瀏覽器就變得卡了,這時候我就想如果能在用戶切換頁簽時候將這些都停掉,不久解決了。百度裏面上下檢索,找到了一個事件visibilitychange,可以用來判斷瀏覽器頁簽是否显示。

有方法了,就寫唄

export default {
  created() {
    window.addEventListener('visibilitychange', this.$_hanldeVisiblityChange)
    // 此處用了hookEvent,可以參考小編前一篇文章
    this.$on('hook:beforeDestroy', () => {
      window.removeEventListener(
        'visibilitychange',
        this.$_hanldeVisiblityChange
      )
    })
  },
  methods: {
    $_hanldeVisiblityChange() {
      if (document.visibilityState === 'hidden') {
        // 停掉那一堆東西
      }
      if (document.visibilityState === 'visible') {
        // 開啟那一堆東西
      }
    }
  }
}

通過上面的代碼,可以看到在每一個需要監聽處理的文件都要寫一堆事件監聽,判斷頁面是否显示的代碼,一處兩處還可以,文件多了就頭疼了,這時候小編突發奇想,定義一個頁面显示隱藏的生命周期鈎子,把這些判斷都封裝起來

自定義生命周期鈎子函數

定義生命周期函數 pageHidden 與 pageVisible

import Vue from 'vue'

// 通知所有組件頁面狀態發生了變化
const notifyVisibilityChange = (lifeCycleName, vm) => {
  // 生命周期函數會存在$options中,通過$options[lifeCycleName]獲取生命周期
  const lifeCycles = vm.$options[lifeCycleName]
  // 因為使用了created的合併策略,所以是一個數組
  if (lifeCycles && lifeCycles.length) {
    // 遍歷 lifeCycleName對應的生命周期函數列表,依次執行
    lifeCycles.forEach(lifecycle => {
      lifecycle.call(vm)
    })
  }
  // 遍歷所有的子組件,然後依次遞歸執行
  if (vm.$children && vm.$children.length) {
    vm.$children.forEach(child => {
      notifyVisibilityChange(lifeCycleName, child)
    })
  }
}

// 添加生命周期函數
export function init() {
  const optionMergeStrategies = Vue.config.optionMergeStrategies
  // 定義了兩個生命周期函數 pageVisible, pageHidden
  // 為什麼要賦值為 optionMergeStrategies.created呢
  // 這個相當於指定 pageVisible, pageHidden 的合併策略與 created的相同(其他生命周期函數都一樣)
  optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate
  optionMergeStrategies.pageHidden = optionMergeStrategies.created
}


// 將事件變化綁定到根節點上面
// rootVm vue根節點實例
export function bind(rootVm) {
  window.addEventListener('visibilitychange', () => {
    // 判斷調用哪個生命周期函數
    let lifeCycleName = undefined
    if (document.visibilityState === 'hidden') {
      lifeCycleName = 'pageHidden'
    } else if (document.visibilityState === 'visible') {
      lifeCycleName = 'pageVisible'
    }
    if (lifeCycleName) {
      // 通過所有組件生命周期發生變化了
      notifyVisibilityChange(lifeCycleName, rootVm)
    }
  })
}

應用

  1. main.js主入口文件引入
import { init, bind } from './utils/custom-life-cycle'

// 初始化生命周期函數, 必須在Vue實例化之前確定合併策略
init()

const vm = new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

// 將rootVm 綁定到生命周期函數監聽裏面
bind(vm)

  2. 在需要的地方監聽生命周期函數

export default {
  pageVisible() {
    console.log('頁面显示出來了')
  },
  pageHidden() {
    console.log('頁面隱藏了')
  }
}

  

provideinject,不止父子傳值,祖宗傳值也可以

Vue相關的面試經常會被面試官問道,Vue父子之間傳值的方式有哪些,通常我們會回答,props傳值,$emit事件傳值,vuex傳值,還有eventbus傳值等等,今天再加一種provideinject傳值,離offer又近了一步。(對了,下一節還有一種)

使用過React的同學都知道,在React中有一個上下文Context,組件可以通過Context向任意後代傳值,而Vueprovideinject的作用於Context的作用基本一樣

先舉一個例子

使用過elemment-ui的同學一定對下面的代碼感到熟悉

<template>
  <el-form :model="formData" size="small">
    <el-form-item label="姓名" prop="name">
      <el-input v-model="formData.name" />
    </el-form-item>
    <el-form-item label="年齡" prop="age">
      <el-input-number v-model="formData.age" />
    </el-form-item>
    <el-button>提交</el-button>
  </el-form>
</template>
<script>
export default {
  data() {
    return {
      formData: {
        name: '',
        age: 0
      }
    }
  }
}
</script>

  

看了上面的代碼,貌似沒啥特殊的,天天寫啊。在el-form上面我們指定了一個屬性size="small",然後有沒有發現表單裏面的所有表單元素以及按鈕的 size都變成了small,這個是怎麼做到的?接下來我們自己手寫一個表單模擬一下

自己手寫一個表單

自定義表單custom-form.vue

<template>
  <form class="custom-form">
    <slot></slot>
  </form>
</template>
<script>
export default {
  props: {
    // 控製表單元素的大小
    size: {
      type: String,
      default: 'default',
      // size 只能是下面的四個值
      validator(value) {
        return ['default', 'large', 'small', 'mini'].includes(value)
      }
    },
    // 控製表單元素的禁用狀態
    disabled: {
      type: Boolean,
      default: false
    }
  },
  // 通過provide將當前表單實例傳遞到所有後代組件中
  provide() {
    return {
      customForm: this
    }
  }
}
</script>

  

在上面代碼中,我們通過provide將當前組件的實例傳遞到後代組件中,provide是一個函數,函數返回的是一個對象

自定義表單項custom-form-item.vue

沒有什麼特殊的,只是加了一個label,element-ui更複雜一些

<template>
  <div class="custom-form-item">
    <label class="custom-form-item__label">{{ label }}</label>
    <div class="custom-form-item__content">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    label: {
      type: String,
      default: ''
    }
  }
}
</script>

自定義輸入框 custom-input.vue

<template>
  <div
    class="custom-input"
    :class="[
      `custom-input--${getSize}`,
      getDisabled && `custom-input--disabled`
    ]"
  >
    <input class="custom-input__input" :value="value" @input="$_handleChange" />
  </div>
</template>
<script>
/* eslint-disable vue/require-default-prop */
export default {
  props: {
    // 這裏用了自定義v-model
    value: {
      type: String,
      default: ''
    },
    size: {
      type: String
    },
    disabled: {
      type: Boolean
    }
  },
  // 通過inject 將form組件注入的實例添加進來
  inject: ['customForm'],
  computed: {
    // 通過計算組件獲取組件的size, 如果當前組件傳入,則使用當前組件的,否則是否form組件的
    getSize() {
      return this.size || this.customForm.size
    },
    // 組件是否禁用
    getDisabled() {
      const { disabled } = this
      if (disabled !== undefined) {
        return disabled
      }
      return this.customForm.disabled
    }
  },
  methods: {
    // 自定義v-model
    $_handleChange(e) {
      this.$emit('input', e.target.value)
    }
  }
}
</script>

  


form中,我們通過
provide返回了一個對象,在
input中,我們可以通過
inject獲取
form中返回對象中的項,如上代碼
inject:['customForm']所示,然後就可以在組件內通過
this.customForm調用
form實例上面的屬性與方法了

在項目中使用

<template>
  <custom-form size="small">
    <custom-form-item label="姓名">
      <custom-input v-model="formData.name" />
    </custom-form-item>
  </custom-form>
</template>
<script>
import CustomForm from '../components/custom-form'
import CustomFormItem from '../components/custom-form-item'
import CustomInput from '../components/custom-input'
export default {
  components: {
    CustomForm,
    CustomFormItem,
    CustomInput
  },
  data() {
    return {
      formData: {
        name: '',
        age: 0
      }
    }
  }
}
</script>

  執行上面代碼,運行結果為:

<form class="custom-form">
  <div class="custom-form-item">
    <label class="custom-form-item__label">姓名</label>
    <div class="custom-form-item__content">
      <!--size=small已經添加到指定的位置了-->
      <div class="custom-input custom-input--small">
        <input class="custom-input__input">
      </div>
    </div>
  </div>
</form>

  

通過上面的代碼可以看到,input組件已經設置組件樣式為custom-input--small

inject格式說明

除了上面代碼中所使用的inject:['customForm']寫法之外,inject還可以是一個對象。且可以指定默認值

修改上例,如果custom-input外部沒有custom-form,則不會注入customForm,此時為customForm指定默認值

{
  inject: {
    customForm: {
      // 對於非原始值,和props一樣,需要提供一個工廠方法
      default: () => ({
        size: 'default'
      })
    }
  }
}

  

使用限制

1.provideinject的綁定不是可響應式的。但是,如果你傳入的是一個可監聽的對象,如上面的customForm: this,那麼其對象的屬性還是可響應的。

2.Vue官網建議provideinject 主要在開發高階插件/組件庫時使用。不推薦用於普通應用程序代碼中。因為provideinject在代碼中是不可追溯的(ctrl + f可以搜),建議可以使用Vuex代替。 但是,也不是說不能用,在局部功能有時候用了作用還是比較大的。

 

插槽,我要鑽到你的懷裡

插槽,相信每一位Vue都有使用過,但是如何更好的去理解插槽,如何去自定義插槽,今天小編為你帶來更形象的說明。

默認插槽

<template>
  <!--這是一個一居室-->
  <div class="one-bedroom">
    <!--添加一個默認插槽,用戶可以在外部隨意定義這個一居室的內容-->
    <slot></slot>
  </div>
</template>

  

<template>
  <!--這裏一居室-->
  <one-bedroom>
    <!--將傢具放到房間裏面,組件內部就是上面提供的默認插槽的空間-->
    <span>先放一個小床,反正沒有女朋友</span>
    <span>再放一個電腦桌,在家還要加班寫bug</span>
  </one-bedroom>
</template>
<script>
import OneBedroom from '../components/one-bedroom'
export default {
  components: {
    OneBedroom
  }
}
</script>

具名插槽

<template>
  <div class="two-bedroom">
    <!--這是主卧-->
    <div class="master-bedroom">
      <!---主卧使用默認插槽-->
      <slot></slot>
    </div>
    <!--這是次卧-->
    <div class="secondary-bedroom">
      <!--次卧使用具名插槽-->
      <slot name="secondard"></slot>
    </div>
  </div>
</template>

  

<template>
  <two-bedroom>
    <!--主卧使用默認插槽-->
    <div>
      <span>放一個大床,要結婚了,嘿嘿嘿</span>
      <span>放一個衣櫃,老婆的衣服太多了</span>
      <span>算了,還是放一個電腦桌吧,還要寫bug</span>
    </div>
    <!--次卧,通過v-slot:secondard 可以指定使用哪一個具名插槽, v-slot:secondard 也可以簡寫為 #secondard-->
    <template v-slot:secondard>
      <div>
        <span>父母要住,放一個硬一點的床,軟床對腰不好</span>
        <span>放一個衣櫃</span>
      </div>
    </template>
  </two-bedroom>
</template>
<script>
import TwoBedroom from '../components/slot/two-bedroom'
export default {
  components: {
    TwoBedroom
  }
}
</script>

作用域插槽

<template>
  <div class="two-bedroom">
    <!--其他內容省略-->
    <div class="toilet">
      <!--通過v-bind 可以向外傳遞參數, 告訴外面衛生間可以放洗衣機-->
      <slot name="toilet" v-bind="{ washer: true }"></slot>
    </div>
  </div>
</template>

  

<template>
  <two-bedroom>
    <!--其他省略-->
    <!--衛生間插槽,通過v-slot="scope"可以獲取組件內部通過v-bind傳的值-->
    <template v-slot:toilet="scope">
      <!--判斷是否可以放洗衣機-->
      <span v-if="scope.washer">這裏放洗衣機</span>
    </template>
  </two-bedroom>
</template>  

插槽默認值

<template>
  <div class="second-hand-house">
    <div class="master-bedroom">
      <!--插槽可以指定默認值,如果外部調用組件時沒有修改插槽內容,則使用默認插槽-->
      <slot>
        <span>這裡有一張水床,玩的夠嗨</span>
        <span>還有一個衣櫃,有點舊了</span>
      </slot>
    </div>
    <!--這是次卧-->
    <div class="secondary-bedroom">
      <!--次卧使用具名插槽-->
      <slot name="secondard">
        <span>這裡有一張嬰兒床</span>
      </slot>
    </div>
  </div>
</template>

  

<second-hand-house>
    <!--主卧使用默認插槽,只裝修主卧-->
    <div>
      <span>放一個大床,要結婚了,嘿嘿嘿</span>
      <span>放一個衣櫃,老婆的衣服太多了</span>
      <span>算了,還是放一個電腦桌吧,還要寫bug</span>
    </div>
  </second-hand-house>

dispatchbroadcast,這是一種有歷史的組件通信方式

dispatch
broadcast是一種有歷史的組件通信方式,為什麼是有歷史的,因為他們是
Vue1.0提供的一種方式,在
Vue2.0中廢棄了。但是廢棄了不代表我們不能自己手動實現,像許多UI庫內部都有實現。本文以
element-ui實現為基礎進行介紹。同時看完本節,你會對組件的
$parent,
$children,
$options有所了解。

方法介紹

$dispatch: $dispatch會向上觸發一個事件,同時傳遞要觸發的祖先組件的名稱與參數,當事件向上傳遞到對應的組件上時會觸發組件上的事件偵聽器,同時傳播會停止。

$broadcast: $broadcast會向所有的後代組件傳播一個事件,同時傳遞要觸發的後代組件的名稱與參數,當事件傳遞到對應的後代組件時,會觸發組件上的事件偵聽器,同時傳播會停止(因為向下傳遞是樹形的,所以只會停止其中一個恭弘=叶 恭弘子分支的傳遞)。

$dispatch實現與應用

1. 代碼實現

 // 向上傳播事件
 // @param {*} eventName 事件名稱
 // @param {*} componentName 接收事件的組件名稱
 // @param {...any} params 傳遞的參數,可以有多個
 
function dispatch(eventName, componentName, ...params) {
  // 如果沒有$parent, 則取$root
  let parent = this.$parent || this.$root
  while (parent) {
    // 組件的name存儲在組件的$options.componentName 上面
    const name = parent.$options.name
    // 如果接收事件的組件是當前組件
    if (name === componentName) {
      // 通過當前組件上面的$emit觸發事件,同事傳遞事件名稱與參數
      parent.$emit.apply(parent, [eventName, ...params])
      break
    } else {
      // 否則繼續向上判斷
      parent = parent.$parent
    }
  }
}

// 導出一個對象,然後在需要用到的地方通過混入添加
export default {
  methods: {
    $dispatch: dispatch
  }
}  

2. 代碼應用

  • 在子組件中通過$dispatch向上觸發事件

    import emitter from '../mixins/emitter'
    export default {
      name: 'Chart',
      // 通過混入將$dispatch加入進來
      mixins: [emitter],
       mounted() {
         // 在組件渲染完之後,將組件通過$dispatch將自己註冊到Board組件上
        this.$dispatch('register', 'Board', this)
      }
    }
  • Board組件上通過$on監聽要註冊的事件

$broadcast實現與應用

1. 代碼實現

  //向下傳播事件
  // @param {*} eventName 事件名稱
  // @param {*} componentName 要觸發組件的名稱
  // @param  {...any} params 傳遞的參數
 
function broadcast(eventName, componentName, ...params) {
  this.$children.forEach(child => {
    const name = child.$options.name
    if (name === componentName) {
      child.$emit.apply(child, [eventName, ...params])
    } else {
      broadcast.apply(child, [eventName, componentName, ...params])
    }
  })
}

// 導出一個對象,然後在需要用到的地方通過混入添加
export default {
  methods: {
    $broadcast: broadcast
  }
}  

2. 代碼應用

在父組件中通過$broadcast向下觸發事件

import emitter from '../mixins/emitter'
export default {
  name: 'Board',
  // 通過混入將$dispatch加入進來
  mixins: [emitter],
  methods:{
  	//在需要的時候,刷新組件
  	$_refreshChildren(params) {
  		this.$broadcast('refresh', 'Chart', params)
  	}
  }
}

在後代組件中通過$on監聽刷新事件

export default {
  name: 'Chart',
  created() {
    this.$on('refresh',(params) => {
      // 刷新事件
    })
  }
}

總結

通過上面的例子,同學們應該都能對$dispatch$broadcast有所了解,但是為什麼Vue2.0要放棄這兩個方法呢?官方給出的解釋是:”因為基於組件樹結構的事件流方式實在是讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱。這種事件方式確實不太好,我們也不希望在以後讓開發者們太痛苦。並且 $dispatch$broadcast 也沒有解決兄弟組件間的通信問題。“

確實如官網所說,這種事件流的方式確實不容易讓人理解,而且後期維護成本比較高。但是在小編看來,不管黑貓白貓,能抓老鼠的都是好貓,在許多特定的業務場景中,因為業務的複雜性,很有可能使用到這樣的通信方式。但是使用歸使用,但是不能濫用,小編一直就在項目中有使用。

 

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

分類
發燒車訊

Java 反射簡介

本文部分內容參考博客。點擊鏈接可以查看原文。

1. 反射的概念

反射是指在運行時將類的屬性、構造函數和方法等元素動態地映射成一個個對象。通過這些對象我們可以動態地生成對象實例,調用類的方法和更改類的屬性值。

2. 使用場景

什麼情況下運用JAVA反射呢?如果編譯時根本無法預知對象和類可能屬於哪些類,程序只依靠運行時信息來發現該對象和類的真實信息,此時就必須使用反射。

使用反射可以實現下面的功能:

  • 在運行時判斷任意一個對象所屬的類
  • 在運行時構造任意一個類的對象
  • 在運行時判斷任意一個類所具有的方法和屬性
  • 在運行時調用任意一個對象的方法
  • 生成動態代理

3. 獲得Class對象的幾種方式

前面已經介紹過了,每個類被加載之後,系統就會為該類生成一個對應的Class對象,通過該Class對象就可以訪問到JVM中的這個類。在Java程序中獲得Class對象通常有如下3種方式。

  • 使用Class類的forName(String clazzName)靜態方法。該方法需要傳入字符串參數,該字符串參數的值是某個類的全限定類名(必須添加完整包名)。
  • 調用某個類的class屬性來獲取該類對應的Class對象。例如,Person.class將會返回Person類對應的Class對象。
  • 調用某個對象的getClass()方法。該方法是java.lang.Object類中的一個方法,所以所有的Java對象都可以調用該方法,該方法將會返回該對象所屬類對應的Class對象。

對於第一種方式和第二種方式都是直接根據類來取得該類的Class對象,相比之下,第二種方式有如下兩種優勢。

  • 代碼更安全。程序在編譯階段就可以檢查需要訪問的Class對象是否存在。
  • 程序性能更好。因為這種方式無須調用方法,所以性能更好。

也就是說,大部分時候我們都應該使用第二種方式來獲取指定類的Class對象。但如果我們只有一個字符串,例如“java.lang.String”,若需要獲取該字符串對應的Class對象,則只能使用第一種方式,使用Class的forName(String clazzName)方法獲取Class對象時,該方法可能拋出一個ClassNotFoundException異常。一旦獲得了某個類所對應的Class對象之後,程序就可以調用Class對象的方法來獲得該對象和該類的真實信息了。

4. Class類 API介紹

通過class類我們能夠獲取大量的信息:

  1. 獲取構造函數
  • Connstructor getConstructor(Class<?>… parameterTypes):返回此Class對象對應類的指定public構造器。
  • Constructor<?>[] getConstructors():返回此Class對象對應類的所有public構造器。
  • Constructor getDeclaredConstructor(Class<?>… parameterTypes):返回此Class對象對應類的指定構造器,與構造器的訪問權限無關。
  • Constructor<?>[] getDeclaredConstructors():返回此Class對象對應類的所有構造器,與構造器的訪問權限無關。
  1. 獲取方法
  • Method getDeclaredMethod(String name, Class<?>… parameterTypes):返回此Class對象對應類的指定方法,與方法的訪問權限無關。
  • Method[] getDeclaredMethods():返回此Class對象對應類的全部方法,與方法的訪問權限無關。
  1. 獲取屬性
  • Field getField(String name):返回此Class對象對應類的指定public Field。
  • Field[] getFields():返回此Class對象對應類的所有public Field。
  • Field getDeclaredField(String name):返回此Class對象對應類的指定Field,與Field的訪問權限無關。
  • Field[] getDeclaredFields():返回此Class對象對應類的全部Field,與Field的訪問權限無關。
  1. 獲取Class對應類上所包含的Annotation。
  • A getAnnotation(Class annotationClass):試圖獲取該Class對象對應類上指定類型的Annotation;如果該類型的註釋不存在,則返回null。
  • Annotation[] getAnnotations():返回該Class對象對應類上的所有Annotation。
  • Annotation[] getDeclaredAnnotations():返回直接修飾該Class對應類的所有Annotation。
  1. 獲取Class對象對應類包含的內部類。
  • Class<?>[] getDeclaredClasses():返回該Class對象對應類里包含的全部內部類。
    如下方法用於訪問該Class對象對應類所在的外部類。
  • Class<?> getDeclaringClass():返回該Class對象對應類所在的外部類。
    如下方法用於訪問該Class對象對應類所繼承的父類、所實現的接口等。
  • Class<?>[] getInterfaces():返回該Class對象對應類所實現的全部接口。
  1. 獲取Class對象對應類所繼承的父類
  • Class<? super T> getSuperclass():返回該Class對象對應類的超類的Class對象。
  1. 獲取Class對象對應類的修飾符、所在包、類名等基本信息。
  • int getModifiers():返回此類或接口的所有修飾符。修飾符由public、protected、private、final、static、abstract等對應的常量組成,返回的整數應使用Modifier工具類的方法來解碼,才可以獲取真實的修飾符。
  • Package getPackage():獲取此類的包。
  • String getName():以字符串形式返回此Class對象所表示的類的名稱。
  • String getSimpleName():以字符串形式返回此Class對象所表示的類的簡稱。
  1. 判斷該類是否為接口、枚舉、註釋類型等
  • boolean isAnnotation():返回此Class對象是否表示一個註釋類型(由@interface定義)。
  • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判斷此Class對象是否使用了Annotation註釋修飾。
  • boolean isAnonymousClass():返回此Class對象是否是一個匿名類。
  • boolean isArray():返回此Class對象是否表示一個數組類。
  • boolean isEnum():返回此Class對象是否表示一個枚舉(由enum關鍵字定義)。
  • boolean isInterface():返回此Class對象是否表示一個接口(使用interface定義)。
  • boolean isInstance(Object obj):判斷obj是否是此Class對象的實例,該方法可以完全代替instanceof操作符。

上面的多個getMethod()方法和getConstructor()方法中,都需要傳入多個類型為Class<?>的參數,用於獲取指定的方法或指定的構造器。關於這個參數的作用,假設某個類內包含如下3個info方法簽名:

  • public void info()
  • public void info(String str)
  • public void info(String str , Integer num)

這3個同名方法屬於重載,它們的方法名相同,但參數列表不同。在Java語言中要確定一個方法光有方法名是不行的,例如,我們指定info方法——實際上可以是上面3個方法中的任意一個!如果需要確定一個方法,則應該由方法名和形參列表來確定,但形參名沒有任何實際意義,所以只能由形參類型來確定。例如,我們想要確定第二個info方法,則必須指定方法名為info,形參列表為String.class——因此在程序中獲取該方法使用如下代碼:

clazz.getMethod("info",String.class);

使用反射生成對象

  1. 利用構造函數生成對象
  • 使用Class對象的newInstance()方法來創建該Class對象對應類的實例,這種方式要求該Class對象的對應類有默認構造器,而執行newInstance()方法時實際上是利用默認構造器來創建該類的實例。
  • 先使用Class對象獲取指定的Constructor對象,再調用Constructor對象的newInstance()方法來創建該Class對象對應類的實例。通過這種方式可以選擇使用指定的構造器來創建實例。
Constructor c = clazz.getConstructor(String.class);
c.newInstance("xx");

調用方法

Method setProName = aClass.getDeclaredMethod("setProName",String.class);
setProName.setAccessible(true);
etProName.invoke(product,"我是一個產品");

操作屬性

Field[] declaredFields = aClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println("fieldName:"+declaredField.getName()+" filedType:"+declaredField.getType());
        }
        Field proName = aClass.getDeclaredField("proName");
        proName.setAccessible(true);
        proName.set(product,"我是一個產品");
        System.out.println("修稿屬性:"+product);

操作數組

//使用反射動態地創建數組
//創建一個元素類型為String,長度為3的數組
Object arr = Array.newInstance(String.class, 3);
//依次為arr數組中index為0,1,2的元素賦值
Array.set(arr, 0, "榮耀盒子");
Array.set(arr, 1, "榮耀8手機");
Array.set(arr, 2, "華為mate9保時捷版");
Object o1= Array.get(arr, 0);
Object o2= Array.get(arr, 1);
Object o3= Array.get(arr, 2);
System.out.println(o1);
System.out.println(o2);
System.out.println(o3);

5. 使用Demo

public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        Class<Product> aClass = Product.class;
        Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println(declaredConstructor.getName());
        }
        Constructor<Product> constructor = aClass.getConstructor(int.class, String.class);
        Product product = constructor.newInstance(10, "ds");
        System.out.println("創建對象:"+product);

        //獲取方法並調用
        Method setProName = aClass.getDeclaredMethod("setProName", String.class);
        setProName.setAccessible(true);
        setProName.invoke(product,"我是一個產品");
        System.out.println("調用方法:"+product);

        //獲取屬性,並設置屬性的值
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println("fieldName:"+declaredField.getName()+" filedType:"+declaredField.getType());
        }
        Field proName = aClass.getDeclaredField("proName");
        proName.setAccessible(true);
        proName.set(product,"我是一個產品");
        System.out.println("修稿屬性:"+product);

        //使用反射動態地創建數組
        //創建一個元素類型為String,長度為3的數組
        Object arr = Array.newInstance(String.class, 3);
        //依次為arr數組中index為0,1,2的元素賦值
        Array.set(arr, 0, "榮耀盒子");
        Array.set(arr, 1, "榮耀8手機");
        Array.set(arr, 2, "華為mate9保時捷版");
        Object o1= Array.get(arr, 0);
        Object o2= Array.get(arr, 1);
        Object o3= Array.get(arr, 2);
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o3);
        
        System.out.println("end...");
    }
}

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

分類
發燒車訊

可拖拽圓形進度條組件(支持移動端)

好久之前寫過一個可拖拽圓形進度條的dome,中間有網友反饋過一些問題,最近比較閑有時間修改了一些問題也做了一些優化,並封裝成組件,基於canvas實現,只需傳入放置組件dom容器,任何框架均可直接使用;

codepen 示例如下:https://codepen.io/pangyongsheng/pen/XRmNRK

 

一、如何使用

npm下載

執行 npm i drag-arc -S 或 cnpm i drag-arc -S

 
import DragArc from 'drag-arc';
 new DragArc({
    el: dom,
    value: 10,
    change: (v) => {
        console.log(v)
    },
    ...
})
或者 也可從項目下載dist/dist/drag-arc.min.js,直接通過srcipt標籤引入

其中dom為放置組件HTML容器,可通過ref獲取;

主要屬性方法(詳見github/npm)

項目地址:https://github.com/pangyongsheng/canvas-arc-draw
npm地址:https://www.npmjs.com/package/drag-arc

Name Description Type Default Required
el 放置組件的DOM元素 Element none Y
change 當前值變化時觸發的事件,回調參數為當前進度值Number(0-100) Function ()=>{} N
startDeg 滑動圓弧的起始弧度 Number  0 N
endDeg 滑動圓弧的結束弧度 Number 1 N
value 默認值 Number (0-100) 0 N
textShow 显示文字 Boolean true N
color 外側圓弧顏色 String,Array [“#06dabc”, “#33aaff”] N
slider 滑塊半徑 Number #FFF N
innerColor 內側弧度的顏色 String #ccc N
outColor 外側圓弧背景顏色 String,Array #ccc N
innerLineWidth 內側弧線寬 Number 1 N
outLineWidth 外側弧線寬 Number 20 N
counterclockwise 逆時針方向 Boolean true N
sliderColor 滑塊顏色 String #CCC N
sliderBorderColor 滑塊邊框顏色 String #fff N

二、實現方法簡介

1、繪製位置幾何關係

如圖所示,以canvas畫布中心點建立坐標系,則有:

滑塊位置與弧度關係:

由圓的參數方程得出
x=rcosφ
y=rsinφ

鼠標移動位置與弧度關係:

通過事件回調參數 我們可以獲得 鼠標mousemove事件或者移動端touchmove事件的x,y坐標,可計算tan值為
tanφ = y/x;
再通過反三角函數有可得:
φ=arctan(tanφ)

以上基本的位置關係已經得出;

2、js實現中的幾個問題

(1)坐標的轉化方法

由於上述位置關係是基於中心坐標實現的,而canvas繪製坐標是以左上角為原點實現的,故需要實現兩種坐標的轉化關係;

(2)canvas弧度位置與正常弧度位置的轉化

下圖是canvas的弧度位置恰好與我們正常計算的方向是相反的,同樣需考慮弧度的轉換;

(3)Math.atan方法返回值與實際弧度的關係

由於Math.atan() 函數返回一個數值的反正切[- π/2 , π/2 ],
而實際中我們需要獲得到[0-2π]直接的值,所以在通過鼠標位置獲取弧度值時需要通過Math.atan(y/x)和xy在中心坐標的正負綜合判斷其所在象限從何獲取實際的獲取弧度值;

(4)弧度與進度條值得關係

由於鼠標移動觸發繪圖方法是較為連續的動畫效果,而進度是間隔的,
這裏我們需要實現個類似d3js中domain和range的比例關係。
這裏我們將值[0,100]對應弧度比例為[startDeg, endDeg]

(5)終點的判斷

由於鼠標移動的位置是任意的,可能導致滑塊到達終點後由於鼠標移動到了起點時,滑塊也直接從終點移動到起點,故需對起點終點做判斷,到達起點后不可再向後滑動,到達終點后不可再向前滑動;

3、詳細實現方法可以參考這篇文章

 https://www.cnblogs.com/pangys/p/6837344.html

 

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

【其他文章推薦】

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

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

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

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

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