一、簡介
后台業務入口類Controller,對於入參的合法性校驗,可以簡單粗暴的寫出一堆的 if 判斷,如下:
@RestController @RequestMapping("user") public class UserController { @PostMapping("saveUser") public String saveUser(UserInfoVo userInfoVo){ if(StrUtil.isBlank(userInfoVo.getUserName())){ return "userName is not null"; } if(StrUtil.isBlank(userInfoVo.getPwd())){ return "pwd is not null"; } return "save success"; } }
二、重要說明
2.1、springboot在2.3之后,spring-boot-starter-web的依賴項已經去除了validate依賴,推薦導入依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2.2、關於 @Valid 和 @Validated
@Validated 是Spring Validation驗證框架對JSR-303規范的一個擴展, javax提供@Valid 是標准的JSR-303規范。使用基本無區別,但是在Group分組使用上還是使用 @Validated方便。
嵌套驗證上,必須在待驗證的vo中的嵌套實體屬性上增加@Valid。
三、實驗出真知
3.1、牛刀小試
定義VO,在需要校驗的字段上加上相應注解
@Data
public class UserInfoVo {
@NotBlank(message = "userName is not null")
private String userName;
@NotNull(message = "age is not null")
private Integer age;
@NotBlank(message = "pwd is not null")
private String pwd;
}
Controller入參加上@Valid、校驗結果BindingResult:
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("saveUser")
public String saveUser(@Valid UserInfoVo userInfoVo, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return bindingResult.getAllErrors().get(0).getDefaultMessage();
}
return "save success";
}
}
通過工具訪問,可看到如果入參不符合,會有相應message返回
以上就完成了一個最簡單的優雅校驗過程,其中內置常用校驗類型:
空檢查
@Null 驗證對象是否為null
@NotNull 驗證對象是否不為null, 無法查檢長度為0的字符串
@NotBlank 檢查約束字符串是不是Null還有被Trim的長度是否大於0,只對字符串,且會去掉前后空格.
@NotEmpty 檢查約束元素是否為NULL或者是EMPTY.
Booelan檢查
@AssertTrue 驗證 Boolean 對象是否為 true
@AssertFalse 驗證 Boolean 對象是否為 false
長度檢查
@Size(min=, max=) 驗證對象(Array,Collection,Map,String)長度是否在給定的范圍之內
@Length(min=, max=) Validates that the annotated string is between min and max included.
日期檢查
@Past 驗證 Date 和 Calendar 對象是否在當前時間之前
@Future 驗證 Date 和 Calendar 對象是否在當前時間之后
@Pattern 驗證 String 對象是否符合正則表達式的規則
數值檢查,建議使用在Stirng,Integer類型,不建議使用在int類型上,因為表單值為“”時無法轉換為int,但可以轉換為Stirng為"",Integer為null
@Min 驗證 Number 和 String 對象是否大等於指定的值
@Max 驗證 Number 和 String 對象是否小等於指定的值
@DecimalMax 被標注的值必須不大於約束中指定的最大值. 這個約束的參數是一個通過BigDecimal定義的最大值的字符串表示.小數存在精度
@DecimalMin 被標注的值必須不小於約束中指定的最小值. 這個約束的參數是一個通過BigDecimal定義的最小值的字符串表示.小數存在精度
@Digits 驗證 Number 和 String 的構成是否合法
@Digits(integer=,fraction=) 驗證字符串是否是符合指定格式的數字,interger指定整數精度,fraction指定小數精度。
@Range(min=, max=) Checks whether the annotated value lies between (inclusive) the specified minimum and maximum.
@Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;
@CreditCardNumber信用卡驗證
@Email 驗證是否是郵件地址,如果為null,不進行驗證,算通過驗證。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)
3.2、第一次改版
入參很多的情況下,可能會同時產生多個不同的錯誤校驗,那么如果每次只是返回一個錯誤提示,每次客戶端改一個,那么體驗是極差的。基於此,封裝返回類,提供統一返回。
@Data @NoArgsConstructor @RequiredArgsConstructor public class ResponseData<T> { @NonNull private Integer code; @NonNull private String message; private T data; public static ResponseData success() { return new ResponseData(HttpStatus.OK.value(), "SUCCESS"); } public static ResponseData success(Object data) { ResponseData entity = success(); entity.setData(data); return entity; } public static ResponseData fail(Integer code, String msg) { return new ResponseData(code, msg); } public static ResponseData fail(Integer code, String msg, Object data) { ResponseData entity = fail(code, msg); entity.setData(data); return entity; } }
@RestController @RequestMapping("user") public class UserController { @PostMapping("saveUser") public ResponseData saveUser(@Valid UserInfoVo userInfoVo, BindingResult bindingResult){ if(bindingResult.hasErrors()){ List<String> collect = bindingResult.getFieldErrors().stream().map(item -> item.getDefaultMessage()).collect(Collectors.toList()); return ResponseData.fail(900, "req param invalid", collect); } return ResponseData.success(); } }
3.3、第二次改版
上述改版,已經能夠一次性的返回所有未校驗通過異常,但是,每個方法中都這么來一遍,還是挺麻煩的。下面利用統一異常處理參數校驗,改造完成后Controller中專注於業務處理即可
@RestController @RequestMapping("user") public class UserController { @PostMapping("saveUser") public ResponseData saveUser(@Valid UserInfoVo userInfoVo){ return ResponseData.success(); } } @RestControllerAdvice public class GlobalExceptionHandler { private static final Integer BAD_REQUEST_CODE = 900; private static final String BAD_REQUEST_MSG = "req param invalid"; @ExceptionHandler(BindException.class) public ResponseData bindExceptionHandler(BindException exception){ List<String> collect = exception.getAllErrors().stream().map(item -> item.getDefaultMessage()) .collect(Collectors.toList()); return ResponseData.fail(BAD_REQUEST_CODE, BAD_REQUEST_MSG, collect); } @ExceptionHandler(Exception.class) public ResponseData exceptionHandler(Exception exception){ return ResponseData.fail(500, exception.getMessage()); } }
3.4、分組校驗
通常,存在場景:name參數在注冊接口必須非空,但是修改接口無所謂。那么此時,分組group很好解決問題
/** * @author cfang 2020/9/23 10:47 * * 關於 extends Default * 繼承 Default 的話,所有定義校驗規則的都會校驗 * 不繼承的話,則只校驗加了group信息的校驗字段 */ public interface UserGroup extends Default{ } @Data public class UserInfoVo { @NotBlank(message = "userName is not null", groups = UserGroup.class) private String userName; @NotNull(message = "age is not null") private Integer age; @NotBlank(message = "pwd is not null") private String pwd; } @RestController @RequestMapping("user") public class UserController { @PostMapping("saveUser") public ResponseData saveUser(@Validated({UserGroup.class}) UserInfoVo userInfoVo){ return ResponseData.success(); } @PostMapping("updateUser") public ResponseData updateUser(@Valid UserInfoVo userInfoVo){ return ResponseData.success(); } @InitBinder public void init(HttpServletRequest request, DataBinder dataBinder){ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); //是否校驗轉化日期格式,true-轉化日期,false-參數錯誤則直接異常。eg. 2020-55-10, true->2024-10-10 , false-異常報錯。默認值true dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); } }
3.5、遞歸校驗
@Data public class AddressInfoVo { @NotBlank(message = "street is not null") private String street; } @Data public class UserInfoVo { @NotBlank(message = "userName is not null", groups = UserGroup.class) private String userName; @NotNull(message = "age is not null") private Integer age; @NotBlank(message = "pwd is not null") private String pwd; @Past(message = "predate is invalid") @NotNull(message = "predate is not null") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone="GMT+8") private Date preDate; @Valid private AddressInfoVo infoVo; } @RestController @RequestMapping("user") public class UserController { @PostMapping("saveUser") public ResponseData saveUser(@Validated({UserGroup.class}) @RequestBody UserInfoVo userInfoVo){ return ResponseData.success(); } @PostMapping("updateUser") public ResponseData updateUser(@Valid @RequestBody UserInfoVo userInfoVo){ return ResponseData.success(); } @InitBinder public void init(HttpServletRequest request, DataBinder dataBinder){ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); } } @RestControllerAdvice public class GlobalExceptionHandler { private static final Integer BAD_REQUEST_CODE = 900; private static final String BAD_REQUEST_MSG = "req param invalid"; @ExceptionHandler(BindException.class) public ResponseData bindExceptionHandler(BindException exception){ List<String> collect = exception.getAllErrors().stream().map(item -> item.getDefaultMessage()) .collect(Collectors.toList()); return ResponseData.fail(BAD_REQUEST_CODE, BAD_REQUEST_MSG, collect); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseData argInvalidExceptionHandler(MethodArgumentNotValidException exception){ List<String> collect = exception.getBindingResult().getFieldErrors().stream().map(item -> item.getDefaultMessage()) .collect(Collectors.toList()); return ResponseData.fail(BAD_REQUEST_CODE, BAD_REQUEST_MSG, collect); } @ExceptionHandler(Exception.class) public ResponseData exceptionHandler(Exception exception){ return ResponseData.fail(500, exception.getMessage()); } }