切換語言為:簡體

常用第三方類庫總結

  • 爱糖宝
  • 2024-07-09
  • 2058
  • 0
  • 0

1. 概述

在專案開發過程中,一般我們都會用到很多類庫,比如 spring,guava,hutool,apache io/commons等等等等相關一堆類庫。本文就來介紹一下常見的類庫以及使用方式,以作備忘和學習。

1.1 為什麼學習類庫?

1.1.1 提高效率

這是使用類庫最重要的原因!

使用別人寫好的類庫可以很大程度上提高開發效率,在Java開發中我們自己寫的程式碼(這裏指的是偏工具型別的程式碼)是很少的,更多的程式碼是由各種類庫來提供的,否則重複造輪子,開發週期會非常的長。

1.1.2 提高安全性

使得程式碼安全性更高

各種的經典類庫被非常多的公司引用,並且執行了無數次,很少出現bug,但是我們自己實現這些功能浪費時間不說,並且還容易出現一些意想不到的bug,說不定什麼時候就暴雷了,而經典的類庫都是經過了時間的考驗相對來說更穩定一些。

1.1.3 學習設計思想

可以學習好的設計思想

一般比較出名的、使用廣泛的類庫,都是由比較出名的組織或者大佬編寫的,其中很多考慮到了擴充套件,健壯,安全,穩定,易用性等等這些,可以從中學習到一些思想,從而給自己助力&提升。


下邊我們介紹一下guava 這個類庫,guava這個類庫很優秀,非常值的我們去使用&學習。

2. Guava

常用第三方類庫總結

2.1 概述

Guava是一個基於Java的開源庫,包含許多Google核心庫,這些庫在許多專案中都有使用

使用他有助於我們去學習最佳編碼方式,並且幫助我們減少編碼錯誤, 它為集合,快取,併發,通用註釋,字串處理,I/O和驗證等相關程式設計過程中的需求, 提供了大量開箱即用的方法。

2.1.1 Guava的優點
  • 高效設計良好的API,被Google的開發者設計,實現和推廣使用。

  • 遵循高效的java語法實踐。

  • 使程式碼更簡練,簡潔,簡單。

  • 節約時間,資源,提高生產力。

2.1.2 原始碼結構

原始碼包包含了以下這些工具,可以根據需求使用,其中比較經典的有cache,collect,eventbus,concurrent,等等, 具體見下邊:

  • com.google.common.annotations:普通註解型別。

  • com.google.common.base:基本工具類庫和介面。

  • com.google.common.cache:快取工具包,非常簡單易用且功能強大的JVM快取。

  • com.google.common.collect:帶泛型的集合介面擴充套件和實現,以及工具類,這裏你會發現很多好玩的集合。

  • com.google.common.eventbus:釋出訂閱風格的事件匯流排。

  • com.google.common.graph:對“圖”數據結構的支援。

  • com.google.common.hash: 雜湊工具包。

  • com.google.common.io:I/O工具包。

  • com.google.common.math:數學相關工具包。

  • com.google.common.net:網路工具包。

  • com.google.common.primitives:八種原始型別和無符號型別的靜態工具包。

  • com.google.common.reflect:反射工具包。

  • com.google.common.util.concurrent:多執行緒工具包。

  • com.google.common.escape:提供了對字串內容中特殊字元進行替換的框架,幷包括了Xml和Html的兩個實現。

  • com.google.common.html:HtmlEscapers封裝了對html中特殊字元的替換。

  • com.google.common.xml:XmlEscapers封裝了對xml中特殊字元的替換。

2.1.3 引入座標

我們可以在maven的pom中引入最新的座標和版本,然後就可以使用了

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

2.2 基礎工具類

Guava的經典很大一部分原因來源於對於基礎工具類的封裝,使用這些類能夠讓我們的程式碼更加優雅且完善,這些類大部分都在com.google.common.base包下

2.2.1 注意事項

JDK有很多借鑑guava的地方,這裏只講guava,並且JDK在不斷的完善,如果JDK中有已經存這些工具類,建議就不要用guava

2.2.2 Optional(注意這裏說的是Guava 中的 Optional)
2.2.2.1 作用

在構造物件的時候就明確申明該物件是否可能為null,快速失敗拒絕null值,可以避免空指標異常。

2.2.2.2 使用
public class OptionTest {
  public static void main(String[] args) {
    Integer a = null;
    Integer b = 10;
    //支援傳入null以及非null的資料
    Optional<Integer> optional_a = Optional.fromNullable(a);
    Optional<Integer> optional_b = Optional.fromNullable(b);
    //of方式支支援非null的資料
    //Optional<Integer> optional_c = Optional.fromNullable(a);
    //建立一個空的沒有物件引用的Option
    Optional<Integer> optional_d = Optional.absent();
    //不存在物件例項不進入
    if (optional_a.isPresent()) {
      System.out.println("A:" + optional_a.get());
     }
    //存在物件例項進入
    if (optional_b.isPresent()) {
      System.out.println("B:" + optional_b.get());
     }
    //不存在物件例項不進入
    if (optional_d.isPresent()) {
      System.out.println("D:" + optional_d.get());
     }
   }
}

2.2.2.3 原始碼結構

Optional封裝了Absent物件以及Present物件,如果引數為空,則Optional封裝Absent否則封裝Present

常用第三方類庫總結

2.2.2.4 JDK8替代

在JDK8以及更高版本,可以使用java.util.Optional來代替使用

2.2.3 Preconditions
2.2.3.1 作用

封裝了前置條件校驗,讓方法中的條件檢查更簡單

實際開發中需要做入參校驗的情況比比皆是,比如開發一個rest介面,肯定要對引數各種校驗,防止錯誤的輸入導致程式出錯,我們可以使用Preconditions(前置條件),這樣我們自己程式碼中就不會出現大段的if程式碼了

2.2.3.2 以前的做法

以前我們都是大段的用if寫各種判斷,如果入參很多,或者校驗邏輯很複雜,這個函式中if會越來越多,圈複雜度越來越高

  public static void query(String name, int age) {
        if (name == null || name.equals("")) {
            throw new IllegalArgumentException("name should not be empty.");
        }
     
        if (age <= 0||age>=100) {
            throw new IllegalArgumentException("age should not be negative.");
        }
    }

2.2.3.3 程式碼最佳化

使用Preconditions對我們的程式碼進行最佳化

public static void query(String name, int age) {
  Preconditions.checkNotNull(name,"name should not be empty.");
  Preconditions.checkArgument(!"".equals(name),"name should not be empty.");
  Preconditions.checkArgument(age >= 0 && age < 100,"age should not be negative.");
}

使用Preconditions就可以消除程式碼中的if了,當然也可以使用Assert ,後邊我們會講。

2.2.3.4 常見的一些校驗
  • checkArgument: 檢查boolean是否為真,用作方法中檢查引數,失敗時丟擲的異常型別: IllegalArgumentException

  • checkNotNull:檢查value不為null, 直接返回value,失敗時丟擲的異常型別:NullPointerException

  • checkState:檢查物件的一些狀態,不依賴方法引數,失敗時丟擲的異常型別:IllegalStateException

  • checkElementIndex:檢查index是否為在一個長度為size的list, string或array合法的範圍,失敗時丟擲的異常型別:IndexOutOfBoundsException

  • checkPositionIndex:檢查位置index是否為在合法的範圍,index的範圍區間是[0, size]失敗時丟擲的異常型別:IndexOutOfBoundsException

