快速上手:SpringBoot自定義請求參數校驗

最近在工作中遇到寫一些API,這些API的請求參數非常多,嵌套也非常複雜,如果參數的校驗代碼全部都手動去實現,寫起來真的非常痛苦。正好Spring輪子裏面有一個Validation,這裏記錄一下怎麼使用,以及怎麼自定義它的返回結果。

一、Bean Validation基本概念

Bean Validation是Java中的一項標準,它通過一些註解表達了對實體的限制規則。通過提出了一些API和擴展性的規範,這個規範是沒有提供具體實現的,希望能夠Constrain once, validate everywhere。現在它已經發展到了2.0,兼容Java8。

hibernate validation實現了Bean Validation標準,裏面還增加了一些註解,在程序中引入它我們就可以直接使用。

Spring MVC也支持Bean Validation,它對hibernate validation進行了二次封裝,添加了自動校驗,並將校驗信息封裝進了特定的BindingResult類中,在SpringBoot中我們可以添加implementation('org.springframework.boot:spring-boot-starter-validation')引入這個庫,實現對bean的校驗功能。

二、基本用法

gradle dependencies如下:

快速上手:SpringBoot自定義請求參數校驗


定義一個示例的Bean,例如下面的User.java。

快速上手:SpringBoot自定義請求參數校驗


在name屬性上,添加@NotBlank和@Size(max=10)的註解,表示User對象的name屬性不能爲字符串且長度不能超過10個字符。

然後我們暫時不添加任何多餘的代碼,直接寫一個UserController對外提供一個RESTful的GET接口,注意接口的參數用到了@Validated註解。

快速上手:SpringBoot自定義請求參數校驗


啓動SpringBoot程序,發一個測試請求看一下:

http://127.0.0.1:8080/validation/get?name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

返回的結果是,注意此時的HTTP STATUS CODE = 400:

快速上手:SpringBoot自定義請求參數校驗


此時已經可以實現參數的校驗了,但是返回的結果不太友好,下面看一下怎麼定製返回的消息。在定製返回結果前,先看下一下內置的校驗註解有哪些,在這裏我不一個個去貼了,寫代碼的時候根據需要進入到源碼裏面去看即可。

快速上手:SpringBoot自定義請求參數校驗


早期Spring版本中,都是在Controller的方法中添加Errors/BindingResult參數,由Spring注入Errors/BindingResult對象,再在Controller中手寫校驗邏輯實現校驗。新版本提供註解的方式(Controller上面bean加一個@Validated註解),將校驗邏輯和Controller分離。

三、自定義校驗

3.1 自定義註解

顯然除了自帶的NotNull、NotBlank、Size等註解,實際業務上還會需要特定的校驗規則。

假設我們有一個參數address,必須以Beijing開頭,那我們可以定義一個註解和一個自定義的Validator。

快速上手:SpringBoot自定義請求參數校驗


然後在User.java中增加一個address屬性,並給它加上上面這個自定義的註解,這裏我們定義了一個可以傳入start參數的註解,表示應該以什麼開頭。

快速上手:SpringBoot自定義請求參數校驗


除了定義可以作用於屬性的註解外,其實還可以定義作用於class的註解(@Target({TYPE})),用於校驗class的實例。

3.2 自定義Validator

第一步,實現一個Validator。(這種方法不需要我們的bean裏面有任何註解之類的東西)

快速上手:SpringBoot自定義請求參數校驗


第二步,修改Controller代碼,注入上面的UserValidator實例,並給Controller的方法參數加上@Validated註解,即可完成和前面自定義註解一樣的校驗功能。

快速上手:SpringBoot自定義請求參數校驗


這個方法和自定義註解的區別在於不需要在Bean裏面添加註解,並且可以更加靈活的把一個Bean裏面所有的Field的校驗代碼都搬到一起,而不是每一個屬性都去加註解,如果校驗的屬性非常多,且默認註解的能力又不夠的話,這種方式也是不錯的,可以避免大量的自定義註解。

3.3 以編程的方式校驗(手動)

這種方式可以算是原始的Hibernate-Validation的方式。直接看代碼,這裏有一個比較不同的是,可以使用Hibernate-Validation的Fail fast mode。因爲前面的方式,都將所有的參數都驗證完了,再把錯誤返回。有時我們希望遇到一個參數錯誤,就立即返回。

設置fast-fail爲true可以達到這個目的。不過貌似不能再用@Validated註解方法參數了,而是要用ValidatorFactory創建Validator。

