Spring Validation參數校驗


ValidValidated 的區別

區別 Valid Validated
提供者 JSR-303規范 Spring
是否支持分組 不支持 支持
標注位置 METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
嵌套校驗 支持 不支持

引入依賴

如果spring-boot版本小於2.3.x,spring-boot-starter-web會自動傳入hibernate-validator依賴。
如果spring-boot版本大於2.3.x,則需要手動引入依賴:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

預定義對象的說明

Result結果

接口統一返回Result格式的結果:

package com.qiankai.valid.common;

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * 返回結果
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 10:38
 */
@Data
@Accessors(chain = true)
public class Result<T> {
    private int code;
    private String message;
    private T data;

    public boolean ok() {
        return this.code == 0;
    }

    public static <T> Result<T> success() {
        return new Result<T>().setCode(0).setMessage("成功");
    }

    public static <T> Result<T> success(T data) {
        return new Result<T>().setCode(0).setMessage("成功").setData(data);
    }

    public static <T> Result<T> failure() {
        return new Result<T>().setCode(-1).setMessage("失敗");
    }

    public static <T> Result<T> failure(int code, String msg) {
        return new Result<T>().setCode(code).setMessage(msg);
    }

    public static <T> Result<T> failure(int code, String msg, T data) {
        return new Result<T>().setCode(-1).setMessage("失敗").setData(data);
    }
}

ErrorCode

全局異常錯誤碼,后面統一處理異常會用到:

一般為了更好的處理全局異常,使用的錯誤碼都是定義成枚舉類型(包含錯誤碼和錯誤描述),我這邊方便演示就隨便定義了一個類

package com.qiankai.valid.common;

/**
 * 錯誤碼
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:08
 */
public final class ErrorCode {

    /**
     * 參數校驗失敗錯誤碼
     */
    public static final int ARGUMENT_VALID_FAILURE = -2;
}

常用參數校驗

在DTO上添加注解,實現參數校驗,假設存在 UserDTO 如下:

@Data
public class UserDTO {
    private Long userId;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

RequestBody校驗

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

/**
 * RequestBody 參數校驗
 * 校驗失敗會拋出 MethodArgumentNotValidException 異常
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 10:37
 */
@RequestMapping("/api/user01")
@RestController
public class User01Controller {

    /**
     * RequestBody 參數校驗
     * 使用 @Valid 和 @Validated 都可以
     */
    @PostMapping("/save/1")
    public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
        return Result.success();
    }

    @PostMapping("/save/2")
    public Result save2User(@RequestBody @Valid UserDTO userDTO) {
        return Result.success();
    }
}

RequestParam / PathVariable 校驗

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * RequestMapping / PathVariable 參數校驗
 * 校驗失敗會拋出 ConstraintViolationException 異常
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 10:57
 */
@RequestMapping("/api/user02")
@RestController
@Validated
public class User02Controller {

    /**
     * 此時必須在Controller上標注 @Validated 注解,並在入參上聲明約束注解
     */

    /**
     * 路徑變量
     * 添加約束注解 @Min
     */
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // 校驗通過,才會執行業務邏輯處理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);
        userDTO.setAccount("11111111111111111");
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.success(userDTO);
    }

    /**
     * 查詢參數
     * 添加約束注解 @Length @NotNull
     */
    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // 校驗通過,才會執行業務邏輯處理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(10000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.success(userDTO);
    }
}

全局異常處理

上面如果校驗失敗,會拋出 MethodArgumentNotValidException 或者 ConstraintViolationException 異常。
在實際項目開發中,通常會用統一異常處理來返回一個更友好的提示。
比如我們系統要求無論發送什么異常,http的狀態碼必須返回200,由業務碼去區分系統的異常情況。

package com.qiankai.valid.exception;

import com.qiankai.valid.common.ErrorCode;
import com.qiankai.valid.common.Result;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

/**
 * 統一異常處理
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:05
 */
@RestControllerAdvice
public class CommonExceptionHandler {
    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校驗失敗:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, msg);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, ex.getMessage());
    }
}

使用全局異常前

使用全局異常后

分組校驗

有時候,為了區分業務場景,對於不同場景下的數據驗證規則可能不一樣(例如新增時可以不用傳遞 ID,而修改時必須傳遞ID),可以使用分組校驗。

代碼示例

DTO 如下:
在約束注解上聲明適用的分組

package com.qiankai.valid.dto;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * 分組校驗
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:12
 */
@Data
public class UserGroupValidDTO {

    @NotNull(groups = Update.class)
    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 保存的時候校驗分組
     */
    public interface Save {
    }

    /**
     * 更新的時候校驗分組
     */
    public interface Update {
    }
}

Controller 如下:
Validated注解上指定校驗的分組

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 分組校驗
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:19
 */
@RestController
@RequestMapping("/api/user_group_valid")
public class UserGroupValidController {

    @PostMapping("/save")
    public Result saveUser(@RequestBody @Validated(UserGroupValidDTO.Save.class) UserGroupValidDTO userDTO) {
        // 校驗通過,才會執行業務邏輯處理
        return Result.success();
    }