2.2.4 Splitter
2.2.4.1 作用

Splitter 可以讓你使用一種非常簡單流暢的模式來控制字元分割的行為

2.2.4.2 String.split的問題

Java 中關於分詞的工具類會有一些古怪的行為,String.split 函式會悄悄地丟棄尾部的分割符,下邊做個演示:

常用第三方類庫總結

2.2.4.3 Splitter最佳化

而使用 Splitter 可以讓你使用一種非常簡單流暢的模式來解決這些令人困惑的行為

public static void guavaSplit(String str) {
    List<String> strings = Splitter.on(",").
            //omitEmptyStrings().
                    splitToList(str);
    strings.forEach(x -> System.out.print(x + ";"));
}

這樣尾部空格不會被跳過,可以正常顯示尾部空串

常用第三方類庫總結

2.2.4.4 去除空格

Splitter還支援自定義分割字串,比如去掉空格、去掉空字串等等,上面程式碼可以最佳化為

public static void guavaSplit(String str) {
    List<String> strings = Splitter.on(",").
                    //將結果中的空格刪除(如果有的話)
                    trimResults().
                    //移去結果中的空字串
                    omitEmptyStrings().
                    //需要分割的字串
                    splitToList(str);
    strings.forEach(x -> System.out.print(x + ";"));
}

執行後將會去除空格(如果有的話)以及空字串

常用第三方類庫總結

2.2.4.5 MapSplitter

Splitter除了可以對字元進行拆分,還可以對URL引數進行拆分,比如URL引數id=123&name=green

 public static void guavaSplit3(String str) {
            //分割字串,獲取URL`?`後面的引數部分
            String param = str.split("\?")[1];
            Map<String, String> paramMap = Splitter.
                    //先按照`&`符號進行分割
                            on("&").
                    //在分割的符號裡面在按照`=`進行分割
                            withKeyValueSeparator("=").
                    //需要切分的字串
                            split(str);
            System.out.println(paramMap);
}

2.2.5 Joiner
2.2.5.1 作用

Guava 的 Joiner 讓字串連線變得極其簡單,即使字串中包含 null,也很容易處理

Joiner相當於spliter的反操作,可以將陣列或者集合等可遍歷的資料轉換成使用分隔符連線的字串

2.2.5.2 Java實現方式

對於這樣的list進行資料進行拼接,需要排除空字串和null的資料

    List<String> list = new ArrayList<String>() {{
        add("1");
        add("2");
        add(null);
        add("3");
    }};

如果只使用Java方式需要使用以下的方式進行實現

public static String javaJoin(List<String> strList, String delimiter) {
        StringBuilder builder = new StringBuilder();
        for (String str : strList) {
            if (null != str) {
                builder.append(str).append(delimiter);
            }
        }
        builder.setLength(builder.length() - delimiter.length());
        return builder.toString();
}

實現方式很簡單,但是很繁瑣

2.2.5.3 Joiner方式最佳化

我們不在考慮更多的細節,並且很有語義的告訴程式碼的閱讀者,用什麼分隔符,需要過濾null值再join

    public static String guavaJoin1(List<String> strList, String delimiter) {
        return Joiner.on(delimiter).skipNulls().join(strList);
    }

2.2.5.4 MapJoinner

Joiner還可以處理URL編碼的生成,將MAP資料轉換成對應的URL引數

Map<String, String> map = ImmutableMap.of("id", "123", "name", "green");
//第一個分隔符使用&,第二個引數分割符使用 =
String joinStr = Joiner.on("&").withKeyValueSeparator("=").join(map);
System.out.println(joinStr);

常用第三方類庫總結

2.2.6 StopWatch

StopWatch用來計算經過的時間(精確到納秒)

2.2.6.1 原始的計時方式

原始的方式程式碼複雜,並且很不美觀,效能也存在問題

 public static void task() throws InterruptedException {
     long currentTime = System.currentTimeMillis();
     //業務程式碼
     Thread.sleep(1000);
     long duration = System.currentTimeMillis() - currentTime;
     System.out.println("執行耗時:" + duration + "ms");
 }

2.2.6.2 最佳化程式碼

我們發現最佳化後從程式碼上來看優雅很多,並且使用起來也比較簡單

    public static void task() throws InterruptedException {
        
        Stopwatch stopwatch = Stopwatch.createStarted();
        
        //業務程式碼
        Thread.sleep(1000);
        //以毫秒列印從計時開始至現在的所用時間,向下取整
        long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS);
        System.out.println("執行耗時:" + duration + "ms");
        
        //停止計時
        stopwatch.stop();
        
        System.out.println("停止計時:" + duration + "ms");
        
        //重置計時器,並且開始計時
        stopwatch.reset().start();
        
        Thread.sleep(1000);
        System.out.println("是否正在執行:" + stopwatch.isRunning());
        //以秒列印從計時開始至現在的所用時間,向下取整
        long millis = stopwatch.elapsed(TimeUnit.SECONDS);
        
        System.out.println("第二次任務耗時:"+millis+"秒");
        System.out.println(stopwatch.toString());
        
        stopwatch.stop();
    }

2.3 集合增強

2.3.1 Guava集合操作工具類

我們一般習慣使用java.util.Collections包含的工具方法對集合進行建立操作,Guava沿著這些路線提供了更多的工具方法:適用於所有集合的靜態方法,這些功能很強大下面我們來看一看

我們用相對直觀的方式把工具類與特定集合介面的對應關係歸納如下:

集合介面 屬於JDK還是Guava 對應的Guava工具類
Collection JDK Collections2,不要和 java.util.Collections 混淆
List JDK Lists
Set JDK Sets
SortedSet JDK Sets
Map JDK Maps
SortedMap JDK Maps
Queue JDK Queues
Multiset Guava Multisets
Multimap Guava Multimaps
BiMap Guava Maps
Table Guava Tables

 

2.3.2 靜態工廠方法
2.3.2.1 JDK建立集合

在 JDK 7之前,構造新的範型集合時要討厭地重複宣告範型

    List<String> list = new ArrayList<String>();

這種建立方式是比較繁瑣的

2.3.2.2 guava建立集合

因此 Guava 提供了能夠推斷範型的靜態工廠方法,現在JDK7以及以上也支援自動型別推斷了

  List<String> list =Lists.newArrayList();   
  Map<String,String> map = Maps.newHashMap();

2.3.2.3 指定初始值

Guava 的靜態工廠方法遠不止這麼簡單,用工廠方法模式,我們可以方便地在初始化時就指定起始元素

    List<String> list =Lists.newArrayList("張三","李四","王五");

2.3.2.4 指定初始容量

透過為工廠方法命名 ,我們可以提高集合初始化大小的可讀性

    //設定初始容量為100
    List<String> exactly100 = Lists.newArrayListWithCapacity(100);
    //設定初始預期容量,預期容量是 5+size+size/100
    List<String> approx100 = Lists.newArrayListWithExpectedSize(100);
    Set<String> approx100Set = Sets.newHashSetWithExpectedSize(100);

2.3.3 不可變集合
2.3.3.1 不可變集合的意義

不可變物件有很多優點,包括:

  • 當物件被不可信的庫呼叫時,不可變形式是安全的

  • 不可變物件被多個執行緒呼叫時,不存在競態條件問題

  • 不可變集合不需要考慮變化,因此可以節省時間和空間,所有不可變的集合都比它們的可變形式有更好的記憶體利用率(分析和測試細節);

  • 不可變物件因為是固定不變的,故可以作為常量來安全使用。

