SpringBoot Validation優雅的全域性引數校驗

kenx發表於2021-07-06

前言

我們都知道在平時寫controller時候,都需要對請求引數進行後端校驗,一般我們可能會這樣寫

public String add(UserVO userVO) {
    if(userVO.getAge() == null){
        return "年齡不能為空";
    }
    if(userVO.getAge() > 120){
        return "年齡不能超過120";
    }
    if(userVO.getName().isEmpty()){
        return "使用者名稱不能為空";
    }
    // 省略一堆引數校驗...
    return "OK";
}

業務程式碼還沒開始寫呢,光引數校驗就寫了一堆判斷。這樣寫雖然沒什麼錯,但是給人的感覺就是:不優雅,不專業,程式碼可讀性也很差,一看就是新手寫的程式碼

作為久經戰爭的老司機怎麼能這樣呢,大神是不允許這樣程式碼出現的,其實SpringBoot提供整合了引數校驗解決方案spring-boot-starter-validation

整合使用

在SpringBootv2.3之前的版本只需要引入 web 依賴就可以了他包含了validation校驗包在此之後SpringBoot版本就獨立出來了需要單獨引入依賴

<!--引數校驗-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

內建的校驗註解有很多,羅列如下:

註解 校驗功能
@AssertFalse 必須是false
@AssertTrue 必須是true
@DecimalMax 小於等於給定的值
@DecimalMin 大於等於給定的值
@Digits 可設定最大整數位數和最大小數位數
@Email 校驗是否符合Email格式
@Future 必須是將來的時間
@FutureOrPresent 當前或將來時間
@Max 最大值
@Min 最小值
@Negative 負數(不包括0)
@NegativeOrZero 負數或0
@NotBlank 不為null並且包含至少一個非空白字元
@NotEmpty 不為null並且不為空
@NotNull 不為null
@Null 為null
@Past 必須是過去的時間
@PastOrPresent 必須是過去的時間,包含現在
@Pattern 必須滿足正規表示式
@PositiveOrZero 正數或0
@Size 校驗容器的元素個數

單個引數校驗

使用很簡單隻需要在需要校驗controller上加上@Validated註解在需校驗引數上加上@NotNull,@NotEmpty之類引數校驗註解就行了,

@Validated
@GetMapping("/home")
public class ProductController {
  public Result index(@NotBlank String name, @Email @NotBlank String email) {
        return ResultResponse.success();
    }
}

物件引數校驗

在上面的基礎上只需要在物件引數前面加上@Validated註解,然後在需要校驗的物件引數的屬性上面加上
@NotNull,@NotEmpty之類引數校驗註解就行了,

 @PostMapping("/user")
public Result index1(@Validated @RequestBody UserParams userParams) {
        log.info("info  test######");
        log.error("error test #####");
        return ResultResponse.success(userParams);
    }
@Data
public class UserParams {

    @NotBlank
    private String username;
    private int age;
    @NotBlank
    private String addr;
    @Email
    private String email;



}

引數校驗異常資訊處理

上面我們進行了引數校驗,預設當引數校驗沒通過後會通過異常方式來丟擲錯誤資訊MethodArgumentNotValidException是在校驗時丟擲的還包括很多其他異常資訊,這時我們可以通過全域性異常捕獲資訊來處理這些引數校驗異常

全域性異常處理類只需要在類上標註@RestControllerAdvice,並在處理相應異常的方法上使用@ExceptionHandler註解,寫明處理哪個異常即可

package cn.soboys.core;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.soboys.core.authentication.AuthenticationException;
import cn.soboys.core.ret.Result;
import cn.soboys.core.ret.ResultCode;
import cn.soboys.core.ret.ResultResponse;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/6/17 20:19
 * 全域性異常統一處理
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 處理 json 請求體呼叫介面物件引數校驗失敗丟擲的異常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result jsonParamsException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        List errorList = CollectionUtil.newArrayList();

        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            String msg = String.format("%s%s;", fieldError.getField(), fieldError.getDefaultMessage());
            errorList.add(msg);
        }
        return ResultResponse.failure(ResultCode.PARAMS_IS_INVALID, errorList);
    }


    /**
     * 處理單個引數校驗失敗丟擲的異常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Result ParamsException(ConstraintViolationException e) {

        List errorList = CollectionUtil.newArrayList();
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        for (ConstraintViolation<?> violation : violations) {
            StringBuilder message = new StringBuilder();
            Path path = violation.getPropertyPath();
            String[] pathArr = StrUtil.splitToArray(path.toString(), ".");
            String msg = message.append(pathArr[1]).append(violation.getMessage()).toString();
            errorList.add(msg);
        }
        return ResultResponse.failure(ResultCode.PARAMS_IS_INVALID, errorList);
    }

    /**
     * @param e
     * @return 處理 form data方式呼叫介面物件引數校驗失敗丟擲的異常
     */
    @ExceptionHandler(BindException.class)
    public Result formDaraParamsException(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
                .map(o -> o.getField() + o.getDefaultMessage())
                .collect(Collectors.toList());
        return ResultResponse.failure(ResultCode.PARAMS_IS_INVALID, collect);
    }

    /**
     * 請求方法不被允許異常
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Result httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        return ResultResponse.failure(ResultCode.METHOD_NOT_ALLOWED);
    }

    /**
     * @param e
     * @return Content-Type/Accept 異常
     * application/json
     * application/x-www-form-urlencoded
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public Result httpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
        return ResultResponse.failure(ResultCode.BAD_REQUEST);
    }

    /**
     * handlerMapping  介面不存在跑出異常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public Result noHandlerFoundException(NoHandlerFoundException e) {
        return ResultResponse.failure(ResultCode.NOT_FOUND, e.getMessage());
    }


    /**
     * 認證異常
     * @param e
     * @return
     */
    @ExceptionHandler(AuthenticationException.class)
    public Result UnNoException(AuthenticationException e) {
        return ResultResponse.failure(ResultCode.UNAUTHORIZED,e.getMessage());
    }

    /**
     *
     * @param e 未知異常捕獲
     * @return
     */
    @ExceptionHandler(Exception.class)
    public Result UnNoException(Exception e) {
        return ResultResponse.failure(ResultCode.INTERNAL_SERVER_ERROR, e.getMessage());
    }
}

這裡關於全域性異常處理請參考我這篇SpringBoot優雅的全域性異常處理
這裡我返回的是自定義響應體api 請參考我這篇Spring Boot 無侵入式 實現RESTful API介面統一JSON格式返回

當然校驗異常的處理還有其它方式,當引數在沒有通過校驗情況下會幫我們把錯誤資訊注入到BindingResult物件中,會注入到對應controller方法,這個僅限於引數有@RequestBody或者@RequestParam修飾的引數才行

public String add1(@Validated UserVO userVO, BindingResult result) {
    List<FieldError> fieldErrors = result.getFieldErrors();
    if(!fieldErrors.isEmpty()){
        return fieldErrors.get(0).getDefaultMessage();
    }
    return "OK";
}

當然我推薦第一種可以通過全域性異常處理的方式統一處理校驗異常

如果每個Controller方法中都寫一遍對BindingResult資訊的處理,使用起來還是很繁瑣。程式碼很冗餘

當我們寫了@validated註解,不寫BindingResult的時候,SpringBoot 就會丟擲異常。由此,可以寫一個全域性異常處理類來統一處理這種校驗異常,從而免去重複組織異常資訊的程式碼。

關注公眾號猿人生獲取更多幹貨分享
image

相關文章