1. 程式人生 > >引數校驗坎坷之路

引數校驗坎坷之路

### 背景 最近端午好久沒有和二胖聚一聚了,於是約了二胖到人民廣場去宰他一頓,正好最近他跳槽加薪了。
**我**:二胖聽說你最近跳槽了,並且還是從傳統軟體公司跳到了網際網路公司,工資是不是漲了一點啊,今天你請客哈。
**二胖**:別說了,工資是漲了點,但是價效比反而變低了,以前到點就下班,現在下班到家都快12點了。
**我**:新公司怎麼樣還適應嗎?除了上班時間久點。
**二胖**:哎,這個還真稍微有點不適應,這不是剛進去沒啥事,`leader`就給我安排了一個簡單的使用者儲存功能(引數校驗),原來以前公司個把小時就做好了的功能,在這新公司硬是折騰了兩三天,真是苦不堪言。我改了好幾個版本最終`leader`才滿意的點了點頭。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200627001236873.png) ### 介面裸奔 - 按照二胖在以前公司的寫法再傳統公司反正系統都是服務內部人員的,在後端寫引數校驗是不存在的事情,完全信賴前端傳過來的內容。這不寫完程式碼自測一把發現可以儲存資料,就屁顛屁顛的發起程式碼`review`了(二胖在以前的公司程式碼`review`是不存在的,只要功能實現就好了)。正好`leader`今天有點時間,看到新同事提交的程式碼看看寫的怎麼樣。 看著這個裸奔的介面,`leader`把二胖叫了過去,語重心長的跟二胖說道:"你這個引數校驗不寫寫嗎?不怕人家攻擊你的介面嗎?這裡不校驗,直接用,不怕引入sql注入嗎?這裡不校驗下郵箱是否符合格式嗎?這個判空也不寫,不怕大量的空指標,服務熔斷嗎?..."。面對`leader`的拼命十三問,二胖心想試用期怕是有點難過哦?只能低著頭回到工位重新按照`leader`的教育整改起來,然後又重新提交了。 ### 引數校驗if判斷 `leader`看了看說到:“這次程式碼比上次好多了,功能基本沒啥問題了,但是這一塊程式碼是不是可以在優化下,這樣寫不是很優雅” ```java 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`嗎?再配合下自定義註解,這樣程式碼就應該比較優雅了吧,說幹就幹。 ### 自定義註解實現 - 首先自定義了一個註解因為要校驗引數 ```java @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD,ElementType.METHOD}) public @interface ParameterValidator { } ``` - 配置一個切面,解析有`ParameterValidator`註解的方法。 然後通過切面獲取所有請求的引數,獲取引數之後就解析引數上面的註解。配置切面啥的都比較簡單,稍微複雜的就是反射解析引數了,因為要涉及到請求引數的巢狀結構。二胖習慣性的面向百度程式設計能`copy`別人的程式碼堅決不去自己寫。百度出來的基本上都是單層結構,簡單基本型別的物件,沒有涉及到是巢狀、級聯的型別的情趣引數。最後在`github`(**全球最大的同性交友網站**)找了一圈也沒有找到合適的。既然拿來主義沒有結果那就只能哼次哼次的自己寫了,幸好自己以前學過點反射的知識。花了一個小時通過遞迴呼叫寫了個粗糙的版本,比較粗糙還有很多場景沒有考慮進去。不過基本可以滿足條件了部分程式碼如下: ```java 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); } } } ``` 然後趕緊測試一波,還不錯基本功能實現了,能夠實現判空檢驗了,也可以實現級聯校驗了。效果如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200628231044461.png) 不過這個現在支援型別為基本型別和`String`、`List`的 後續如果引數型別是陣列、或者`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 Validator` 是 `Bean 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`依賴,詳細內容見[官網描述](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes#validation-starter-no-longer-included-in-web-starters) ```java ``` - 非`springboot`專案的話直接引入 ```java ``` 程式碼演示: 方法前面這個註解`@Valid`是必須的,否則不生效哦。 ```java @PostMapping(value = "/save2") @ResponseBody public ResultViewModel save2(@Valid @RequestBody User user){ boolean saveUser = saveUser(user); if (saveUser) { return ResultViewModelUtil.success(); } return ResultViewModelUtil.error(); } ``` 實體類上標上需要校驗的規則註解就好了。 ```java //被註釋的元素,值必須是一個字串,不能為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`這個註解,裡面的校驗才會生效列?有知道的或者感興趣的可以去看看原始碼給我留言哦。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020062723172130.png) 然後在配置一個全域性的異常捕獲器就好了,由於篇幅原因程式碼就不貼了,程式碼上傳到了`github`上。 校驗結果: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200627234105132.png) #### 總結 - `Hibernate-Validator`還可以自定義註解實現。 - 還可以分組校驗(有這樣一種場景,新增使用者資訊的時候,不需要驗證`userId`(因為系統生成);修改的時候需要驗證`userId`,這時候可用使用者到`validator`的分組驗證功能) - 如果專案不是`springboot`的、比如使用的是[Jfinal框架](http://www.jfinal.com/)(這個是個國產框架大多數人能都不知道)、或者`soa`呼叫引數校驗的時候,這時候可以怎麼使用列? - 更多使用姿勢大家感興趣的可以去[官網解鎖](https://docs.jboss.org/hibernate/validator/6.1/reference/en-US/html_single/#validator-gettingstarted)哦,不過日常開發的以上介紹基本就可以滿足需求了。 - 二胖看到這豐富的`api`,以及炒雞簡單的用法,趕緊把自己寫的輪子給刪除了,立馬換上了這個`Hibernate-Validator`框架。重新修改提交後,`leader`的臉上終於露出了滿意的笑容。 - [專案地址](https://github.com/worit1/validator) ### 結束 - 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。 - 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。 - 感謝您的閱讀,十分歡迎並感謝您的關注。 參考 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/78120