2.3.3.2 JDK的不可變操作

JDK也提供了Collections.unmodifiableXXX方法把集合包裝為不可變形式,但是有以下缺點

  • 笨重而且累贅:不能舒適地用在所有想做防禦性複製的場景;

  • 不安全:要保證沒人透過原集合的引用進行修改,返回的集合纔是事實上不可變的;

  • 低效:包裝過的集合仍然保有可變集合的開銷,比如併發修改的檢查、雜湊表的額外空間,等等。

2.3.3.3 注意事項

所有Guava不可變集合的實現都不接受null值

如果你需要在不可變集合中使用null,請使用JDK中的Collections.unmodifiableXXX方法

2.3.3.4 建立不可變集合

建造者模式進行建立

public static void immutabList() {
            //建立不可變的List
            List<String> immutableNamedColors = ImmutableList.<String>builder()
                    .add("王五", "李四", "王五", "趙六", "錢七")
                    .build();
            //建立成功後新增刪除會直接報錯
            immutableNamedColors.add("王八");
        }
    }

還可以使用of方式進行快速建立

    ImmutableList.of("王五", "李四", "王五", "趙六", "錢七");

也可以透過copyOf進行建立

    ImmutableList.copyOf(new String[]{"王五", "李四", "王五", "趙六", "錢七"});

2.3.3.5 不可變集合的使用

不可變集合的使用和普通集合一樣,只是不能使用他們的add,remove等修改集合的方法,並且程式碼結構如下

常用第三方類庫總結

其中add以及remove方法執行後會直接報錯

  /** @deprecated */
    @Deprecated
    @CanIgnoreReturnValue
    @DoNotCall("Always throws UnsupportedOperationException")
    public final boolean add(E e) {
        throw new UnsupportedOperationException();
    }


    /** @deprecated */
    @Deprecated
    @CanIgnoreReturnValue
    @DoNotCall("Always throws UnsupportedOperationException")
    public final boolean remove(@CheckForNull Object object) {
        throw new UnsupportedOperationException();
    }

2.3.4 Multiset

Guava提供了一個新集合型別Multiset,它可以多次新增相同的元素,且和元素順序無關,Multiset繼承於JDK的Cllection介面,而不是Set介面

常用第三方類庫總結

2.3.4.1 程式碼結構

Multiset程式碼結構如下

常用第三方類庫總結

2.3.4.2 作用

Multiset和Set的區別就是可以儲存多個相同的物件

在JDK中,List和Set有一個基本的區別,就是List可以包含多個相同物件,且是有順序的,而Set不能有重複,且不保證順序,所以Multiset佔據了List和Set之間的一個灰色地帶:允許重複,但是不保證順序

常見使用場景:Multiset有一個有用的功能,就是跟蹤每種物件的數量,所以你可以用來進行數字統計。下邊做個演示

2.3.4.3 案例場景

由於某些的需求,我們經常會這樣去用Map數據結構,比如對一系列key計數

下面的程式碼實現方式沒有問題,只是程式碼實在是醜陋不堪,尤其是其中的if判斷,程式碼噪音極重

/**
      * 統計字元次數
      */
    public static void testWordCount() {
        Map<String, Integer> countMap = new HashMap<>();
        //遍歷單詞
        for (String word : STR_WORLD_ARRAY) {
            if (!countMap.containsKey(word)) {
                countMap.put(word, 0);
            }
            ////獲取key出現的次數並進行計數
            Integer count = countMap.get(word);
            //進行計數
            countMap.put(word, ++count);
        }
        //列印結果
        System.out.println("--------------單詞出現頻率--------------");
        for (String key : countMap.keySet()) {
            System.out.println(key + " count:" + countMap.get(key));
        }
    }

2.3.4.4 Multiset最佳化程式碼

可以使用guava的一種Multiset的數據結構,專門用於簡化這類問題,如果使用實現Multiset介面的具體類就可以很容易實現以上的功能需求

        /**
         * 統計字元次數
         */
        public static void testWordCount() {
            Multiset<String> wordsMultiset = HashMultiset.create();
            //遍歷單詞
            for (String word : STR_WORLD_ARRAY) {
                //新增字元到Multiset
                wordsMultiset.add(word);
            }
            //列印結果
            System.out.println("--------------單詞出現頻率--------------");
            for (String key : wordsMultiset.elementSet()) {
                System.out.println(key + " count:" + wordsMultiset.count(key));
            }
        }

我們發現程式碼經過最佳化後簡潔多了,操作有原來的四行變成了一行

2.3.4.5 Multiset的實現類

Guava提供了Multiset的多種實現,這些實現基本對應了JDK中Map的實現

常用第三方類庫總結

 

  • HashMultiset: 元素存放於 HashMap

  • LinkedHashMultiset: 元素存放於 LinkedHashMap,即元素的排列順序由第一次放入的順序決定

  • TreeMultiset:元素被排序存放於TreeMap

  • EnumMultiset: 元素必須是 enum 型別

  • ImmutableMultiset: 不可修改的 Mutiset

2.3.4.6 Multiset主要方法
  • add(E element) :向其中新增單個元素

  • add(E element,int occurrences) : 向其中新增指定個數的元素

  • count(Object element) : 返回給定引數元素的個數

  • remove(E element) : 移除一個元素,其count值 會響應減少

  • remove(E element,int occurrences): 移除相應個數的元素

  • elementSet() : 將不同的元素放入一個Set中

  • entrySet(): 類似與Map.entrySet 返回Set<Multiset.Entry>。包含的Entry支援使用getElement()和getCount()

  • setCount(E element ,int count): 設定某一個元素的重複次數

  • setCount(E element,int oldCount,int newCount): 將符合原有重複個數的元素修改爲新的重複次數

  • retainAll(Collection c) : 保留出現在給定集合引數的所有的元素

  • removeAll(Collectionc) : 去除出現給給定集合引數的所有的元素

2.3.4.7 Multiset和Map區別

需要注意的是Multiset不是一個Map<E,Integer>,儘管Multiset提供一部分類似的功能實現

  • Multiset中的元素的重複個數只會是正數,且最大不會超過Integer.MAX_VALUE,設定計數為0的元素將不會出現multiset中,也不會出現elementSet()和entrySet()的返回結果中。

  • multiset.size() 方法返回的是所有的元素的總和,相當於是將所有重複的個數相加,如果需要知道每個元素的個數可以使用elementSet().size()得到.(因而呼叫add(E)方法會是multiset.size()增加1).

  • multiset.iterator() 會迴圈迭代每一個出現的元素,迭代的次數與multiset.size()相同。

  • Multiset 支援新增、移除多個元素以及重新設定元素的個數,執行setCount(element,0)相當於移除multiset中所有的相同元素。

  • 呼叫multiset.count(elem)方法時,如果該元素不在該集中,那麼返回的結果只會是0

2.3.5 雙向Map

我們知道Map是一種鍵值對對映,這個對映是鍵到值的對映,而BiMap首先也是一種Map,他的特別之處在於,既提供鍵到值的對映,也提供值到鍵的對映,所以它是雙向Map

常用第三方類庫總結

2.3.5.1 作用

Java類庫中的Map是一種對映的數據結構,由鍵值對組成一個Map的集合元素,這種對映是單方向的,由鍵(key)到值(value)的對映,開發者可以透過key獲得對應的唯一value的值,但是無法透過value反向獲得與之對應的唯一key的值。

