二胖的參數校驗坎坷之路

背景

最近端午好久沒有和二胖聚一聚了,於是約了二胖到人民廣場去宰他一頓,正好最近他跳槽加薪了。

:二胖聽說你最近跳槽了,並且還是從傳統軟件公司跳到了互聯網公司,工資是不是漲了一點啊,今天你請客哈。

二胖:別說了,工資是漲了點,但是性價比反而變低了,以前到點就下班,現在下班到家都快12點了。

:新公司怎麼樣還適應嗎?除了上班時間久點。

二胖:哎,這個還真稍微有點不適應,這不是剛進去沒啥事,leader就給我安排了一個簡單的用戶保存功能(參數校驗),原來以前公司個把小時就做好了的功能,在這新公司硬是折騰了兩三天,真是苦不堪言。我改了好幾個版本最終leader才滿意的點了點頭。
在這裏插入圖片描述

接口裸奔

  • 按照二胖在以前公司的寫法再傳統公司反正系統都是服務內部人員的,在後端寫參數校驗是不存在的事情,完全信賴前端傳過來的內容。這不寫完代碼自測一把發現可以保存數據,就屁顛屁顛的發起代碼review了(二胖在以前的公司代碼review是不存在的,只要功能實現就好了)。正好leader今天有點時間,看到新同事提交的代碼看看寫的怎麼樣。 看着這個裸奔的接口,leader把二胖叫了過去,語重心長的跟二胖說道:“你這個參數校驗不寫寫嗎?不怕人家攻擊你的接口嗎?這裏不校驗,直接用,不怕引入sql注入嗎?這裏不校驗下郵箱是否符合格式嗎?這個判空也不寫,不怕大量的空指針,服務熔斷嗎?…”。面對leader的拼命十三問,二胖心想試用期怕是有點難過哦?只能低着頭回到工位重新按照leader的教育整改起來,然後又重新提交了。

參數校驗if判斷

leader看了看說到:“這次代碼比上次好多了,功能基本沒啥問題了,但是這一塊代碼是不是可以在優化下,這樣寫不是很優雅”

   if(Objects.isNull(user)){
            throw new IllegalArgumentException("用戶不能爲空");
        }
        if(StringUtils.isEmpty(user.getUserName())){
            throw new IllegalArgumentException("用戶名不能爲空");
        }
        if(StringUtils.isEmpty(user.getUserName())){
            throw new IllegalArgumentException("用戶名不能爲空");
        }
        if(StringUtils.isEmpty(user.getSex())){
            throw new IllegalArgumentException("用戶性別不能爲空");
        }
        if(Objects.isNull(user.getUserDetail())){
            throw new IllegalArgumentException("用戶詳細信息不能爲空");
        }
        if(Objects.isNull(user.getUserDetail().getAddress())){
            throw new IllegalArgumentException("用戶地址不能爲空");
        }
        if(!"M".equals(user.getSex()) && !"F".equals(user.getSex())){
            throw new IllegalArgumentException("用戶性別不合法");
        }

二胖也是一陣鬱悶,還是懷念以前的公司啊,功能實現就好,代碼想怎麼寫就怎麼寫。互聯網公司就是規矩多,寫完代碼還要寫單測,還要監控一堆破事,活該這羣人996.時間都花到這上面去了。抱怨該抱怨但是代碼還得改啊。現在疫情期間好不容易找一個工作不能丟啊。
二狗想到以前不是學過aop嗎?再配合下自定義註解,這樣代碼就應該比較優雅了吧,說幹就幹。