在實際開發中,不必每次都編寫代碼創建Validator,可以採用@Configuration的方式創建,然後再@Autowired注入到每個需要使用Validator的Controller當中。

快速上手:SpringBoot自定義請求參數校驗


3.4 定義分組校驗

有的時候,我們會有兩個不同的接口,但是會使用到同一個Bean來作爲VO(意思是兩個接口的URI不同,但參數中都用到了同一個Bean)。而在不同的接口上,對Bean的校驗需求可能不一樣,比如接口2需要校驗studentId,而接口1不需要。那麼此時就可以用到校驗註解的分組groups。

快速上手:SpringBoot自定義請求參數校驗


到這裏,也可以帶一嘴Valid和Validated註解的區別,其代碼註釋寫着後者是對前者的一個擴展,支持了group分組的功能。

3.5 定製返回碼和消息

第二節中定義了一個ServiceResponse,其實作爲一個開放的API,不論用戶傳入任何參數,返回的結果都應該是預先定義好的格式,並且可以寫明在接口文檔中,即使發生了校驗失敗,應該返回一個包含錯誤碼code(發生錯誤時一般大於0)和message字段。

快速上手:SpringBoot自定義請求參數校驗


的結果,而HTTP STATUS CODE一直都是200。

爲了實現這個目的,我們加一個全局異常處理方法。

快速上手:SpringBoot自定義請求參數校驗


在上面的方法中,我們處理了BindException(非請求body參數,例如@RequestParam接收的)和MethodArgumentNotValidException(請求body裏面的參數,例如@RequestBody接收的),這兩類Exception裏面都有一個BindingResult對象,它裏面有一個包裝成FieldError的List,保存着Bean對象出現錯誤的Field等信息。

取出它裏面defaultMessage,放到統一的ServiceResponse返回即可實現返回碼和消息的定製。由於消息內容是有註解默認的DefaultMessage決定的,爲了按照自定義的描述返回,在Bean對象的註解上需要手動賦值爲希望返回的消息內容。

快速上手:SpringBoot自定義請求參數校驗


這樣當name參數長度超過10時,就會返回

快速上手:SpringBoot自定義請求參數校驗


這裏的FieldError fieldError = ex.getFieldError();只會隨機返回一個出錯的屬性,如果Bean對象的多個屬性都出錯了,可以調用ex.getFieldErrors()來獲得,這裏也可以看到Spring Validation在參數校驗時不會在第一次碰到參數錯誤時就返回,而是會校驗完成所有的參數。

如果不想手動編程去校驗,那麼這裏可以只讀取一個隨機的FieldError,返回它的錯誤消息即可。

3.6 更加細緻的返回碼和消息

其實還有一種比較典型的自定義返回,就是錯誤碼(code)和消息(message)是一一對應的,比如:

  • 51001:字符串長度過長

  • 51002:參數取值過大

這種情況比較特殊,一般當參數錯誤的時候,會返回一個整體的參數錯誤的錯誤碼,然後攜帶參數的錯誤信息。但有時,業務上就要不同的參數錯誤,既要錯誤碼不同,錯誤信息也要不同。我想了下,有兩種思路。

  • 第一種:通過message同時包含錯誤碼和錯誤信息,在全局異常捕獲方法中,再把它們拆開。

  • 第二種:手動校驗,拋出自定義的Exception(裏面帶有code、message)。手動校驗這裏,如果每一個Controller都去寫一遍,確實比較費勁,可以結合AOP來實現,或者抽出一個基類BaseController的方式。

四、小結

其實在實際的工作中,肯定還有更復雜的校驗邏輯,但是不一定非要都用框架去實現,框架裏面的實現(比如註解)應該是一個比較簡單通用的校驗,能夠達到複用,減少重複的勞動。而更加複雜的邏輯校驗,一定是存在具體業務當中的,最好是在業務代碼裏面實現。

還有一點需要注意,Spring Validation的isValid方法,如果返回false,那麼Controller不再會被調用,而是直接返回。如果你在Controller上面加了AOP進行接口調用統計的話,可能會漏掉。這個時候,我們不應該讓Controller不調用,建議這種情況在AOP裏面對Controller的參數切面進行校驗後,拋出統一的業務異常。

覺得文章不錯就給小老弟點個關注吧!加入我的個人粉絲羣(Java架構技術棧:644872653)即可領取一份Java面試寶典《Java核心知識點整理.pdf》和《Java208道面試題(含答案)》喔!


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