BiMap可以理解為是一種雙向的鍵值對對映,既可以透過key獲取value的值,也可以透過value反向獲取key的值

2.3.5.2 案例場景

我們需要做一個星期幾的中英文表示的相互對映,例如Monday對應的中文表示是星期一,同樣星期一對應的英文表示是Monday如果使用傳統的Map來實現

 public static void main(String[] args) {
        Map<String, String> weekNameMap = new HashMap<>();
        weekNameMap.put("星期一", "Monday");
        weekNameMap.put("星期二", "Tuesday");
        weekNameMap.put("星期三", "Wednesday");
        weekNameMap.put("星期四", "Thursday");
        weekNameMap.put("星期五", "Friday");
        weekNameMap.put("星期六", "Saturday");
        weekNameMap.put("星期日", "Sunday");


        System.out.println("星期日的英文名是" + weekNameMap.get("星期日"));
        //根據value獲取對應的日期
        for (Map.Entry<String, String> entry : weekNameMap.entrySet()) {
            if ("Sunday".equals(entry.getValue())) {
                System.out.println("Sunday的中文名是" + entry.getKey());
                break;
            }
        }
}

透過value獲取key需要遍歷,並且還需要進行判斷,程式碼不簡潔,並且可能存在一些問題

  1. 如何處理重複的value的情況,不考慮的話,反轉的時候就會出現覆蓋的情況.

2.3.5.3 BiMap最佳化程式碼

這裏使用BiMap是一個非常好的場景,讓我們上面的程式碼變得十分簡潔

 public static void main(String[] args) {
            BiMap<String, String> weekNameMap = HashBiMap.create();
            weekNameMap.put("星期一", "Monday");
            weekNameMap.put("星期二", "Tuesday");
            weekNameMap.put("星期三", "Wednesday");
            weekNameMap.put("星期四", "Thursday");
            weekNameMap.put("星期五", "Friday");
            weekNameMap.put("星期六", "Saturday");
            weekNameMap.put("星期日", "Sunday");


            System.out.println("星期日的英文名是" + weekNameMap.get("星期日"));
            //透過這種方式獲取key是不是十分簡潔
            System.out.println("Sunday的中文是" + weekNameMap.inverse().get("Sunday"));
        }

BiMap的值鍵對的Map可以透過inverse()方法得到

2.3.5.4 資料的強制唯一

在使用BiMap時,會要求Value的唯一性,如果value重複了則會丟擲錯誤:java.lang.IllegalArgumentException

    BiMap<String, String> weekNameMap = HashBiMap.create();
    weekNameMap.put("星期一", "Monday");
    weekNameMap.put("星期二", "Tuesday");
    weekNameMap.put("星期三", "Wednesday");
    weekNameMap.put("星期四", "Thursday");
    weekNameMap.put("星期五", "Friday");
    weekNameMap.put("星期六", "Saturday");
    weekNameMap.put("星期日", "Sunday");
    //程式碼執行後會報錯
    weekNameMap.put("星期某", "Sunday");

如果我們確實需要插入重複的value值,那可以選擇forcePut方法,但是我們需要注意的是前面的key也會被覆蓋了

    BiMap<String, String> weekNameMap = HashBiMap.create();
    weekNameMap.put("星期一", "Monday");
    weekNameMap.put("星期二", "Tuesday");
    weekNameMap.put("星期三", "Wednesday");
    weekNameMap.put("星期四", "Thursday");
    weekNameMap.put("星期五", "Friday");
    weekNameMap.put("星期六", "Saturday");
    weekNameMap.put("星期日", "Sunday");
    weekNameMap.forcePut("星期某", "Sunday");
    System.out.println("weekNameMap:"+weekNameMap);

輸出結果

weekNameMap:{星期一=Monday, 星期二=Tuesday, 星期三=Wednesday, 星期四=Thursday, 星期五=Friday, 星期六=Saturday, 星期某=Sunday}

2.3.5.5 理解inverse方法

inverse方法會返回一個反轉的BiMap,但是注意這個反轉的map不是新的map物件,它實現了一種檢視關聯,這樣你對於反轉後的map的所有操作都會影響原先的map物件

    BiMap<String, String> weekNameMap = HashBiMap.create();
    weekNameMap.put("星期一", "Monday");
    weekNameMap.put("星期二", "Tuesday");
    weekNameMap.put("星期三", "Wednesday");
    weekNameMap.put("星期四", "Thursday");
    weekNameMap.put("星期五", "Friday");
    weekNameMap.put("星期六", "Saturday");
    weekNameMap.put("星期日", "Sunday");
    weekNameMap.forcePut("星期某", "Sunday");
    BiMap<String, String> inverseMap = weekNameMap.inverse();
    System.out.println("反轉前後是否是同一個物件"+(inverseMap.hashCode()==weekNameMap.hashCode()));
    System.out.println("反轉前的結果:"+weekNameMap);
    System.out.println("反轉後的結果"+inverseMap);

輸出結果

反轉前後是否是同一個物件true
反轉前的結果:{星期一=Monday, 星期二=Tuesday, 星期三=Wednesday, 星期四=Thursday, 星期五=Friday, 星期六=Saturday, 星期某=Sunday}
反轉後的結果{Monday=星期一, Tuesday=星期二, Wednesday=星期三, Thursday=星期四, Friday=星期五, Saturday=星期六, Sunday=星期某}

2.3.6 一鍵多值的Map

有時候我們需要這樣的資料型別Map<String,Collection>,Multimap就是爲了解決這類問題的

2.3.6.1 相關實現類

Multimap提供了豐富的實現,所以你可以用它來替代程式裡的Map<K, Collection>

實現 Key實現 Value實現
ArrayListMultimap HashMap ArrayList
HashMultimap HashMap HashSet
LinkedListMultimap LinkedHashMap LinkedList
LinkedHashMultimap LinkedHashMap LinkedHashSet
TreeMultimap TreeMap TreeSet
ImmutableListMultimap ImmutableMap ImmutableList
ImmutableSetMultimap ImmutableMap ImmutableSet
2.3.6.2 案例場景

假如目前有個需求是給對學生的成績進行統計,統計出來各個成績的學員分佈,方法內容如下:

public static void statics(Map<String, Integer> studentMap) {
    Map<Integer, List<String>> scoreMap = new HashMap<>();
    for (
            Map.Entry<String, Integer> entry : studentMap.entrySet()) {
        if (!scoreMap.containsKey(entry.getValue())) {
            scoreMap.put(entry.getValue(), new ArrayList<>());
        }
        scoreMap.get(entry.getValue()).add(entry.getKey());
    }
    System.out.println("學員分數統計:" + scoreMap);
}

列印結果

{80=[張三, 吳九, 趙六], 70=[陳二, 王五, 周八], 60=[李四, 孫七, 鄭十, 劉一]}

可以看到我們實現起來特別麻煩,需要檢查key是否存在,不存在時則建立一個,存在時在List後面新增上一個

2.3.6.3 程式碼最佳化

Multimap 提供了一個方便地把一個鍵對應到多個值的數據結構,可以透過下面程式碼簡單實現

Multimap<Integer,String> multimap = HashMultimap.create();
for (Map.Entry<String, Integer> entry : studentMap.entrySet()) {
    multimap.put(entry.getValue(),entry.getKey());
}
System.out.println("學員分數統計:"+multimap);

常用第三方類庫總結

2.4 快取增強

