Spring Validation參數(shù)校驗(yàn)

簡(jiǎn)介

Spring Validation是在Spring Context包下的,在Spring Boot項(xiàng)目中,我們引入spring-boot-starter-web便會(huì)引入進(jìn)來(lái),Spring Validation是對(duì)Hibernate Validator的二次封裝,使我們可以更方便的在Spring MVC中完成自動(dòng)校驗(yàn)。

Hibernate Validator是對(duì)JSR-303(Bean Validation)的參考實(shí)現(xiàn)。Hibernate Validator 提供了JSR-303規(guī)范中所有內(nèi)置constraint的實(shí)現(xiàn),除此之外還有一些附加的constraint。

JSR-303定義的constraint

Constraint Description
@Null 被注解的元素必須為null
@NotNull 被注解的元素必須不為null
@AssertTure 被注解的元素必須為ture
@AssertFalse 被注解的元素必須為false
@Min(value) 被注解的元素必須是數(shù)字且必須大于等于指定值
@Max(value) 被注解的元素必須是數(shù)字且必須小于等于指定值
@DecimalMin(value) 被注解的元素必須是數(shù)字且必須大于等于指定值
@DecimalMax(value) 被注解的元素必須是數(shù)字且必須小于等于指定值
@Size(max, min) 被注解的元素必須在指定的范圍內(nèi)
@Digits(integer, fraction) 被注解的元素必須是數(shù)字且其值必須在給定的范圍內(nèi)
@Past 被注解的元素必須是一個(gè)過(guò)去的日期
@Future 被注解的元素必須是一個(gè)將來(lái)的日期
@Pattern(value) 被注解的元素必須符合給定正則表達(dá)式

Hibernate Validator附加實(shí)現(xiàn)的constraint

Constraint Description
@Email 被注解的元素必須是Email地址
@Length(min, max) 被注解的元素長(zhǎng)度必須在指定的范圍內(nèi)
@NotEmpty 被注解的元素必須非空
@Range 被注解的元素(可以是數(shù)字或者表示數(shù)字的字符串)必須在給定的范圍內(nèi)
@URL 被注解的元素必須是URL

當(dāng)然,我們也可以自定義實(shí)現(xiàn),自定義實(shí)現(xiàn)在下面使用中在講吧。

使用

首先是引入依賴(lài),在Spring Boot項(xiàng)目中,我們引入web就已經(jīng)可以使用了,這里就不再具體贅述了。?

使用@Validated注解攔截校驗(yàn)

先定義下要校驗(yàn)的實(shí)體吧:

public class User {

    @Length(min = 1, max = 22, message = "name字段不合法")
    private String name;
    @Min(value = 1, message = "age字段不合法")
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Controller中,我們需要校驗(yàn)前端傳遞過(guò)來(lái)的參數(shù),我們可以這么寫(xiě)

@RestController
public class TestController {

    @PostMapping("/test")
    public Object test(@RequestBody @Validated User user, BindingResult result) {
        if (result.hasErrors()) {
            return result.getAllErrors().stream()
              .map(ObjectError::getDefaultMessage)
              .collect(Collectors.toList());
        }
        return user;
    }
}

只需要在需要校驗(yàn)的實(shí)體前面打上@Validated注解就可以了,這時(shí)候,如果我們傳遞的參數(shù)符合要求,則會(huì)正常返回。否則返回:

[
    "age字段不合法",
    "name字段不合法"
]

它會(huì)將我們所有不合法信息一次性全部返回,在日常開(kāi)發(fā)中,我們可以吧校驗(yàn)BindingResult是否有錯(cuò)誤信息的校驗(yàn)統(tǒng)一抽出到一個(gè)工具類(lèi)中去做處理,使用項(xiàng)目中統(tǒng)一格式返回錯(cuò)誤信息就好。這就是一個(gè)最簡(jiǎn)單的校驗(yàn)示例了,其他注解也都是類(lèi)似的,就不多舉例了,可以自己嘗試著玩玩。

在日常開(kāi)發(fā)中想必都曾遇到過(guò)這樣的需求,比如這個(gè)age這個(gè)字段,我想要這個(gè)字段只在PC端校驗(yàn),在App端不做限制,這就需要用到分組校驗(yàn)了,每個(gè)注解都提供了一個(gè)group屬性,利用這個(gè)屬性就可以輕易做到以上需求。比如在User上的注解中加入group屬性,指定其被校驗(yàn)的group

public class User {