自定義註解實現

  • 首先自定義了一個註解因爲要校驗參數
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
public @interface ParameterValidator {

}
  • 配置一個切面,解析有ParameterValidator註解的方法。
    然後通過切面獲取所有請求的參數,獲取參數之後就解析參數上面的註解。配置切面啥的都比較簡單,稍微複雜的就是反射解析參數了,因爲要涉及到請求參數的嵌套結構。二胖習慣性的面向百度編程能copy別人的代碼堅決不去自己寫。百度出來的基本上都是單層結構,簡單基本類型的對象,沒有涉及到是嵌套、級聯的類型的情趣參數。最後在github全球最大的同性交友網站)找了一圈也沒有找到合適的。既然拿來主義沒有結果那就只能哼次哼次的自己寫了,幸好自己以前學過點反射的知識。花了一個小時通過遞歸調用寫了個粗糙的版本,比較粗糙還有很多場景沒有考慮進去。不過基本可以滿足條件了部分代碼如下:
 public static void checkField(Object object, Class<?> aClass) throws IllegalAccessException {
        boolean primitive = isPrimitive(aClass);
        if (primitive) {
            return;
        }
        Field[] declaredFields = filterField(aClass.getDeclaredFields());
        for (Field field : declaredFields) {
            makeAccessible(field);
            // 校驗我們自定義註解
            MyNotBlank fieldAnnotation = field.getAnnotation(MyNotBlank.class);
            Object currentObject = field.get(object);
            if (Objects.nonNull(fieldAnnotation)) {
                if (StringUtils.isEmpty(currentObject)) {
                    throw  new IllegalArgumentException(field.getName()+":"+fieldAnnotation.message());
                }
            }
            if (!isJavaClass(field.getType())) {
                // 遞歸調用,有級聯參數時候
                checkField(currentObject);
            } else if (field.getType().isPrimitive()) {

            } else if (field.getType().isAssignableFrom(List.class)) {
                // 遞歸調用,解析list類型
                getList(field, currentObject);
            }
        }

    }

然後趕緊測試一波,還不錯基本功能實現了,能夠實現判空檢驗了,也可以實現級聯校驗了。效果如下:
在這裏插入圖片描述
不過這個現在支持類型爲基本類型和StringList
後續如果參數類型是數組、或者Map等等還得去解析。 這時候同事二狗從旁邊走過,看到二胖這麼認真的在敲代碼。

二狗:二胖你又在寫什麼bug啊。

二胖:在自己造個輪子,寫個通用的參數校驗。

二狗:這個現在市面上不是已經有現成的方案了嗎?jsr(Java Specification Requests)可以去了解下哦。

二胖:好的我馬上去查下資料。

jsr(Java Specification Requests) Java 規範提案

  • 說到jsr我們就得先了解下什麼是JCP(Java Community Process)

JCP(Java Community Process) 是一個開放的國際組織,主要由Java開發者以及被授權者組成,職能是發展和更新。

  • JSR又是個什麼東東列?

它是指向JCP提出新增一個標準化技術規範的正式請求。任何人都可以提交JSR,(如果你覺得自己牛逼你也可以提交一個)
以向Java平臺增添新的API和服務。JSR已成爲Java界的一個重要標準。

Bean Validation

Bean Validation 顧名思義是對 java Bean 的校驗,目前爲止,Java 對 Bean 的校驗有3個規範。

  • JSR-303 : Bean Validation
  • JSR 349 : Bean Validation 1.1
  • JSR 380 : Bean Validation 2.0

Hibernate-Validator

Hibernate ValidatorBean Validation 的參考實現 . Hibernate Validator 提供了 JSR 303 規範中所有內置 constraint 的實現,除此之外還有一些附加的 constraint

代碼實現

  • 如果項目的框架是 spring boot 的話,在 spring-boot-starter-web 中已經包含了 Hibernate-validator 的依賴(版本必須是2.3之前)。2.3以後的版本 spring-boot-starter-web已經去除了這個依賴,需要手動引入 Hibernate-validator依賴,詳細內容見官網描述
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • springboot項目的話直接引入
<dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.0.17.Final</version>
</dependency>