Guava Cache 是Guava中的一個記憶體快取模組,用於將資料快取到JVM記憶體中

2.4.1 功能介紹
  • 提供了get、put封裝操作,能夠整合資料來源

  • 執行緒安全的快取,與ConcurrentMap相似,但前者增加了更多的元素失效策略,後者只能顯示的移除元素

  • Guava Cache提供了多種基本的快取回收方式

  • 監控快取載入/命中情況

2.4.2 使用場景
  • 願意花費一些記憶體來提高速度。

  • 使用場景有時會多次查詢key。

  • 快取將不需要儲存超出RAM容量的資料

2.4.3 JVM快取的缺點

JVM 快取,是堆快取,其實就是建立一些全域性容器,比如List、Set、Map等,這些容器用來做資料儲存,存在著很多的問題

  • 不能按照一定的規則淘汰資料,如 LRU,LFU,FIFO 等。

  • 清除資料時的回撥通知

  • 併發處理能力差,針對併發可以使用CurrentHashMap,但快取的其他功能需要自行實現快取過期處理,快取資料載入重新整理等都需要手工實現

2.4.4 快取分類
2.4.4.1 Cache

Cache是透過CacheBuilder的build()方法構建,它是Gauva提供的最基本的快取介面,並且它提供了一些常用的快取api

//構建Cache快取
Cache<String, String> cache = CacheBuilder.newBuilder().build();
// 放入/覆蓋一個快取
cache.put("key", "value");
// 獲取一個快取,如果該快取不存在則返回一個null值
String value = cache.getIfPresent("key");
System.out.println(value);
// 獲取快取,當快取不存在時,則通Callable進行載入並返回,該操作是原子
String getValue = cache.get("k", () -> "v");
System.out.println(getValue);

2.4.4.2 LoadingCache

LoadingCache繼承自Cache,在構建LoadingCache時,需要透過CacheBuilder的build(CacheLoader<? super K1, V1> loader)方法構建

    //構建Cache快取
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            System.out.println("load-key:" + key);
            return "value";
        }
    });
    // 獲取快取,當快取不存在時,會透過CacheLoader自動載入,該方法會丟擲ExecutionException異常
    String value = loadingCache.get("key1");
    System.out.println("快取物件:" + value);
    //獲取快取時不會自動載入,只檢查當前快取中是否存在
    String value1 = loadingCache.getIfPresent("key1");
    System.out.println("快取物件:" + value1);

2.4.5 快取配置策略
2.4.5.1 快取的併發級別

Guava提供了設定併發級別的API,使得快取支援併發的寫入和讀取。

ConcurrentHashMap類似,Guava cache的併發也是透過分片鎖實現,在通常情況下,推薦將併發級別設定為伺服器cpu核心數

    CacheBuilder.newBuilder()
            // 設定併發級別為cpu核心數,預設為4
            .concurrencyLevel(Runtime.getRuntime().availableProcessors()) 
            .build();

2.4.5.2 初始容量設定

我們在構建快取時可以為快取設定一個合理大小初始容量,由於Guava的快取使用了分片鎖的機制,擴容的代價非常昂貴,所以合理的初始容量能夠減少快取容器的擴容次數

 
    CacheBuilder.newBuilder()
            // 設定初始容量為100
            .initialCapacity(100)
            .build();

2.4.5.3 設定最大儲存

Guava Cache可以在構建快取物件時指定快取所能夠儲存的最大記錄數量

當Cache中的記錄數量達到最大值後再呼叫put方法向其中新增物件,Guava會先從當前快取的物件記錄中選擇一條刪除掉,騰出空間後再將新的物件儲存到Cache中

    CacheBuilder.newBuilder()
            // 設定最大容量為1000
            .maximumSize(1000)
            .build();

2.4.6 快取清除策略

Guava Cache可以在構建快取物件時指定快取的清除策略,當快取滿的時候根據情況進行清除資料

2.4.6.1 基於存活時間的清除策略

可以根據設定的讀寫的存活事件進行設定,expireAfterWrite引數設定寫快取後多久過期,expireAfterAccess引數設定讀快取後多久過期,存活時間策略可以單獨設定或組合配置

     CacheBuilder.newBuilder().
        //讀資料30分鐘後過期
        expireAfterAccess(Duration.ofMillis(30)).
        //寫資料30分鐘後過期
        expireAfterWrite(Duration.ofMinutes(30)).
        build();

2.4.6.2 基於容量的清除策略

透過CacheBuilder.maximumSize(long)方法可以設定Cache的最大容量數,當快取數量達到或接近該最大值時,Cache將清除掉那些最近最少使用的快取

     CacheBuilder.newBuilder().
        //設定最大容量是1000,接近最大容量將清除掉那些最近最少使用的快取
        maximumSize(1000).
        build();

2.4.6.3 基於權重的清除策略

使用CacheBuilder.weigher(Weigher)指定一個權重函式,並且用CacheBuilder.maximumWeight(long)指定最大總重

如每一項快取所佔據的記憶體空間大小都不一樣,可以看作它們有不同的“權重”(weights),作為執行清除策略時最佳化回收的物件

2.4.6.4 顯式清除

自動清除實時性沒有那麼好,如果條件允許可以採用手動清除

  • 清除單個key:Cache.invalidate(key)

  • 批次清除key:Cache.invalidateAll(keys)

  • 清除所有快取項:Cache.invalidateAll()

2.4.6.5 引用清除

在構建Cache例項過程中,透過設定使用弱引用的鍵、或弱引用的值、或軟引用的值,從而使JVM在GC時順帶實現快取的清除

  • CacheBuilder.weakKeys():使用弱引用儲存鍵,當鍵沒有其它(強或軟)引用時,快取項可以被垃圾回收

  • CacheBuilder.weakValues():使用弱引用儲存值, 當值沒有其它(強或軟)引用時,快取項可以被垃圾回

  • CacheBuilder.softValues():使用軟引用儲存值,軟引用只有在響應記憶體需要時,才按照全域性最近最少使用的順序回收。考慮到使用軟引用的效能影響,我們通常建議使用更有效能預測性的快取大小限定

     CacheBuilder.newBuilder().
        //使用弱引用的key
        weakKeys().
        //使用軟引用的值
        softValues().build();

2.4.7 快取重新整理

在Guava cache中支援定時重新整理和顯式重新整理兩種方式,其中只有LoadingCache能夠進行定時重新整理。

2.4.7.1 定時重新整理

在進行快取定時重新整理時,我們需要指定快取的重新整理間隔,和一個用來載入快取的CacheLoader,當達到重新整理時間間隔後,下一次獲取快取時,會呼叫CacheLoader的load方法重新整理快取

例如構建個重新整理頻率為10秒的快取

     //構建Cache快取
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().
        //十秒後自動重新整理快取
        refreshAfterWrite(Duration.ofSeconds(10)).
        build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println("load-key:" + key);
                return "value";
            }
        });
    loadingCache.put("key", "xxxx");
    System.out.println("key的值是:"+loadingCache.get("key"));
    Thread.sleep(15000);
    System.out.println("key的值是:"+loadingCache.get("key"));

執行後檢視執行效果

key的值是:xxxx
load-key:key
key的值是:value

2.4.7.2 顯式重新整理

