1. 程式人生 > 實用技巧 >Spring Validated 引數校驗

Spring Validated 引數校驗

預定義物件的說明

Result結果

Restful介面統一返回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 如下:

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();
    }
}

介面呼叫示例

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