代碼演示:
方法前面這個註解@Valid是必須的,否則不生效哦。

    @PostMapping(value = "/save2")
    @ResponseBody
    public ResultViewModel save2(@Valid @RequestBody User user){
        boolean saveUser = saveUser(user);
        if (saveUser) {
            return ResultViewModelUtil.success();
        }
        return ResultViewModelUtil.error();
    }

實體類上標上需要校驗的規則註解就好了。

//被註釋的元素,值必須是一個字符串,不能爲null,且調用trim()後,長度必須大於0
@NotBlank(message = "")

//被註釋的元素,值不能爲null,但可以爲"空",用於基本數據類型的非空校驗上,而且被其標註的字段可以使用 @size、@Max、@Min 等對字段數值進行大小的控制
@NotNull(message = "")

//被註釋的的元素,值不能爲null,且長度必須大於0,一般用在集合類上面
@NotEmpty(message = "")

//被註釋的元素必須符合指定的正則表達式。
@Pattern(regexp = "", message = "")

//被註釋的元素的大小必須在指定的範圍內。
@Size(min =, max =)

//被註釋的元素,值必須是一個數字,且值必須大於等於指定的最小值
@Min(value = long以內的值, message = "")

//被註釋的元素,值必須是一個數字,且值必須小於等於指定的最大值
@Max(value = long以內的值, message = "")

//被註釋的元素,值必須是一個數字,其值必須大於等於指定的最小值
@DecimalMin(value = 可以是小數, message = "")

//被註釋的元素,值必須是一個數字,其值必須小於等於指定的最大值
@DecimalMax(value = 可以是小數, message = "")

//被註釋的元素,值必須爲null
@Null(message = "")

//被註釋的元素必須是一個數字,其值必須在可接受的範圍內
@Digits(integer =, fraction =)

//被註釋的元素,值必須爲true
@AssertTrue(message = "")

//被註釋的元素,值必須爲false
@AssertFalse(message = "")

//被註釋的元素必須是一個過去的日期
@Past(message = "")

//被註釋的元素必須是一個將來的日期
@Future(message = "")

//被註釋的元素必須是電子郵件地址
@Email(regexp = "", message = "")
//被註釋的元素必須在合適的範圍內
@Range(min =, max =, message = "")

//被註釋的字符串的大小必須在指定的範圍內
@Length(min =, max =, message = "")

唯一需要注意的點就是如果是級聯校驗的話需要在最外層加上@Valid
爲什麼需要在校驗的上一次標上@Valid這個註解,裏面的校驗纔會生效列?有知道的或者感興趣的可以去看看源碼給我留言哦。
在這裏插入圖片描述
然後在配置一個全局的異常捕獲器就好了,由於篇幅原因代碼就不貼了,代碼上傳到了github上。
校驗結果:
在這裏插入圖片描述

總結

  • Hibernate-Validator還可以自定義註解實現。
  • 還可以分組校驗(有這樣一種場景,新增用戶信息的時候,不需要驗證userId(因爲系統生成);修改的時候需要驗證userId,這時候可用用戶到validator的分組驗證功能)
  • 如果項目不是springboot的、比如使用的是Jfinal框架(這個是個國產框架大多數人能都不知道)、或者soa調用參數校驗的時候,這時候可以怎麼使用列?
  • 更多使用姿勢大家感興趣的可以去官網解鎖哦,不過日常開發的以上介紹基本就可以滿足需求了。
  • 二胖看到這豐富的api,以及炒雞簡單的用法,趕緊把自己寫的輪子給刪除了,立馬換上了這個Hibernate-Validator框架。重新修改提交後,leader的臉上終於露出了滿意的笑容。
  • 項目地址

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。
    在這裏插入圖片描述

參考
http://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html_single/#validator-gettingstarted(官網中文版本貼心吧
https://docs.jboss.org/hibernate/validator/6.1/reference/en-US/html_single/#validator-gettingstarted
https://juejin.im/post/5dd8d44c518825734e4cda22
https://www.cnblogs.com/mr-yang-localhost/p/7812038.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章