Spring Boot 參數(shù)校驗(yàn)以及分組校驗(yàn)的使用
一 前言
做web開發(fā)有一點(diǎn)很煩人就是要對(duì)前端輸入?yún)?shù)進(jìn)行校驗(yàn),基本上每個(gè)接口都要對(duì)參數(shù)進(jìn)行校驗(yàn),比如一些非空校驗(yàn)、格式校驗(yàn)等。如果參數(shù)比較少的話還是容易處理的一但參數(shù)比較多了的話代碼中就會(huì)出現(xiàn)大量的if-else語句。
使用這種方式雖然簡單直接,但是也有不好的地方,一是降低了開發(fā)效率,因?yàn)槲覀冃枰r?yàn)的參數(shù)會(huì)存在很多地方,并且不同地方會(huì)有重復(fù)校驗(yàn),其次降低了代碼可讀性,因?yàn)樵跇I(yè)務(wù)代碼中摻雜了太多額外工作的代碼。
所以我們可以使用validator組件來代替我們進(jìn)行不必要的coding操作。本文基于validator的介紹資料,也結(jié)合自己在項(xiàng)目中的實(shí)際使用經(jīng)驗(yàn)進(jìn)行了總結(jié),希望能幫到大家。
1 什么是validator
Bean Validation是Java定義的一套基于注解的數(shù)據(jù)校驗(yàn)規(guī)范,目前已經(jīng)從JSR 303的1.0版本升級(jí)到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08),已經(jīng)經(jīng)歷了三個(gè)版本 。需要注意的是,JSR只是一項(xiàng)標(biāo)準(zhǔn),它規(guī)定了一些校驗(yàn)注解的規(guī)范,但沒有實(shí)現(xiàn),比如@Null、@NotNull、@Pattern等,它們位于 javax.validation.constraints這個(gè)包下。而hibernate validator是對(duì)這個(gè)規(guī)范的實(shí)現(xiàn),并增加了一些其他校驗(yàn)注解,如 @NotBlank、@NotEmpty、@Length等,它們位于org.hibernate.validator.constraints這個(gè)包下。
如果我們的項(xiàng)目使用了Spring Boot,hibernate validator框架已經(jīng)集成在 spring-boot-starter-web中,所以無需再添加其他依賴。如果不是Spring Boot項(xiàng)目,需要添加如下依賴。
二 注解介紹
1 validator內(nèi)置注解
hibernate validator中擴(kuò)展定義了如下注解:
三 使用
使用起來比較簡單,都是使用注解方式使用。具體來說分為單參數(shù)校驗(yàn)、對(duì)象參數(shù)校驗(yàn),單參數(shù)校驗(yàn)就是controller接口按照單參數(shù)接收前端傳值,沒有封裝對(duì)象進(jìn)行接收,如果有封裝對(duì)象那就是對(duì)象參數(shù)校驗(yàn)。
1 單參數(shù)校驗(yàn)
單參數(shù)校驗(yàn)只需要在參數(shù)前添加注解即可,如下所示:
- public Result deleteUser(@NotNull(message = "id不能為空") Long id) { // do something}
但有一點(diǎn)需要注意,如果使用單參數(shù)校驗(yàn),controller類上必須添加@Validated注解,如下所示:
- @RestController@RequestMapping("/user")@Validated // 單參數(shù)校驗(yàn)需要加的注解public class UserController { // do something}
2 對(duì)象參數(shù)校驗(yàn)
對(duì)象參數(shù)校驗(yàn)使用時(shí),需要先在對(duì)象的校驗(yàn)屬性上添加注解,然后在Controller方法的對(duì)象參數(shù)前添加@Validated 注解,如下所示:
- public Result addUser(@Validated UserAO userAo) { // do something}public class UserAO { @NotBlank private String name; @NotNull private Integer age; ……}
注解分組
在對(duì)象參數(shù)校驗(yàn)場(chǎng)景下,有一種特殊場(chǎng)景,同一個(gè)參數(shù)對(duì)象在不同的場(chǎng)景下有不同的校驗(yàn)規(guī)則。比如,在創(chuàng)建對(duì)象時(shí)不需要傳入id字段(id字段是主鍵,由系統(tǒng)生成,不由用戶指定),但是在修改對(duì)象時(shí)就必須要傳入id字段。在這樣的場(chǎng)景下就需要對(duì)注解進(jìn)行分組。
1)組件有個(gè)默認(rèn)分組Default.class, 所以我們可以再創(chuàng)建一個(gè)分組UpdateAction.class,如下所示:
public interface UpdateAction {}
2)在參數(shù)類中需要校驗(yàn)的屬性上,在注解中添加groups屬性:
- public class UserAO { @NotNull(groups = UpdateAction.class, message = "id不能為空") private Long id; @NotBlank private String name; @NotNull private Integer age; ……}
如上所示,就表示只在UpdateAction分組下校驗(yàn)id字段,在默認(rèn)情況下就會(huì)校驗(yàn)name字段和age字段。
然后在controller的方法中,在@Validated注解里指定哪種場(chǎng)景即可,沒有指定就代表采用Default.class,采用其他分組就需要顯示指定。如下代碼便表示在addUser()接口中按照默認(rèn)情況進(jìn)行參數(shù)校驗(yàn),在updateUser()接口中按照默認(rèn)情況和UpdateAction分組對(duì)參數(shù)進(jìn)行共同校驗(yàn)。
- public Result addUser(@Validated UserAO userAo) { // do something}
- public Result updateUser(@Validated({Default.class, UpdateAction.class}) UserAO userAo) { // do something}
對(duì)象嵌套
如果需要校驗(yàn)的參數(shù)對(duì)象中還嵌套有一個(gè)對(duì)象屬性,而該嵌套的對(duì)象屬性也需要校驗(yàn),那么就需要在該對(duì)象屬性上增加@Valid注解。
- public class UserAO { @NotNull(groups = UpdateAction.class, message = "id不能為空") private Long id; @NotBlank private String name; @NotNull private Integer age; @Valid private Phone phone; ……}public class Phone { @NotBlank private String operatorType; @NotBlank private String phoneNum;}
3 錯(cuò)誤消息的捕獲
參數(shù)校驗(yàn)失敗后會(huì)拋出異常,我們只需要在全局異常處理類中捕獲參數(shù)校驗(yàn)的失敗異常,然后將錯(cuò)誤消息添加到返回值中即可。捕獲異常的方法如下所示,返回值Result是我們系統(tǒng)自定義的返回值類。
- @RestControllerAdvice(basePackages= {"com.alibaba.dc.controller","com.alibaba.dc.service"})public class GlobalExceptionHandler { @ExceptionHandler(value = {Throwable.class}) Result handleException(Throwable e, HttpServletRequest request){ // 異常處理 }}
需要注意的是,如果缺少參數(shù)拋出的異常是MissingServletRequestParameterException,單參數(shù)校驗(yàn)失敗后拋出的異常是ConstraintViolationException,get請(qǐng)求的對(duì)象參數(shù)校驗(yàn)失敗后拋出的異常是BindException,post請(qǐng)求的對(duì)象參數(shù)校驗(yàn)失敗后拋出的異常是MethodArgumentNotValidException,不同異常對(duì)象的結(jié)構(gòu)不同,對(duì)異常消息的提取方式也就不同。如下圖所示:
1)MissingServletRequestParameterException
- if(e instanceof MissingServletRequestParameterException){ Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); String msg = MessageFormat.format("缺少參數(shù){0}", ((MissingServletRequestParameterException) e).getParameterName()); result.setMessage(msg); return result;}
2)ConstraintViolationException異常
- if(e instanceof ConstraintViolationException){ // 單個(gè)參數(shù)校驗(yàn)異常 Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); Set<ConstraintViolation<?>> sets = ((ConstraintViolationException) e).getConstraintViolations(); if(CollectionUtils.isNotEmpty(sets)){ StringBuilder sb = new StringBuilder(); sets.forEach(error -> { if (error instanceof FieldError) { sb.append(((FieldError)error).getField()).append(":"); } sb.append(error.getMessage()).append(";"); }); String msg = sb.toString(); msg = StringUtils.substring(msg, 0, msg.length() -1); result.setMessage(msg); } return result;}
3)BindException異常
- if (e instanceof BindException){ // get請(qǐng)求的對(duì)象參數(shù)校驗(yàn)異常 Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); List<ObjectError> errors = ((BindException) e).getBindingResult().getAllErrors(); String msg = getValidExceptionMsg(errors); if (StringUtils.isNotBlank(msg)){ result.setMessage(msg); } return result;}
- private String getValidExceptionMsg(List<ObjectError> errors) { if(CollectionUtils.isNotEmpty(errors)){ StringBuilder sb = new StringBuilder(); errors.forEach(error -> { if (error instanceof FieldError) { sb.append(((FieldError)error).getField()).append(":"); } sb.append(error.getDefaultMessage()).append(";"); }); String msg = sb.toString(); msg = StringUtils.substring(msg, 0, msg.length() -1); return msg; } return null;}
4)MethodArgumentNotValidException異常
- if (e instanceof MethodArgumentNotValidException){ // post請(qǐng)求的對(duì)象參數(shù)校驗(yàn)異常 Result result = Result.buildErrorResult(ErrorCodeEnum.PARAM_ILLEGAL); List<ObjectError> errors = ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors(); String msg = getValidExceptionMsg(errors); if (StringUtils.isNotBlank(msg)){ result.setMessage(msg); } return result;}