在快取構建完畢後,我們可以透過Cache提供的一些藉口方法,顯式的對快取進行重新整理覆蓋

    //構建Cache快取
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().
        //十秒後自動重新整理快取
        refreshAfterWrite(Duration.ofSeconds(10)).
        build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println("load-key:" + key);
                return "value";
            }
        });
    loadingCache.put("key", "xxxx");
    System.out.println("key的值是:" + loadingCache.get("key"));
    //對key執行顯示重新整理
    loadingCache.refresh("key");
    System.out.println("key的值是:" + loadingCache.get("key"));

2.5 單機限流

在網際網路高併發場景下,限流是用來保證系統穩定性的一種手段,當系統遭遇瞬時流量激增時,可能會由於系統資源耗盡導致宕機。而限流可以把一小部分流量拒絕掉,保證大部分流量可以正常訪問,從而保證系統只接收承受範圍以內的請求,多餘的請求給拒絕掉

2.5.1 常見的限流演算法

常用的限流演算法有 漏桶演算法、令牌桶演算法

2.5.1.1 漏桶演算法

漏桶的意思就像一個漏斗一樣,水一滴一滴的滴下去,流出是勻速的。

當訪問量過大的時候這個漏斗就會積水,漏桶演算法的實現依賴佇列,一個處理器從隊頭依照固定頻率取出資料進行處理,如果請求量過大導致佇列堆滿那麼新來的請求就會被拋棄,漏桶一般按照固定的速率流出,圖解如下:

常用第三方類庫總結

2.5.1.2 令牌桶演算法

令牌桶則是存放固定容量的令牌,按照固定速率從桶中取出令牌。

初始給桶中新增固定容量令牌,當桶中令牌不夠取出的時候則拒絕新的請求,令牌桶不限制取出令牌的速度,只要有令牌就能處理,所以令牌桶允許一定程度的突發,圖解如下:

常用第三方類庫總結

2.5.1.3 兩種區別
  • 漏桶演算法是桶中有水就需要等待,桶滿就拒絕請求,而令牌桶是桶變空了需要等待令牌產生

  • 漏桶演算法漏水的速率固定,令牌桶演算法往桶中放令牌的速率固定

  • 令牌桶可以接收的瞬時流量比漏桶大,比如桶的容量為100,令牌桶會裝滿100個令牌,當有瞬時80個併發過來時可以從桶中迅速拿到令牌進行處理,而漏桶的消費速率固定,當瞬時80個併發過來時,可能需要進行排隊等待

2.5.2 RateLimiter

Guava中的限流使用的是令牌桶演算法,RateLimiter提供了兩種限流實現

2.5.2.1 平滑突發限流(SmoothBursty)

每秒以固定的速率輸出令牌,以達到平滑輸出的效果

         public static void smoothBursty() throws InterruptedException {
            //每秒5個令牌
            RateLimiter rateLimiter = RateLimiter.create(5);
            while (true) {
                //獲取令牌等待的時間
                System.out.println("獲取令牌等待:" + rateLimiter.acquire() + "秒");
            }
        }

輸出結果

獲取令牌等待:0.0秒
獲取令牌等待:0.198623秒
獲取令牌等待:0.195207秒
獲取令牌等待:0.199541秒
獲取令牌等待:0.193337秒
獲取令牌等待:0.199644秒
獲取令牌等待:0.193321秒

平均每個0.2秒左右,很均勻,但是當產生令牌的速率大於取令牌的速率時,是不需要等待令牌時間的

          public static void smoothBursty() throws InterruptedException {
            //每秒5個令牌
            RateLimiter rateLimiter = RateLimiter.create(5);
            //執行緒休眠,給足夠的時間生成令牌
            Thread.sleep(1000);
            while (true) {
                //獲取令牌等待的時間
                System.out.println("獲取令牌等待:" + rateLimiter.acquire() + "秒");
            }
        }

輸出結果

獲取令牌等待:0.0秒
獲取令牌等待:0.0秒
獲取令牌等待:0.0秒
獲取令牌等待:0.0秒
獲取令牌等待:0.0秒
獲取令牌等待:0.0秒
獲取令牌等待:0.0秒

由於令牌可以積累,所以我一次可以取多個令牌,只要令牌充足,可以快速響應

  public class RateLimiterTest {
        public static void main(String[] args) throws InterruptedException {
            //每秒5個令牌
            RateLimiter rateLimiter = RateLimiter.create(5);
            while (true) {
                //獲取令牌等待的時間,一次獲取5個令牌
                System.out.println("獲取5個令牌等待:" + rateLimiter.acquire(5) + "秒");
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("----------------------------------------------------");
            }
        }
    }

列印結果

獲取5個令牌等待:0.0秒
獲取1個令牌等待:0.998742秒
獲取1個令牌等待:0.196268秒
獲取1個令牌等待:0.199579秒
----------------------------------------------------
獲取5個令牌等待:0.191254秒
獲取1個令牌等待:0.999548秒
獲取1個令牌等待:0.190791秒
獲取1個令牌等待:0.19923秒
----------------------------------------------------
獲取5個令牌等待:0.199584秒
獲取1個令牌等待:0.99069秒
獲取1個令牌等待:0.1984秒
獲取1個令牌等待:0.199753秒

2.5.2.2 平滑預熱限流(SmoothWarmingUp)

平滑預熱限流帶有預熱期的平滑限流,它啟動後會有一段預熱期,逐步將令牌產生的頻率提升到配置的速率,這種方式適用於系統啟動後需要一段時間來進行預熱的場景

比如,我設定的是每秒5個令牌,預熱期為5秒,那麼它就不會是0.2左右產生一個令牌,在前5秒鐘它不是一個均勻的速率,5秒後恢復均勻的速率

    public class RateLimiterTest {
        public static void main(String[] args) {
            //每秒5個令牌,預熱期為5秒
            RateLimiter rateLimiter = RateLimiter.create(5, 5, TimeUnit.SECONDS);
            while (true) {
                //獲取令牌等待的時間,一次獲取5個令牌
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("獲取1個令牌等待:" + rateLimiter.acquire(1) + "秒");
                System.out.println("----------------------------------------------------");
            }
        }
    }

輸出結果,我們發現隨著時間發展,令牌的獲取速度越來越快,一直到5S後速度維持穩定

獲取1個令牌等待:0.0秒
獲取1個令牌等待:0.578588秒
獲取1個令牌等待:0.548351秒
獲取1個令牌等待:0.519537秒
獲取1個令牌等待:0.47878秒
----------------------------------------------------
獲取1個令牌等待:0.454648秒
獲取1個令牌等待:0.422842秒
獲取1個令牌等待:0.391856秒
獲取1個令牌等待:0.359569秒
獲取1個令牌等待:0.31791秒
----------------------------------------------------
獲取1個令牌等待:0.294656秒
獲取1個令牌等待:0.26316秒
獲取1個令牌等待:0.231666秒
獲取1個令牌等待:0.203027秒
獲取1個令牌等待:0.199316秒
----------------------------------------------------
獲取1個令牌等待:0.199024秒
獲取1個令牌等待:0.199573秒
獲取1個令牌等待:0.194181秒
獲取1個令牌等待:0.199364秒
獲取1個令牌等待:0.200051秒

2.6 數學增強

2.6.1 Guava資料工具類優點
2.6.1.1 充分測試

Guava Math針對各種不常見的溢位情況都有充分的測試;

對溢位語義,Guava文件也有相應的說明;如果運算的溢位檢查不能透過,將導致快速失敗;

2.6.1.2 效能優異

Guava Math的效能經過了精心的設計和調優;

