螞蟻金服宮孫:guava探究系列之優雅校驗資料

支付寶技術團隊發表於2019-07-05

優雅校驗資料-前置條件

前言

根據防禦式程式設計的要求, 在日常的開發中, 總少不了對函式的各種入參做校驗, 以便保證函式能按照預期的流程執行下去. 比如各種費率的值就沒可能是負數, 如果費率出現負數, 所以資料有問題, 我們需要做的事情就是把這些有問題的資料挑出來. 自己手寫這些校驗函式未免過於繁瑣, 所幸的是我們需要的函式已經有現成的:

Guava 提供了一系列的靜態方法用於校驗函式和類的構造器是否符合預期, 並稱其為前置條件(preconditions). 如果前置條件校驗失敗, 就會丟擲一個指定的異常.

前置函式特徵

目前的前置校驗方法有如下特徵:

須需要, 下面例子中的 checkArgument 函式可以替換成任何一個前置條件校驗函式

  1. 這些前置方法一般接受一個布林表示式作為入參,並判斷表達是否為 true , 格式如:
Preconditions.checkArgument(a>1)// 如果表示式為false, 丟擲IllegalArgumentException
  1. 除了用於判斷的布林表示式之外, 前置方法可以接受一個額外的 Object 作為入參, 在丟擲異常的時候, 把 Object.toString() 作為異常資訊, 如:
public enum ErrorDetail {
    SC_NOT_FOUND("404", "Resource could not be fount");    // 省略部分內容
    @Override
    public String toString() {        return "ErrorDetail{" + "code='" + code + '\'' + ", description='" + description + '\'' + '}';
    }
}@Testpublic void testCheckArgument() {
    Preconditions.checkArgument(1 > 2, ErrorDetail.SC_NOT_FOUND);
}// 結果如下:// java.lang.IllegalArgumentException: ErrorDetail{code='404', description='Resource could not be fount'}
  1. Guava的前置表示式還支援類似 printf 函式那樣的格式化輸出錯誤資訊, 只不過出於相容性和效能的考慮, 只支援使用 %s 指示符格式化字串, 不支援其他型別. 如:
int i=-1;
checkArgument(i >= 0, "Argument was %s but expected nonnegative", i);// 結果如下:// java.lang.IllegalArgumentException: Argument was -1 but expected nonnegative

前置條件函式介紹

須注意的是, 下面介紹的 checkArgument checkArgument checkState 函式都有三個對應的過載函式,分別對應前文所述的三種特徵, 下文不會三種函式都介紹, 只介紹標準格式的前置條件函式. 以 checkArgument 函式為例, 三個過載函式分別是(忽略函式體):

public static void checkArgument(boolean expression);public static void checkArgument(boolean expression, @Nullable Object errorMessage);public static void checkArgument(boolean expression,@Nullable String errorMessageTemplate,@Nullable Object... errorMessageArgs)

checkArgument

函式的簽名如下:

public static void checkArgument(boolean expression);

入參是一個布林表示式, 函式校驗這個表示式是否為 true , 如果為 false , 丟擲 IllegalArgumentException . 例子如下:

@Testpublic void testCheckArgument() {
    Preconditions.checkArgument(1 > 2);
}

checkNotNull

這是個泛型函式, 函式簽名如下:

public static <T> T checkNotNull(T reference);

入參是個任意型別的物件, 函式校驗這個物件是否為 null , 如果為空, 丟擲 NullPointerException , 否則直接返回該物件, 所以 checkNotNull 的用法就比較有趣, 可以在呼叫 setter 方法前作前置校驗. 例子如下:

PreconditionTest caller = new PreconditionTest();
caller.setErrorDetail(Preconditions.checkNotNull(ErrorDetail.SC_INTERNAL_SERVER_ERROR));

checkState

函式簽名如下:

public static void checkState(boolean expression);

看著這個函式, 我個人感覺很奇怪: 這個函式和 checkNotNull 函式功能非常相似, 實現也基本一樣, 都是判斷表示式是否為 true , 只是丟擲的異常不一樣而已, 是否有必要開發這個函式. 兩個函式的實現如下:

public static void checkArgument(boolean expression) {  if (!expression) {    throw new IllegalArgumentException();
  }
}public static void checkState(boolean expression) {  if (!expression) {    throw new IllegalStateException();
  }
}

此外, 因為這兩個函式相當類似, 就不展示相應例子了.

checkElementIndex

函式簽名如下:

public static int checkElementIndex(int index, int size);

這個函式用於判斷指定陣列, 列表, 字串的下標是否越界,  index 是下標,  size 是陣列, 列表或字串的長度, 下標的有效範圍是 [0,陣列長度)  即  0<=index<size . 如果陣列下標越界(即 index <0 或者  index >= size ), 那麼丟擲 IndexOutOfBoundsException 異常, 否則返回陣列的下標, 也就是 index . 例子如下:

