SpringValid優雅校驗入參

阿呆很呆非常呆發表於2020-09-23

一、簡介

  後臺業務入口類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());
    }
}

 

  

相關文章