雖然效能不可避免地依據具體硬體細節而有所差異,但Guava Math的速度通常可以與Apache Commons的MathUtils相比,在某些場景下甚至還有顯著提升;

2.6.1.3 可讀性高

Guava Math在設計上考慮了可讀性和正確的程式設計習慣

IntMath.log2(x, CEILING) 所表達的含義,即使在快速閱讀時也是清晰明確的,而32-Integer.numberOfLeadingZeros(x – 1)對於閱讀者來說則不夠清晰

2.6.2 整數運算

Guava Math主要處理三種整數型別:int、long和BigInteger,這三種類型的運算工具類分別叫做IntMath、LongMath和BigIntegerMath

2.6.2.1 直接計算的問題

在JDK中進行數值計算需要判斷邊界,如果一旦判斷不好就容易出現問題

//資料一旦溢位後,資料就變成了負數
int n = Integer.MAX_VALUE+10;
System.out.println(n);

類似於上面的程式碼,一旦溢位就變成了負數,出現了Bug

2.6.1.2 有溢位檢查的運算

Guava Math提供了若干有溢位檢查的運算方法:結果溢位時,這些方法將快速失敗而不是忽略溢位

//一旦溢位就會報錯,不會出現出現溢位值
int n = IntMath.checkedAdd(Integer.MAX_VALUE, 10);
System.out.println(n);

2.6.1.3 常用的API

常見的檢查並進行操作的有以下幾個API

Int型別 Long型別 檢查操作
IntMath.checkedAdd LongMath.checkedAdd
IntMath.checkedSubtrac LongMath.checkedSubtract
IntMath.checkedMultiply LongMath.checkedMultiply
IntMath.checkedPow LongMath.checkedPow 次方

3. Spring中的工具類

Spring Framework裡的spring-core核心包裡面,有個org.springframework.util裡面有不少非常實用的工具類

該工具包裡面的工具類雖然是被定義在Spring下面的,但是由於Spring框架目前幾乎成了JavaEE實際的標準了,因此我們直接使用也是無妨的,很多時候能夠大大的提高我們的生產力

常用第三方類庫總結

3.1 Assert 斷言工具類

Assert斷言工具類,通常用於資料合法性檢查,

3.1.1 正常程式碼方式

這種方式程式碼比較繁瑣,並且不太優雅

if (message== null || message.equls("")) {  
        throw new IllegalArgumentException("輸入資訊錯誤!");  
}

3.1.2 Assert方式

可以透過Assert方式最佳化的進行驗證引數

    Assert.hasText("","輸入資訊錯誤!");

3.1.3 常用的斷言
    Assert.notNull(Object object, "object is required") //物件非空 
    Assert.isTrue(Object object, "object must be true") //物件必須為true   
    Assert.notEmpty(Collection collection, "collection must not be empty") //集合非空  
    Assert.hasLength(String text, "text must be specified")// 字元不為null且字元長度不為0   
    Assert.hasText(String text, "text must not be empty") //  text 不為null且必須至少包含

一個非空格的字元   Assert.isInstanceOf(Class clazz, Object obj, "clazz must be of type [clazz]")//obj必須能被正確造型成為clazz 指定的類

當然你也可以自行寫斷言工具類,尤其是需要丟擲業務異常時 !

3.2 PathMatcher 路徑匹配器

Spring提供的實現:AntPathMatcher Ant路徑匹配規則

SpringMVC的路徑匹配規則是依照Ant的來的,實際上不只是SpringMVC,整個Spring框架的路徑解析都是按照Ant的風格來的

AntPathMatcher不僅可以匹配Spring的@RequestMapping路徑,也可以用來匹配各種字串,包括檔案路徑等

3.2.1 什麼是Ant路徑

Ant路徑就是我們常用的路徑模式

3.2.1.1 Ant萬用字元

ANT萬用字元有三種

萬用字元 說明
? 匹配任何單字元
* 匹配0或者任意數量的字元
** 匹配0或者更多的目錄
3.2.1.2 Ant路徑舉例
URL路徑 說明
/app/*.x 匹配(Matches)所有在app路徑下的.x檔案
/app/p?ttern 匹配(Matches) /app/pattern 和 /app/pXttern,但是不包括/app/pttern
/**/example 匹配(Matches) /app/example, /app/foo/example, 和 /example
/app//dir/file. 匹配(Matches) /app/dir/file.jsp, /app/foo/dir/file.html,/app/foo/bar/dir/file.pdf, 和 /app/dir/file.java
/* / .jsp 匹配(Matches)任何的.jsp 檔案
3.2.2 路徑匹配問題

你是否曾今在你們的Filter裡看過類似下面的程式碼

常用第三方類庫總結

這種所謂的白名單URL這樣來匹配,可謂非常的不優雅並且難於閱讀,而且透過窮舉法的擴充套件性非常差

3.2.3 最佳化程式碼

我們可以使用Spring的路徑匹配器來進行程式碼的最佳化

    PathMatcher pathMatcher = new AntPathMatcher();
    String requestPath = "/user/list.htm?username=xxx&sex=0";
    //路徑匹配模版
    String patternPath = "/user/list.htm**";
    Assert.isTrue(pathMatcher.match(patternPath,requestPath),"路徑匹配錯誤");

3.3 PropertyPlaceholderHelper

將字串裡的佔位符內容,用我們配置的properties裡的替換

這個是一個單純的類,沒有繼承沒有實現,並且沒有依賴Spring框架其他的任何類

3.3.1 原始程式碼

是否有過這種場景,定義一個模板,根據不同的變數生成不同的值

    String template = "姓名:{name},年齡:{age}, 性別:{sex}";

上面的模板如何根據變數來生成內容呢?我們可以使用MessageFormat來實現,但是要求必須使用{index}方式

    String template = "姓名:{0},年齡:{1}, 性別:{2}";
    String message = MessageFormat.format(template, "張三", "25", "man");
    System.out.println(message);

感覺明顯不太好用,並且還限定了邊界的符號,不能自定義符號

3.3.2 最佳化程式碼

我們可以使用PropertyPlaceholderHelper來最佳化程式碼的實現

    //定義屬性,可以從檔案中讀取
    Properties properties = new Properties();
    properties.put("name", "張三");
    properties.put("age", "25");
    properties.put("sex", "man");
    //定義propertyPlaceholderHelper
    PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("{", "}");
    //定義模板
    String template = "姓名:{name},年齡:{age}, 性別:{sex}";
    //==============開始解析此字串==============
    String content = propertyPlaceholderHelper.replacePlaceholders(template, properties);
    System.out.println("替換前模板內容:" + template);
    System.out.println("替換模板後內容:" + content);

3.4 BeanUtils

BeanUtils 工具類提供了非常豐富的Java反射API,開發過程中使用恰當可以減少很懂工作量, 其中最常用的莫過於copyProperties

3.4.1 使用場景

在開發中,經常用到屬性copy,比如從一個各種VO,DTO,BO等的轉換,大部分屬性都差不多,如果手動轉換會非常麻煩,比如這裏用到一個UserVOUserBO,如果進行轉換則需要如下的程式碼

 
    UserBO userBO = new UserBO();
    userBO.setUserName(userVO.getUserName());
    userBO.setAge(userVO.getAge());
    //....

當然,現在一般都會使用mapstruct來做實體轉換,因為他是編譯器生成轉換程式碼(set get )使用jdk原生語法,效能更好些。

3.4.2 使用屬性copy最佳化

使用屬性copy會非常方便

 
    UserBO userBO = new UserBO();
    BeanUtils.copyProperties(userVO,userBO);
    //不批配的在另外處理

3.5 DigestUtils

可以對位元組陣列、InputStream流生成摘要

3.5.1 計算檔案摘要

在開發中對於檔案上傳一般都是需要生成檔案摘要的,防止檔案重複,如果檔案摘要是一樣的就認為是同一個檔案,不需要在將檔案上傳上去了,這樣可以節省硬碟空間,防止產生大量重複的檔案

    String digest = DigestUtils.md5DigestAsHex(new FileInputStream(new File("/Users/hzz/Downloads/out.txt")));

執行後就輸出了檔案的摘要

2645fc604371f0b7a0809f2b93abb21f

3.5.2 密碼加密

透過這種計算摘要演算法還可以對密碼進行加密,這個和MD5類似,也是不可破解的方式進行加密的

   String password = "123qwe!@#QWE";
    //密碼加鹽後計算MD5的值
    String digest = DigestUtils.md5DigestAsHex((password+"XCFGCG").getBytes("UTF-8"));

可以直接透過Spring提供的工具類進行MD5加密

4. HuTool(擴充套件)

Hutool是一個小而全的Java工具類庫,透過靜態方法封裝,降低相關API的學習成本,提高工作效率,使Java擁有函式式語言般的優雅,讓Java語言也可以“甜甜的”。

4.1 HuTool簡介

Hutool中的工具方法來自每個使用者的精雕細琢,它涵蓋了Java開發底層程式碼中的方方面面,它既是大型專案開發中解決小問題的利器,也是小型專案中的效率擔當

4.1.1 設計哲學

Hutool的設計思想是儘量減少重複的定義,讓專案中的util這個package儘量少,

  1. 減少程式碼錄入

  2. 常用功能組合起來,實現一個功能只用一個方法

  3. 簡化Java API,原來需要幾個類實現的功能我也只是用一個類甚至一個方法(想想爲了個執行緒池我得new多少類……而且名字還不好記)

  4. 一些固定使用的演算法收集到一起,不用每次問度娘了(例如Base64演算法、MD5、Sha-1,還有Hash演算法)

  5. 借鑑Python的很多小技巧(例如列表切片,列表支援負數index),讓Java更加好用。

  6. 非常好用的ORM框架,同樣借鑑Python的Django框架,以鍵值對的實體代替物件實體,大大降低資料庫訪問的難度(再也不用像Hibernate一樣配置半天ORM Mapping了)。

  7. 極大簡化了檔案、日期的操作,尤其是相對路徑和絕對路徑問題做了非常好的封裝,降低學習成本。

4.1.2 安裝

在專案的pom.xml的dependencies中加入以下內容

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.6.6</version>
</dependency>

4.2 包含的元件

一個Java基礎工具類,對檔案、流、加密解密、轉碼、正則、執行緒、XML等JDK方法進行封裝,組成各種Util工具類,同時提供以下元件

模組 介紹
hutool-aop JDK動態代理封裝,提供非IOC下的切面支援
hutool-bloomFilter 布隆過濾,提供一些Hash演算法的布隆過濾
hutool-cache 簡單快取實現
hutool-core 核心,包括Bean操作、日期、各種Util等
hutool-cron 定時任務模組,提供類Crontab表示式的定時任務
hutool-crypto 加密解密模組,提供對稱、非對稱和摘要演算法封裝
hutool-db JDBC封裝後的資料操作,基於ActiveRecord思想
hutool-dfa 基於DFA模型的多關鍵字查詢
hutool-extra 擴充套件模組,對第三方封裝(模板引擎、郵件、Servlet、二維碼、Emoji、FTP、分詞等)
hutool-http 基於HttpUrlConnection的Http客戶端封裝
hutool-log 自動識別日誌實現的日誌門面
hutool-script 指令碼執行封裝,例如Javascript
hutool-setting 功能更強大的Setting配置檔案和Properties封裝
hutool-system 系統引數呼叫封裝(JVM資訊等)
hutool-json JSON實現
hutool-captcha 圖片驗證碼實現
hutool-poi 針對POI中Excel和Word的封裝
hutool-socket 基於Java的NIO和AIO的Socket封裝
hutool-jwt JSON Web Token (JWT)封裝實現

4.3 文件介紹

hutool提供有很詳細的官方文件,並且內容也比較詳細,地址是https://www.hutool.cn/docs/index.html,下面我們簡單的介紹幾個比較好用的

4.4 型別轉換工具類

4.4.1 Convert類

Convert類可以說是一個工具方法類,裡面封裝了針對Java常見型別的轉換,用於簡化型別轉換

Convert類中大部分方法為toXXX,引數為Object,可以實現將任意可能的型別轉換為指定型別,同時支援第二個引數defaultValue用於在轉換失敗時返回一個預設值

    public class ConvertTest {
        public static void main(String[] args) {
            String str = "1234";
            //將字串轉換成int,如果轉換失敗預設值為0
            int num = Convert.toInt(str, 0);
            System.out.println("轉成成數字:" + num);
            //將金額轉換成大寫
            float money = 12345.67f;
            String moneyUpper = Convert.digitToChinese(money);
            System.out.println("大寫金額:" + moneyUpper);
            //將數字進行簡化表示
            float number = 789563;
            String simp = Convert.numberToSimple(number);
            System.out.println("簡化後的數字:" + simp);
        }
    }

輸出結果

轉成成數字:1234
大寫金額:壹萬貳仟叄佰肆拾伍元陸角柒分
簡化後的數字:78.96w

4.2 IO工具類

下面列舉幾個比較常用的IO工具類的使用

4.2.1 獲取檔案型別

在檔案上傳時,有時候我們需要判斷檔案型別。但是又不能簡單的透過副檔名來判斷(防止惡意指令碼等透過上傳到伺服器上),於是我們需要在服務端透過讀取檔案的首部幾個二進制位來判斷常用的檔案型別

      File file = FileUtil.file("/Users/hzz/Downloads/out.txt");
    System.out.println(FileTypeUtil.getType(file));

上面的程式碼out.txt實際是一張jpg圖片,只是將圖片的副檔名改成了txt,透過該工具可以將檔案的真實格式讀取出來

4.2.2 檔案監聽

很多時候我們需要監聽一個檔案的變化或者目錄的變動,包括檔案的建立、修改、刪除,以及目錄下檔案的建立、修改和刪除

public class FileUtilTest {


    public static void main(String[] args) {
        File file = FileUtil.file("/Users/hzz/Downloads/out.txt");
        WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);
        watchMonitor.setWatcher(new Watcher(){
            @Override
            public void onCreate(WatchEvent<?> event, Path currentPath) {
                Object obj = event.context();
                Console.log("建立:{}-> {}", currentPath, obj);
            }


            @Override
            public void onModify(WatchEvent<?> event, Path currentPath) {
                Object obj = event.context();
                Console.log("修改:{}-> {}", currentPath, obj);
            }


            @Override
            public void onDelete(WatchEvent<?> event, Path currentPath) {
                Object obj = event.context();
                Console.log("刪除:{}-> {}", currentPath, obj);
            }


            @Override
            public void onOverflow(WatchEvent<?> event, Path currentPath) {
                Object obj = event.context();
                Console.log("Overflow:{}-> {}", currentPath, obj);
            }
        });
        watchMonitor.start();
    }
}

ok本文就到這裏,後續如果遇到更好的並且我也有時間將持續更新此文。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.