Preconditions.checkElementIndex("test".length(), "test".length());// 執行結果:// 丟擲異常: java.lang.IndexOutOfBoundsException: index (4) must be less than size (4)Assert.assertEquals(3, Preconditions.checkElementIndex("test".length() - 1, "test".length()));// 執行結果:// 透過

checkPositionIndex

函式的簽名如下:

public static int checkPositionIndex(int index, int size);

這個函式和 checkElementIndex 非常類似, 連Guava wiki的說明也基本一致(只有一個單詞不同), 除了一點,  checkElementIndex 函式的下標有效範圍是 [0, 陣列長度) , 而 checkPositionIndex 函式的下標有有效範圍是 [0, 陣列長度] , 即 0<=index<=size . 例子如下:

Preconditions.checkPositionIndex("test".length() + 1, "test".length());// 執行結果:// 丟擲異常: java.lang.IndexOutOfBoundsException: index (5) must be less than size (4)Assert.assertEquals(4, Preconditions.checkPositionIndex("test".length(), "test".length()));// 執行結果:// 透過

checkPositionIndexes

函式的簽名如下:

public static void checkPositionIndexes(int start, int end, int size);

這個函式是用於判斷 [start,end] 這個範圍是否是個有效範圍, 即 [start, end]  是否在 [0, size]  範圍內(如果 [start, end]  和 [0, size] 相同, 也認為在範圍內), 如果不在, 則丟擲 IndexOutOfBoundsException 異常. 例子如下:

Preconditions.checkPositionIndexes(1, 3, 2);// 執行結果:// 丟擲異常: java.lang.IndexOutOfBoundsException: end index (3) must not be greater than size (2)Preconditions.checkPositionIndexes(0, 2, 2);// 執行結果:// 校驗透過

前置條件在實際專案的應用

前置條件在檢驗條件不成交的時候拋的異常型別雖說是合情合理(比如,  checkArgument 函式丟擲 IllegalArgumentException ), 但是對於業務系統來說, 你丟擲個 IllegalArgumentException 或者 NullPointerException , 介面呼叫方對於這個異常摸不著頭腦, 雖說只是正常的資料問題, 還是很容易覺得介面提供方服務出了問題, 甚至還會被質疑技術不過硬. 我們們又不是底層元件, 拋個 NPE , 著實是不成體統. 基於各種有的沒的的原因, 我們的業務系統在使用前置條件的時候進行了封裝, 將前置條件丟擲的異常進行了轉換, 換成正常的業務異常, 提供完整的異常資訊, 程式碼如下:

// 封裝程式碼:public final class AssertUtils {        /**
        * 檢查條件表示式是否為真
        *
        * @param expression 條件表示式
        * @param errDetailEnum 錯誤碼
        * @param msgTemplate 錯誤訊息模板
        * @param vars 佔位符對應變數
        * @throws BkmpException 條件表示式結果為假
        */
    public static void checkArgument(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate,
                                        Object... vars) {        try {
            Preconditions.checkArgument(expression);
        } catch (IllegalArgumentException e) {            throw new BkmpException(errDetailEnum, msgTemplate, vars);
        }
    }        /**
        * 檢查條件表示式是否為假
        *
        * @param expression 條件表示式
        * @param errDetailEnum 錯誤碼
        * @param msgTemplate 錯誤訊息模板
        * @param vars 佔位符對應變數
        * @throws BkmpException 條件表示式結果為假
        */
    public static void checkArgumentNotTrue(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate,
                                            Object... vars) {        try {
            Preconditions.checkArgument(!expression);
        } catch (IllegalArgumentException e) {            throw new BkmpException(errDetailEnum, msgTemplate, vars);
        }
    }
}// 省略其他部分的封裝// 呼叫例子:AssertUtils.checkArgument(merchantEntity.exist(), ErrDetailEnum.DATA_NOT_EXIT, "商戶不存在");

Guava Precondition vs Apache Common Validate

自古文無第一, 武無第二, 文人之間的口水戰總是少不了的. 沒想到這不是國人的專利, 原來國外也有文人相輕的風氣: Guava wiki 在介紹完preconditions之後, 還踩了一波競品Apache Common Validate, 認為Guava的preconditions 比Apache Common 更加清晰明瞭, 也更加美觀, 我個人對Apache Common Validate 瞭解不深, 也不好隨意置喙. 除了踩競品之外, Guava wiki 還提了兩點最佳實踐(best practice):

  1. 使用前置條件校驗的時候, 推薦每個校驗條件單獨一行, 這樣即更瞭然, 出問題也更方便除錯.
  2. 使用前置條件校驗的時候, 儘量提供有用的錯誤資訊, 這樣可以更快地定位問題.

參考資料

總結

程式碼大全一書有一章是關於防禦式程式設計的, 用於提高程式的健壯性, 主要思想是子程式應該不因傳入錯誤資料而被破壞,要保護程式免遭非法輸入資料的破壞. 而Guava的preconditions 就是實現防禦式程式設計的有力工具呢. oh yeah!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69904796/viewspace-2649674/,如需轉載,請註明出處,否則將追究法律責任。

相關文章