    @Length(min = 1, max = 22, message = "name字段不合法", groups = {App.class, PC.class})
    private String name;
    @Min(value = 1, message = "age字段不合法", groups = PC.class)
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Controller中的@Validated中指定當(dāng)前group

@RestController
public class TestController {

    @PostMapping("/test")
    public Object test(@RequestBody @Validated(App.class) User user, BindingResult result) {
        if (result.hasErrors()) {
            return result.getAllErrors().stream()
              .map(ObjectError::getDefaultMessage)
              .collect(Collectors.toList());
        }
        return user;
    }
}

這時(shí)候我再使用兩個(gè)不合法字段訪(fǎng)問(wèn)返回:

[
    "name字段不合法"
]

可以看到,它并沒(méi)有對(duì)age字段進(jìn)行校驗(yàn)。這就是它的分組校驗(yàn)。

在方法實(shí)現(xiàn)中攔截校驗(yàn)

它不只是在Controller校驗(yàn)前端傳遞過(guò)來(lái)的參數(shù)的時(shí)候可以用,它在方法中同樣可以用,我們可以這樣來(lái)使用:

@RestController
public class TestController {

    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    SmartValidator smartValidator;

    @GetMapping("/test")
    public Object test() {
        String context = "{\"name\": \"felixu\",\"age\": 0}";
        User user = null;
        try {
            user = objectMapper.readValue(context, User.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        BeanPropertyBindingResult result = new BeanPropertyBindingResult(user, "user");
        smartValidator.validate(user, result);
        if (result.hasErrors()) {
            return result.getAllErrors().stream()
              .map(ObjectError::getDefaultMessage)
              .collect(Collectors.toList());
        }
        return user;
    }
}

使用需要被校驗(yàn)的實(shí)體構(gòu)造BeanPropertyBindingResult對(duì)象,然后將傳遞給SmartValidatorvalidate方法來(lái)完成跟上面相同的校驗(yàn)。validate有個(gè)重載方法,也接收分組,所以這種方式同樣可以實(shí)現(xiàn)分組校驗(yàn)。

自定義實(shí)現(xiàn)

需求總是多變的,有時(shí)候,可能上面的校驗(yàn)方式并不能滿(mǎn)足我們的要求,這時(shí)候就需要我們自定義一下校驗(yàn)了,要做到自定義注解來(lái)校驗(yàn),我們需要做以下兩步,首先實(shí)現(xiàn)ConstraintValidator<A extends Annotation, T>(ps:原諒我的自戀。。。不要管我干了啥,關(guān)鍵是要知道可以用來(lái)干啥對(duì)不對(duì),哈哈哈哈):

public class IsFelixuValidator implements ConstraintValidator<IsFelixu, String> {

    @Override
    public void initialize(IsFelixu annotation) {
        
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ("felixu".equals(value)) {
            return true;
        }
        return false;
    }
}

isValid便是我們的校驗(yàn)邏輯,true為通過(guò)校驗(yàn)。

然后我們實(shí)現(xiàn)注解:

@Documented
@Constraint(
    // 指定對(duì)應(yīng)的校驗(yàn)類(lèi)
    validatedBy = {IsFelixuValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsFelixu {

    String message() default "this value is not felixu";
    // 這兩個(gè)屬性必須要存在
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

這樣就ok了,我們繼續(xù)使用之前的來(lái)做測(cè)試,在Username屬性上加上@IsFelixu注解,此時(shí)測(cè)試,如果不傳遞namefelixu的值,則會(huì)提示如下信息:

[
    "this value is not felixu",
    "age字段不合法"
]

這個(gè)多多少少看著有點(diǎn)沙雕,我決定拿個(gè)別的舉例了,先看注解

@Documented
@Constraint(
        validatedBy = {PrecisionValidator.class}
)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Precision {

    String message() default "精度不符合要求";
    // 最少小數(shù)位
    int min() default 0;
    // 最多小數(shù)位
    int max() default Integer.MAX_VALUE;
    // 是否固定小數(shù)位,固定多少位,-1 為不固定
    int fixed() default -1;

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

emmmm,這是個(gè)校驗(yàn)精度的,我們可以看到它有好幾個(gè)屬性,比如min啊、max的之類(lèi)的,就是為了標(biāo)注當(dāng)前值要最小幾位小數(shù),最多多少位小數(shù)。

下面我們?cè)賮?lái)看看是怎么實(shí)現(xiàn)校驗(yàn)的:

public class PrecisionValidator implements ConstraintValidator<Precision, Object> {

    // Hibernate validator 自帶的一些錯(cuò)誤提示
    private static final Log LOG = LoggerFactory.make(MethodHandles.lookup());

    private int min;

    private int max;

    private int fixed;

    // 項(xiàng)目啟動(dòng)時(shí),此方法便會(huì)被調(diào)用,可以拿到注解中各屬性的值,用以做合法性校驗(yàn)
    // 比如這里就檢查了這三個(gè)屬性的合法性
    @Override
    public void initialize(Precision precision) {
        min = precision.min();
        max = precision.max();
        fixed = precision.fixed();
        validateParameters();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // 數(shù)字轉(zhuǎn)字符串
        if (value instanceof Number)
            value = String.valueOf(value);
        if (value instanceof CharSequence) {
            // 這是自己的一個(gè)工具類(lèi),可以不用管,就是把 value 以 . 切割成數(shù)組
            List<String> vals = Splitters.DOT.splitToList((String) value);
            // 如果只有 1 位,說(shuō)明沒(méi)有小數(shù)位,直接放行
            if (vals.size() == 1)
                return true;
            // 不是 2 位說(shuō)明參數(shù)是瞎特么傳的
            if (vals.size() != 2)
                return false;
            // 獲取小數(shù)位的長(zhǎng)度
            int length = vals.get(1).length();
            // 判斷有沒(méi)有指定固定小數(shù)位, -1 認(rèn)為是沒(méi)有
            if (fixed != -1)
                return length == fixed;
            // 判斷長(zhǎng)度是否在范圍內(nèi)
            return length >= min && length <= max;
        }
        return false;
    }
   
    // 注解屬性值校驗(yàn)方法
    private void validateParameters() {
        if (min < 0)
            throw LOG.getMinCannotBeNegativeException();
        if (max < 0)
            throw LOG.getMaxCannotBeNegativeException();
        if (max < min)
            throw LOG.getLengthCannotBeNegativeException();
        if (fixed < 0 && fixed != -1)
            throw new IllegalArgumentException("The fixed cannot be negative.");
    }
}

之前實(shí)現(xiàn)那個(gè)沙雕注解的時(shí)候有些方法沒(méi)有詳細(xì)介紹,可以看一下上面這個(gè)實(shí)現(xiàn)中的注釋。當(dāng)時(shí)這個(gè)是為了滿(mǎn)足產(chǎn)品的一個(gè)沙雕要求,正好拿出來(lái)舉個(gè)??。

補(bǔ)充說(shuō)明

在上面舉例中,我在controller中注入了BindingResult來(lái)獲取錯(cuò)誤信息,可以將錯(cuò)誤信息封裝到一個(gè)工具類(lèi)中來(lái)統(tǒng)一返回,例如下面代碼中的onValidFail方法,將錯(cuò)誤信息封裝到統(tǒng)一返回中

public class RespDTO<T> implements Serializable{

    public int code;

    public String error;

    public T data;

    public static <T> RespDTO<T> onSuc() {
        return onSuc(null);
    }

    public static <T> RespDTO<T> onSuc(T data) {
        return build(ErrorCode.OK.getCode(), ErrorCode.OK.getMsg(), data);
    }

    public static <T> RespDTO<T> onValidFail(BindingResult result) {
        String errorMsg = result.getAllErrors()
                .stream()
                .map(objectError -> {
                    FieldError error = (FieldError) objectError;
                    return error.getField() + ", " + error.getDefaultMessage();
                })
                .collect(Collectors.joining("\n"));
        return build(ErrorCode.PARAM_ERROR.getCode(), errorMsg, null);
    }

    public static <T> RespDTO<T> onFail(ErrorCode errorCode) {

        return onFail(errorCode.getCode(), errorCode.getMsg());
    }

    public static <T> RespDTO<T> onFail(int code, String msg) {
        return build(code, msg, null);
    }

    private static <T> RespDTO<T> build(int ret, String msg, T data) {
        return new RespDTO<T>(ret, msg, data);
    }
}

當(dāng)然我們也可以不去注入BindingResult,而直接使用注解,這樣校驗(yàn)失敗就會(huì)拋出異常,再由我們自己的統(tǒng)一異常攔截去攔截,之后再處理成統(tǒng)一返回,這樣也是可以的,比如下面這樣寫(xiě)controller

public class DemoController {
    @PostMapping
    public RespDTO<Boolean> create(@Validated({Create.class, Default.class}) @RequestBody RoutineInfo routineInfo) {
        Account account = accountService.getDefaultAccount(routineInfo.getUserId());
        routineInfo.setAccountId(account.getId());
        return RespDTO.onSuc(routineInfoService.save(routineInfo));
    }
}

然后定義異常攔截去攔截:

@RestControllerAdvice
public class DemoExceptionHandler {
    @ExceptionHandler(BindException.class)
    public ResponseEntity<RespDTO<Object>> handleBindException(BindException e) {
        return new ResponseEntity<>(RespDTO.onValidFail(e), HttpStatus.OK);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<RespDTO<Object>> methodArgumentNotValidHandler(MethodArgumentNotValidException e) {
        return new ResponseEntity<>(RespDTO.onValidFail(e.getBindingResult()), HttpStatus.OK);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<RespDTO<Object>> validationExceptionHandler(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        Map<String, String> result = new HashMap<>(violations.size());
        for (ConstraintViolation<?> violation : violations) {
            String fieldName = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
            result.put(fieldName, violation.getMessage());
        }
        BindException exception = new BindException(e, "exception");
        result.forEach((key, value) -> exception.addError(new FieldError(exception.getObjectName(), key, value)));
        return new ResponseEntity<>(RespDTO.onValidFail(exception.getBindingResult()), HttpStatus.OK);
    }
}

這樣校驗(yàn)失敗拋出的異常便會(huì)被統(tǒng)一的異常攔截器攔截到,然后被處理成統(tǒng)一返回,返回給前端了。

結(jié)語(yǔ)

JSR-303 的發(fā)布使得在數(shù)據(jù)自動(dòng)綁定和驗(yàn)證變得簡(jiǎn)單,使開(kāi)發(fā)人員在定義數(shù)據(jù)模型時(shí)不必考慮實(shí)現(xiàn)框架的限制。當(dāng)然Bean Validation還只是提供了一些最基本的constraint。

上面只是相對(duì)簡(jiǎn)單的用法,在實(shí)際的開(kāi)發(fā)過(guò)程中,用戶(hù)可以根據(jù)自己的需要組合或開(kāi)發(fā)出更加復(fù)雜的constraint。這就需要想象力了,從上面的用法中應(yīng)該可以想到很多地方可以去使用,但是設(shè)計(jì)和實(shí)現(xiàn)時(shí),往往需要考慮諸多因素,比如易用性和封裝的復(fù)雜度,等等方面,還需要自己去考量了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容