    @PostMapping("/update")
    public Result updateUser(@RequestBody @Validated(UserGroupValidDTO.Update.class) UserGroupValidDTO userDTO) {
        // 校驗通過,才會執行業務邏輯處理
        return Result.success();
    }
}

嵌套校驗

上面的校驗主要是針對基本類型進行了校驗,如果DTO中包含了自定義的實體類,就需要用到嵌套校驗。

代碼示例

DTO 如下:

package com.qiankai.valid.dto;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * 嵌套校驗
 * DTO中的某個字段也是一個對象,這種情況下,可以使用嵌套校驗
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 12:21
 */
@Data
public class UserNestedValidDTO {
    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 此時DTO類的對應字段必須標記@Valid注解
     */
    @Valid
    @NotNull(groups = {Save.class, Update.class})
    private Job job;

    @Data
    public static class Job {

        @NotNull(groups = {Update.class})
        @Min(value = 1, groups = Update.class)
        private Long jobId;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }

    /**
     * 保存的時候校驗分組
     */
    public interface Save {
    }

    /**
     * 更新的時候校驗分組
     */
    public interface Update {
    }
}

Controller 如下:

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserNestedValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 嵌套校驗
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 13:31
 */
@RestController
@RequestMapping("/api/user_nested_valid")
public class UserNestedValidController {

    @PostMapping("/save")
    public Result saveUser(@RequestBody @Validated(UserNestedValidDTO.Save.class) UserNestedValidDTO userDTO) {
        // 校驗通過,才會執行業務邏輯處理
        return Result.success();
    }

    @PostMapping("/update")
    public Result updateUser(@RequestBody @Validated(UserNestedValidDTO.Update.class) UserNestedValidDTO userDTO) {
        // 校驗通過,才會執行業務邏輯處理
        return Result.success();
    }
}

接口調用示例

嵌套參數校驗-save-success

嵌套參數校驗-update-success

根據DTO以及Controller中的校驗規則,在update時,如果不傳 jobId 嵌套校驗就會報錯,如下:

嵌套參數校驗-update-failure

集合校驗

如果請求體直接傳遞了json數組給后台,並希望對數組中的每一項都進行參數校驗。
此時,如果我們直接使用java.util.Collection下的list或者set來接收數據,參數校驗並不會生效!我們可以使用自定義list集合來接收參數:

代碼示例

先自定義一個用於包裝List的集合


package com.qiankai.valid.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Delegate;

import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;

/**
 * 包裝 List類型,並聲明 @Valid 注解
 * @param <E>
 */
@Getter
@Setter
public class ValidationList<E> implements List<E> {

    @Delegate // @Delegate是lombok注解
    @Valid // 一定要加@Valid注解
    public List<E> list = new ArrayList<>();

    // 一定要記得重寫toString方法
    @Override
    public String toString() {
        return list.toString();
    }
}

@Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校驗不通過,會拋出 NotReadablePropertyException,同樣可以使用統一異常進行處理。

Controller 如下:

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import com.qiankai.valid.entity.ValidationList;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 集合校驗
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 14:14
 */
@RestController
@RequestMapping("/api/valid_list")
public class ValidListController {

    @PostMapping("/saveList")
    public Result saveList(@RequestBody @Validated(UserGroupValidDTO.Save.class) ValidationList<UserGroupValidDTO> userList) {
        // 校驗通過,才會執行業務邏輯處理
        return Result.success();
    }
}

調用結果

集合校驗調用參數校驗-failure

編程式校驗

上面都是通過注解來進行校驗,也可以使用編程的方式進行校驗:

Controller:

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.ConstraintViolation;
import java.util.Set;

/**
 * 編程式校驗參數
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 15:34
 */
@RequestMapping("/api/valid_with_code")
@RestController
public class ValidWithCodeController {
    @Autowired
    private javax.validation.Validator globalValidator;

    /**
     * 編程式校驗
     */
    @PostMapping("/saveWithCodingValidate")
    public Result saveWithCodingValidate(@RequestBody UserGroupValidDTO userGroupValidDTO) {
        Set<ConstraintViolation<UserGroupValidDTO>> validate = globalValidator.validate(userGroupValidDTO, UserGroupValidDTO.Save.class);
        // 如果校驗通過,validate為空;否則,validate包含未校驗通過項
        if (validate.isEmpty()) {
            // 校驗通過,才會執行業務邏輯處理

        } else {
            for (ConstraintViolation<UserGroupValidDTO> userGroupValidDTOConstraintViolation : validate) {
                // 校驗失敗,做其它邏輯
                System.out.println(userGroupValidDTOConstraintViolation);
                // throw new RuntimeException();
            }
        }
        return Result.success();
    }
}

結果如下:

配置快速失敗

Spring Validation默認會校驗完所有字段,然后才拋出異常。可以通過一些簡單的配置,開啟Fali Fast模式,一旦校驗失敗就立即返回。

/**
 * 配置快速失敗
 */
@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失敗模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

參考博客:https://juejin.im/post/6856541106626363399


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM