10.02 這麼寫參數校驗 Validator 就不會被勸退了

優質文章,及時送達

这么写参数校验 Validator 就不会被劝退了

作者 | 錦成同學

鏈接 | juejin.im/post/5d3fbeb46fb9a06b317b3c48

很痛苦遇到大量的參數進行校驗,在業務中還要拋出異常或者不斷的返回異常時的校驗信息,在代碼中相當冗長, 充滿了if-else這種校驗代碼,今天我們就來學習 Spring 的javax.validation 註解式參數校驗。

1. javax.validation的一系列註解可以幫我們完成參數校驗,免去繁瑣的串行校驗

不然我們的代碼就像下面這樣:

<code>@PostMapping("/save/serial")/<code><code> public Object save(@RequestBody UserVO userVO) {/<code><code> String mobile = userVO.getMobile;/<code>
<code> //手動逐個 參數校驗~ 寫法/<code><code> if (StringUtils.isBlank(mobile)) {/<code><code> return RspDTO.paramFail("mobile:手機號碼不能為空");/<code><code> } else if (!Pattern.matches("^[1][3,4,5,6,7,8,9][0-9]{9}$", mobile)) {/<code><code> return RspDTO.paramFail("mobile:手機號碼格式不對");/<code><code> }/<code>
<code> //拋出自定義異常等~寫法/<code><code> if (StringUtils.isBlank(userVO.getUsername)) {/<code><code> throw new BizException(Constant.PARAM_FAIL_CODE, "用戶名不能為空");/<code><code> }/<code>
<code> // 比如寫一個map返回/<code><code> if (StringUtils.isBlank(userVO.getSex)) {/<code><code> Map<string> result = new HashMap<>(5);/<string>/<code><code> result.put("code", Constant.PARAM_FAIL_CODE);/<code><code> result.put("msg", "性別不能為空");/<code><code> return result;/<code><code> }/<code><code> //.........各種寫法 .../<code><code> userService.save(userVO);/<code><code> return RspDTO.success;/<code><code>}/<code>

這被大佬看見,一定說,都9102了還這麼寫,然後被勸退了…..

2. 什麼是javax.validation

JSR303 是一套JavaBean參數校驗的標準,它定義了很多常用的校驗註解,我們可以直接將這些註解加在我們JavaBean的屬性上面(面向註解編程的時代),就可以在需要校驗的時候進行校驗了,在SpringBoot中已經包含在starter-web中,再其他項目中可以引用依賴,並自行調整版本:

<code> /<code><code> <dependency>/<code><code> <groupid>javax.validation/<groupid>/<code><code> <artifactid>validation-api/<artifactid>/<code><code> <version>1.1.0.Final/<version>/<code><code> /<code><code> /<code><code> <dependency>/<code><code> <groupid>org.hibernate/<groupid>/<code><code> <artifactid>hibernate-validator/<artifactid>/<code><code> <version>5.2.0.Final/<version>/<code><code> /<code>

這麼寫參數校驗 Validator 就不會被勸退了

3. 註解說明

  • @Not:不能為,但可以為empty(""," "," ")

  • @NotEmpty:不能為,而且長度必須大於0 (" "," ")

  • @NotBlank:只能作用在String上,不能為,而且調用trim後,長度必須大於0("test") 即:必須有實際字符

这么写参数校验 Validator 就不会被劝退了
这么写参数校验 Validator 就不会被劝退了

此處只列出Hibernate Validator提供的大部分驗證約束註解,請參考hibernate validator官方文檔瞭解其他驗證約束註解和進行自定義的驗證約束註解定義。

實戰演練

話不多說,直接走實踐路線,同樣使用的是SpringBoot的快速框架

詳細代碼見:

https://github.com/leaJone/mybot

1. @Validated 聲明要檢查的參數

<code> /**/<code><code> * 走參數校驗註解/<code><code> */<code><code> * @param userDTO/<code><code> * @return/<code><code> *//<code><code> @PostMapping("/save/valid")/<code><code> public RspDTO save(@RequestBody @Validated UserDTO userDTO) {/<code><code> userService.save(userDTO);/<code><code> return RspDTO.success;/<code><code> }/<code>

2. 對參數的字段進行註解標註

<code>@Data/<code><code>public class UserDTO implements Serializable {/<code>
<code> private static final long serialVersionUID = 1L;/<code>
<code> /*** 用戶ID*//<code><code> @Not(message = "用戶id不能為空")/<code><code> private Long userId;/<code>
<code> /** 用戶名*//<code><code> @NotBlank(message = "用戶名不能為空")/<code><code> @Length(max = 20, message = "用戶名不能超過20個字符")/<code><code> @Pattern(regexp = "^[\\\\\\u4E00-\\\\\\u9FA5A-Za-z0-9\\\\*]*$", message = "用戶暱稱限制:最多20字符,包含文字、字母和數字")/<code><code> private String username;/<code>
<code> /** 手機號*//<code><code> @NotBlank(message = "手機號不能為空")/<code><code> @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手機號格式有誤")/<code><code> private String mobile;/<code>
<code> /**性別*//<code><code> private String sex;/<code>
<code> /** 郵箱*//<code><code> @NotBlank(message = "聯繫郵箱不能為空")/<code><code> @Email(message = "郵箱格式不對")/<code><code> private String email;/<code>
<code> /** 密碼*//<code><code> private String password;/<code>
<code> /*** 創建時間 *//<code><code> @Future(message = "時間必須是將來時間")/<code><code> private Date createTime;/<code>
<code>}/<code>

3. 在全局校驗中增加校驗異常

MethodArgumentNotValidException是springBoot中進行綁定參數校驗時的異常,需要在springBoot中處理,其他需要處理ConstraintViolationException異常進行處理.

為了優雅一點,我們將參數異常,業務異常,統一做了一個全局異常,將控制層的異常包裝到我們自定義的異常中.

為了優雅一點,我們還做了一個統一的結構體,將請求的code,和msg,data一起統一封裝到結構體中,增加了代碼的複用性.

<code>@RestControllerAdvice/<code><code>public class GlobalExceptionHandler {/<code>
<code> private Logger logger = LoggerFactory.getLogger(getClass);/<code>
<code> private static int DUPLICATE_KEY_CODE = 1001;/<code><code> private static int PARAM_FAIL_CODE = 1002;/<code><code> private static int VALIDATION_CODE = 1003;/<code>
<code> /**/<code><code> * 處理自定義異常/<code><code> *//<code><code> @ExceptionHandler(BizException.class)/<code><code> public RspDTO handleRRException(BizException e) {/<code><code> logger.error(e.getMessage, e);/<code><code> return new RspDTO(e.getCode, e.getMessage);/<code><code> }/<code>
<code> /**/<code><code> * 方法參數校驗/<code><code> *//<code><code> @ExceptionHandler(MethodArgumentNotValidException.class)/<code><code> public RspDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {/<code><code> logger.error(e.getMessage, e);/<code><code> return new RspDTO(PARAM_FAIL_CODE, e.getBindingResult.getFieldError.getDefaultMessage);/<code><code> }/<code>
<code> /**/<code><code> * ValidationException/<code><code> *//<code><code> @ExceptionHandler(ValidationException.class)/<code><code> public RspDTO handleValidationException(ValidationException e) {/<code><code> logger.error(e.getMessage, e);/<code><code> return new RspDTO(VALIDATION_CODE, e.getCause.getMessage);/<code><code> }/<code>
<code> /**/<code><code> * ConstraintViolationException/<code><code> *//<code><code> @ExceptionHandler(ConstraintViolationException.class)/<code><code> public RspDTO handleConstraintViolationException(ConstraintViolationException e) {/<code><code> logger.error(e.getMessage, e);/<code><code> return new RspDTO(PARAM_FAIL_CODE, e.getMessage);/<code><code> }/<code>
<code> @ExceptionHandler(NoHandlerFoundException.class)/<code><code> public RspDTO handlerNoFoundException(Exception e) {/<code><code> logger.error(e.getMessage, e);/<code><code> return new RspDTO(404, "路徑不存在,請檢查路徑是否正確");/<code><code> }/<code>
<code> @ExceptionHandler(DuplicateKeyException.class)/<code><code> public RspDTO handleDuplicateKeyException(DuplicateKeyException e) {/<code><code> logger.error(e.getMessage, e);/<code><code> return new RspDTO(DUPLICATE_KEY_CODE, "數據重複,請檢查後提交");/<code><code> }/<code>

<code> @ExceptionHandler(Exception.class)/<code><code> public RspDTO handleException(Exception e) {/<code><code> logger.error(e.getMessage, e);/<code><code> return new RspDTO(500, "系統繁忙,請稍後再試");/<code><code> }/<code><code>}/<code>

4. 測試

如下文:確實做到了參數校驗時返回異常信息和對應的code,方便了我們不再繁瑣的處理參數校驗

这么写参数校验 Validator 就不会被劝退了

在ValidationMessages.properties 就是校驗的message,有著已經寫好的默認的message,且是支持i18n的,大家可以閱讀源碼賞析

自定義參數註解

1. 比如我們來個 自定義身份證校驗 註解

<code>@Documented/<code><code>@Target({ElementType.PARAMETER, ElementType.FIELD})/<code><code>@Retention(RetentionPolicy.RUNTIME)/<code><code>@Constraint(validatedBy = IdentityCardNumberValidator.class)/<code><code>public @interface IdentityCardNumber {/<code>
<code> String message default "身份證號碼不合法";/<code>
<code> Class> groups default {};/<code>
<code> Class extends Payload> payload default {};/<code><code>}/<code>

這個註解是作用在Field字段上,運行時生效,觸發的是IdentityCardNumber這個驗證類。

  • message 定製化的提示信息,主要是從ValidationMessages.properties裡提取,也可以依據實際情況進行定製

  • groups 這裡主要進行將validator進行分類,不同的類group中會執行不同的validator操作

  • payload 主要是針對bean的,使用不多。

2. 然後自定義Validator

這個是真正進行驗證的邏輯代碼:

<code>public class IdentityCardNumberValidator implements ConstraintValidator<identitycardnumber> {/<identitycardnumber>/<code>
<code> @Override/<code><code> public void initialize(IdentityCardNumber identityCardNumber) {/<code><code> }/<code>
<code> @Override/<code><code> public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {/<code><code> return IdCardValidatorUtils.isValidate18Idcard(o.toString);/<code><code> }/<code><code>}/<code>

IdCardValidatorUtils在項目源碼中,可自行查看

3. 使用自定義的註解

<code> @NotBlank(message = "身份證號不能為空")/<code><code> @IdentityCardNumber(message = "身份證信息有誤,請核對後提交")/<code><code> private String clientCardNo;/<code>

4. 使用groups的校驗

有的寶寶說同一個對象要複用,比如UserDTO在更新時候要校驗userId,在保存的時候不需要校驗userId,在兩種情況下都要校驗username,那就用上groups了:

先定義groups的分組接口Create和Update

<code>import javax.validation.groups.Default;/<code>
<code>public interface Create extends Default {/<code><code>}/<code>
<code>import javax.validation.groups.Default;/<code>
<code>public interface Update extends Default{/<code><code>}/<code>

再在需要校驗的地方@Validated聲明校驗組

<code> /**/<code><code> * 走參數校驗註解的 groups 組合校驗/<code><code> */<code><code> * @param userDTO/<code><code> * @return/<code><code> *//<code><code> @PostMapping("/update/groups")/<code><code> public RspDTO update(@RequestBody @Validated(Update.class) UserDTO userDTO) {/<code><code> userService.updateById(userDTO);/<code><code> return RspDTO.success;/<code><code> }/<code>

在DTO中的字段上定義好groups = {}的分組類型

<code>@Data/<code><code>public class UserDTO implements Serializable {/<code>
<code> private static final long serialVersionUID = 1L;/<code>
<code> /*** 用戶ID*//<code><code> @Not(message = "用戶id不能為空", groups = Update.class)/<code><code> private Long userId;/<code>
<code> /**/<code><code> * 用戶名/<code><code> *//<code><code> @NotBlank(message = "用戶名不能為空")/<code><code> @Length(max = 20, message = "用戶名不能超過20個字符", groups = {Create.class, Update.class})/<code><code> @Pattern(regexp = "^[\\\\\\u4E00-\\\\\\u9FA5A-Za-z0-9\\\\*]*$", message = "用戶暱稱限制:最多20字符,包含文字、字母和數字")/<code><code> private String username;/<code>
<code> /**/<code><code> * 手機號/<code><code> *//<code><code> @NotBlank(message = "手機號不能為空")/<code><code> @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手機號格式有誤", groups = {Create.class, Update.class})/<code><code> private String mobile;/<code>
<code> /**/<code><code> * 性別/<code><code> *//<code><code> private String sex;/<code>
<code> /**/<code><code> * 郵箱/<code><code> *//<code><code> @NotBlank(message = "聯繫郵箱不能為空")/<code><code> @Email(message = "郵箱格式不對")/<code><code> private String email;/<code>
<code> /**/<code><code> * 密碼/<code><code> *//<code><code> private String password;/<code>
<code> /*** 創建時間 *//<code><code> @Future(message = "時間必須是將來時間", groups = {Create.class})/<code><code> private Date createTime;/<code>
<code>}/<code>

注意:在聲明分組的時候儘量加上 extend javax.validation.groups.Default 否則,在你聲明@Validated(Update.class)的時候,就會出現你在默認沒添加groups = {}的時候的校驗組@Email(message = "郵箱格式不對"),會不去校驗,因為默認的校驗組是groups = {Default.class}.

5. restful風格用法

在多個參數校驗,或者@RequestParam 形式時候,需要在controller上加註@Validated

<code> @GetMapping("/get")/<code><code> public RspDTO getUser(@RequestParam("userId") @Not(message = "用戶id不能為空") Long userId) {/<code><code> User user = userService.selectById(userId);/<code><code> if (user == ) {/<code><code> return new RspDTO<user>.nonAbsent("用戶不存在");/<user>/<code><code> }/<code><code> return new RspDTO<user>.success(user);/<user>/<code><code> }/<code>
<code>@RestController/<code><code>@RequestMapping("user/")/<code><code>@Validated/<code><code>public class UserController extends AbstractController { .../<code>

總結

用起來很簡單,soEasy,重點參與的統一結構體返回,統一參數校驗,是減少我們代碼大量的try catch 的法寶,我覺得在項目中,將異常處理好,並將異常做好日誌管理,才是很好的昇華,文章淺顯,只是一個菜鳥的進階筆記.

-END-


分享到:


相關文章: