Java Bean Validation

Java Bean Validation

Bean Validation 顾名思义是对 java Bean 的校验,目前为止,Java 对 Bean 的校验有3个规范。

  • JSR-303 : Bean Validation

  • JSR 349 : Bean Validation 1.1

  • JSR 380 : Bean Validation 2.0

JSR是Java Specification Requests的缩写,意思是Java 规范提案。关于数据校验这块,最新的是JSR380,也就是我们常说的Bean Validation 2.0

参考:

Spring 参数校验详解

深入了解数据校验:Java Bean Validation 2.0(JSR380)

javax.validation

javax.validation 是 Java 官方提供的对 JAVA Bean Validation 的规范,并没有提供实现。

由于 Bean Validation 有多个版本,因此 javax.validation 也提供了对于版本的实现

其中

  • 1.0.x 对应 JSR-303 : Bean Validation
  • 1.1.x 对应 JSR 349 : Bean Validation 1.1
  • 2.0.x 对应 JSR 380 : Bean Validation 2.0
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

在这里插入图片描述

Hibernate Validator

Bean Validation的实现,基于 javax.validation,并在它的基础上提供了更多的注解功能

<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.5.Final</version>
</dependency>

如果你只引入了 javax.validation,而没有引入Hibernate Validator,就会报如下的错误:

javax.validation.NoProviderFoundException: Unable to create a Configuration, because no Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath.

当我们引入这两个依赖之后

  <!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.1.5.Final</version>
        </dependency>

我们写一个代码进行测试,首先我们来定义一个Bean

package org.example;

import lombok.Builder;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
@Builder(toBuilder = true)
public class Foo {
    @NotBlank
    private String name;
    @NotNull
    private Integer age;
}

之后是测试类

package org.example;

import org.junit.jupiter.api.Test;

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

public class TestFoo {

    @Test
    public void test01() {
        Foo foo = Foo.builder().build();
        Set<ConstraintViolation<Foo>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(foo);
        System.out.println(validate);
    }
}

结果运行报错了:

javax.validation.ValidationException: HV000183: Unable to initialize 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead

查看文档之后,发现还需要加入如下的依赖:

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>3.0.3</version>
</dependency>

再次运行我们就可以看到控制台打印出了内容了:

[ConstraintViolationImpl{interpolatedMessage='不能为空', propertyPath=name, rootBeanClass=class org.example.Foo, messageTemplate='{javax.validation.constraints.NotBlank.message}'}, ConstraintViolationImpl{interpolatedMessage='不能为null', propertyPath=age, rootBeanClass=class org.example.Foo, messageTemplate='{javax.validation.constraints.NotNull.message}'}]

参考:

https://hibernate.org/validator/documentation/

Hibernate Validator 6.1.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

验证模式

  • 快速失败(fail fast)
    当发生第一个验证失败时就立即结束。
@Test
    public void testBeanPropertyValidationFailFast() {
        Foo foo = Foo.builder().build();
        Validator validator = Validation.byProvider(HibernateValidator.class)
                .configure()
                .failFast(true)
                .buildValidatorFactory()
                .getValidator();
        Set<ConstraintViolation<Foo>> validate = validator.validateProperty(foo, "name");
        for (ConstraintViolation<Foo> fooConstraintViolation : validate) {
            // property:name,error message:must not be blank
            System.out.println(MessageFormat.format("property:{0},error message:{1}", fooConstraintViolation.getPropertyPath().toString(), fooConstraintViolation.getMessage()));
        }
    }
  • 非快速失败模式(默认)
    收集所有失败再结束
@Test
    public void testBeanPropertyValidation() {
        Foo foo = Foo.builder().build();
        Set<ConstraintViolation<Foo>> validate = Validation.buildDefaultValidatorFactory().getValidator().validateProperty(foo, "name");
        for (ConstraintViolation<Foo> fooConstraintViolation : validate) {
            // property:name,error message:must not be null
            // property:name,error message:must not be blank
            System.out.println(MessageFormat.format("property:{0},error message:{1}", fooConstraintViolation.getPropertyPath().toString(), fooConstraintViolation.getMessage()));
        }
    }

自定义验证消息

@Data
@Builder(toBuilder = true)
public class Foo {
    @NotBlank
    @NotNull
    @Size(min = 2,
            max = 14,
            message = "name:[${validatedValue}],length: {min} to {max}"
    )
    private String name;
    @NotNull
    private Integer age;
}
  • {}包围的成为消息参数
  • ${}包围的称为消息表达式

默认的消息定义如下:

在这里插入图片描述

约束组(Group)

每个约束都至少要属于一个组,没有指定则属于默认(javax.validation.groups.Default)组。不分配groups,默认每次都要进行验证。如果指定则不再属于默认组。

/*
 * Bean Validation API
 *
 * License: Apache License, Version 2.0
 * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
 */
package javax.validation.groups;

/**
 * Default Bean Validation group.
 * <p>
 * Unless a list of groups is explicitly defined:
 * <ul>
 *     <li>constraints belong to the {@code Default} group</li>
 *     <li>validation applies to the {@code Default} group</li>
 * </ul>
 * Most structural constraints should belong to the default group.
 *
 * @author Emmanuel Bernard
 */
public interface Default {
}

把约束分组可以让我们在对bean进行验证时可以更灵活。
比如:同样一个bean,在新增和修改两个业务场景中需要验证的属性是不一样的(如新增不需要验证id,修改时则需要)

package org.example;

import lombok.Builder;
import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Data
@Builder(toBuilder = true)
public class Student {
    @NotNull(groups = ModGroup.class)
    public String no;
    @NotEmpty(groups = {AddGroup.class, ModGroup.class})
    public String name;
}

package org.example;

public interface AddGroup {
}


package org.example;

public interface ModGroup {
}

package org.example;

import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import java.text.MessageFormat;
import java.util.Set;

public class TestStudent {

    @Test
    public void testBeanValidationGroup() {
        Student student = Student.builder().build();
        Set<ConstraintViolation<Student>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(student, AddGroup.class);
        for (ConstraintViolation<Student> studentConstraintViolation : validate) {
            // property:name,error message:must not be empty
            System.out.println(MessageFormat.format("property:{0},error message:{1}", studentConstraintViolation.getPropertyPath().toString(), studentConstraintViolation.getMessage()));
        }
    }
}

当然还可以指定多个组

Validation.buildDefaultValidatorFactory().getValidator().validate(student, AddGroup.class,ModGroup.class);

如果设置了groups,但是校验的时候不指定groups,则默认检查Default组,所以下面这个代码不会检测出任何的error

@Test
    public void testBeanValidationGroup() {
        Student student = Student.builder().build();
        Set<ConstraintViolation<Student>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(student);
        for (ConstraintViolation<Student> studentConstraintViolation : validate) {
            System.out.println(MessageFormat.format("property:{0},error message:{1}", studentConstraintViolation.getPropertyPath().toString(), studentConstraintViolation.getMessage()));
        }
    }

@GroupSequence组序列

@GroupSequence它是JSR标准提供的注解

顾名思义,它表示Group组序列默认情况下,不同组别的约束验证是无序的 在某些情况下,约束验证的顺序是非常的重要的,比如如下两个场景:

  1. 第二个的约束验证依赖于第一个约束执行完成的结果(必须第一个约束正确了,第二个约束执行才有意义)
  2. 某个Group组的校验非常耗时,并且会消耗比较大的CPU/内存。那么我们的做法应该是把这种校验放到最后,所以对顺序提出了要求

一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候如果序列前边的组验证失败,则后面的组将不再给予验证。

package org.example;

import lombok.Builder;
import lombok.Data;

import javax.validation.GroupSequence;
import javax.validation.constraints.NotEmpty;
import javax.validation.groups.Default;

@Data
@Builder(toBuilder = true)
public class User {

    @NotEmpty(message = "firstName may be empty")
    private String firstName;
    @NotEmpty(message = "middleName may be empty", groups = Default.class)
    private String middleName;
    @NotEmpty(message = "lastName may be empty", groups = GroupA.class)
    private String lastName;
    @NotEmpty(message = "country may be empty", groups = GroupB.class)
    private String country;


    public interface GroupA {
    }

    public interface GroupB {
    }

    // 组序列
    @GroupSequence({Default.class, GroupA.class, GroupB.class})
    public interface Group {
    }
}
package org.example;

import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import java.text.MessageFormat;
import java.util.Set;

public class TestUser {
    @Test
    public void testGroupSequence() {
        User user = User.builder().build();
        Set<ConstraintViolation<User>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class);
        // property: middleName, error message: middleName may be empty, invalid value: null
        // property: firstName, error message: firstName may be empty, invalid value: null
        validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }

    @Test
    public void testGroupSequence2() {
        User user = User.builder().firstName("k").middleName("c").build();
        Set<ConstraintViolation<User>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class);
        // property: lastName, error message: lastName may be empty, invalid value: null
        validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }


    @Test
    public void testGroupSequence3() {
        User user = User.builder().firstName("k").middleName("c").lastName("g").build();
        Set<ConstraintViolation<User>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class);
        // property: country, error message: country may be empty, invalid value: null
        validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }
}

级联校验

通过使用@Valid可以实现递归验证,因此可以标注在List上,对它里面的每个对象都执行校验

package org.example;

import lombok.Builder;
import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;

@Data
@Builder(toBuilder = true)
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;

    @Valid
    @NotNull
    @Size(min = 3, max = 5)
    private List<Child> childList;

    @Valid
    @NotNull
    private Child child;


}

@Data
@Builder(toBuilder = true)
class Child {
    @NotNull
    private String name;
}
package org.example;

import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Set;

public class TestPerson {

    @Test
    public void testPersonValidation() {
        Person person = Person.builder()
                .child(Child.builder().build())
                .childList(new ArrayList<Child>() {{
                    add(Child.builder().build());
                }}).build();
        Set<ConstraintViolation<Person>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(person);
        // property: child.name, error message: must not be null, invalid value: null
        // property: age, error message: must not be null, invalid value: null
        // property: name, error message: must not be null, invalid value: null
        // property: childList, error message: size must be between 3 and 5, invalid value: [Child(name=null)]
        // property: childList[0].name, error message: must not be null, invalid value: null
        validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }
}

方法约束声明和验证,ExecutableValidator

从Bean Validation 1.1开始,约束不仅可以应用于JavaBean及其属性,而且可以应用于任何Java类型的方法和构造函数的参数和返回值

package org.example;

import lombok.Builder;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Data
@Builder(toBuilder = true)
public class Teacher {
    private String name;

    public Teacher(@NotNull String name) {
        this.name = name;
    }


    public void teach(@NotBlank String content) {
        System.out.println(content);
    }

    public @Size(min = 5, max = 10) String getTeacherName(String name) {
        return name;
    }
}

package org.example;

import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.executable.ExecutableValidator;
import java.text.MessageFormat;
import java.util.Set;

public class TestTeacher {

    @Test
    public void testConstructorValidation() throws NoSuchMethodException {
        ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables();
        Set<ConstraintViolation<Teacher>> constraintViolations = executableValidator.validateConstructorParameters(Teacher.class.getConstructor(String.class), new Object[]{null});
        // property: Teacher.arg0, error message: must not be null, invalid value: null
        constraintViolations.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }


    @Test
    public void testMethodValidation() throws NoSuchMethodException {
        Teacher teacher = Teacher.builder().build();
        ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables();
        Set<ConstraintViolation<Teacher>> constraintViolations = executableValidator.validateParameters(teacher, Teacher.class.getMethod("teach", String.class), new Object[]{null});
        // property: teach.arg0, error message: must not be blank, invalid value: null
        constraintViolations.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }


    @Test
    public void testReturnValueValidation() throws NoSuchMethodException {
        Teacher teacher = Teacher.builder().build();
        ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables();
        Set<ConstraintViolation<Teacher>> constraintViolations = executableValidator.validateReturnValue(teacher, Teacher.class.getMethod("getTeacherName", String.class), "CC");
        // property: getTeacherName.<return value>, error message: size must be between 5 and 10, invalid value: CC
        constraintViolations.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }
}

自定义约束

1.创建约束注解

package org.example;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UpperCaseValidator.class)
public @interface UpperCase {
    String message() default "must upper";

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

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

}

2.编写验证器(Implement ConstraintValidator)

package org.example;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class UpperCaseValidator implements ConstraintValidator<UpperCase, String> {
    @Override
    public void initialize(UpperCase constraintAnnotation) {

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }
        return value.equals(value.toUpperCase());
    }
}

package org.example;

import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import java.text.MessageFormat;
import java.util.Set;

public class TestEngineer {

    @Test
    public void testConstraintValidator() {
        Engineer engineer = Engineer.builder().name("test").build();
        Set<ConstraintViolation<Engineer>> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(engineer);
        // property: name, error message: must upper, invalid value: test
        validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }
}

在Spring中使用Hibernate Validator

spring-boot-starter-web中是添加了hibernate-validator依赖的,说明Spring Boot本身也是使用到了Hibernate Validator验证框架的

在这里插入图片描述

注意我这里的spring boot版本是2.2.2.RELEASE, 新版本的有可能没有依赖hibernate-validator, 所以需要自己添加spring-boot-starter-validation的依赖。

改造一下我们之前的测试方法,利用@Autowired的方式注入Validator,还有加入@SpringBootTest,当然也别忘记加入Spring Boot的启动类

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.text.MessageFormat;
import java.util.Set;

@SpringBootTest
public class TestEngineer {
    @Autowired
    private Validator validator;

    @Test
    public void testConstraintValidator() {
        Engineer engineer = Engineer.builder().name("test").build();
        Set<ConstraintViolation<Engineer>> validate = validator.validate(engineer);
        // property: name, error message: must upper, invalid value: test
        validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println);
    }
}

配置Validator

同样的我们可以给validator设置快速失败模式,可以通过方法 failFast(true)addProperty("hibernate.validator.fail_fast", "true")设置为快速失败模式,快速失败模式在校验过程中,当遇到第一个不满足条件的参数时就立即返回,不再继续后面参数的校验。否则会一次性校验所有参数,并返回所有不符合要求的错误信息

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

@Configuration
public class ValidatorConfig {

    /**
     * 配置验证器
     *
     * @return validator
     */
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 快速失败模式
                .failFast(true)
                // .addProperty( "hibernate.validator.fail_fast", "true" )
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

Controller请求参数的校验

package org.example.dto;

import lombok.Data;
import lombok.ToString;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
@ToString
public class UserDto {
    @NotBlank
    private String name;
    @NotNull
    private Integer age;
}

package org.example.controller;

import lombok.extern.slf4j.Slf4j;
import org.example.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;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {


    @PostMapping("/register")
    public void register(@RequestBody @Valid UserDto userDto) {
        log.info("register user:{}", userDto);
    }
}

请求参数的验证,需要在参数前加上@Valid或Spring的 @Validated注解,这两种注释都会导致应用标准Bean验证。如果验证不通过会抛出BindException异常,并变成400(BAD_REQUEST)响应。另外,如果参数前有@RequestBody注解,验证错误会抛出MethodArgumentNotValidException异常。

在这里插入图片描述

通过Errors参数在控制器内本地处理验证错误。

@PostMapping("/register")
    public void register(@RequestBody @Valid UserDto userDto, Errors errors) {
        log.info("register user:{}", userDto);
        if (errors.hasErrors()) {
            log.info("errors:{}", errors);
            errors.getAllErrors().forEach(error -> log.info(error.getDefaultMessage()));
        }
    }

请求参数为:{"age":11},这个时候不会像之前一样默认response错误内容,而是在控制台中log出信息

2020-09-12 22:59:13.181  INFO 8092 --- [nio-8081-exec-1] org.example.controller.UserController    : register user:UserDto(name=null, age=11)
2020-09-12 22:59:13.187  INFO 8092 --- [nio-8081-exec-1] org.example.controller.UserController    : errors:org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'userDto' on field 'name': rejected value [null]; codes [NotBlank.userDto.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDto.name,name]; arguments []; default message [name]]; default message [不能为空]
2020-09-12 22:59:13.187  INFO 8092 --- [nio-8081-exec-1] org.example.controller.UserController    : 不能为空

也可以通过BindingResult参数在控制器内本地处理验证错误。

@PostMapping("/register2")
    public void register2(@RequestBody @Valid UserDto userDto, BindingResult result) {
        log.info("register user:{}", userDto);
        if (result.hasErrors()) {
            log.info("result:{}", result);
            result.getAllErrors().forEach(re -> log.info(re.getDefaultMessage()));
        }
    }
2020-09-12 23:02:37.689  INFO 8152 --- [nio-8081-exec-1] org.example.controller.UserController    : register user:UserDto(name=null, age=11)
2020-09-12 23:02:37.695  INFO 8152 --- [nio-8081-exec-1] org.example.controller.UserController    : result:org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'userDto' on field 'name': rejected value [null]; codes [NotBlank.userDto.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDto.name,name]; arguments []; default message [name]]; default message [不能为空]
2020-09-12 23:02:37.695  INFO 8152 --- [nio-8081-exec-1] org.example.controller.UserController    : 不能为空

更多的测试代码:

测试json:{"name":"test"}

package org.example.controller;

import lombok.extern.slf4j.Slf4j;
import org.example.dto.UserDto;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
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;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {


    @PostMapping("/register")
    public void register(@RequestBody @Valid UserDto userDto, Errors errors) {
        log.info("register user:{}", userDto);
        if (errors.hasErrors()) {
            log.info("errors:{}", errors);
            errors.getAllErrors().forEach(error -> log.info(error.getDefaultMessage()));
        }
    }

    @PostMapping("/register2")
    public void register2(@RequestBody @Valid UserDto userDto, BindingResult result) {
        log.info("register user:{}", userDto);
        if (result.hasErrors()) {
            log.info("result:{}", result);
            result.getAllErrors().forEach(re -> log.info(re.getDefaultMessage()));
        }
    }


    // @Validated 和 @Valid 的效果是一样的

    @PostMapping("/register3")
    public void register3(@RequestBody @Validated UserDto userDto, Errors errors) {
        log.info("register user:{}", userDto);
        if (errors.hasErrors()) {
            log.info("errors:{}", errors);
            errors.getAllErrors().forEach(error -> log.info(error.getDefaultMessage()));
        }
    }


    @PostMapping("/register4")
    public void register4(@RequestBody @Validated UserDto userDto, BindingResult result) {
        log.info("register user:{}", userDto);
        if (result.hasErrors()) {
            log.info("result:{}", result);
            result.getAllErrors().forEach(re -> log.info(re.getDefaultMessage()));
        }
    }


    /**
     * 如果参数前有`@RequestBody`注解,验证错误会抛出`MethodArgumentNotValidException`异常。
     * Resolved [org.springframework.web.bind.MethodArgumentNotValidException:
     * Validation failed for argument [0] in public void org.example.controller.UserController.register5
     *
     * @param userDto
     */
    @PostMapping("/register5")
    public void register5(@RequestBody @Validated UserDto userDto) {
        log.info("register user:{}", userDto);
    }


    /**
     * 验证不通过会抛出`BindException`异常,并变成400(BAD_REQUEST)响应
     * Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
     *
     * @param userDto
     */
    @PostMapping("/register6")
    public void register6(@Validated UserDto userDto) {
        log.info("register user:{}", userDto);
    }
}

统一的校验异常错误处理

package org.example.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * hibernate validator 数据绑定验证异常拦截
     *
     * @param e 绑定验证异常
     * @return 错误返回消息
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public String validateErrorHandler(BindException e) {
        ObjectError error = e.getAllErrors().get(0);
        log.info("BindException 数据验证异常:{}", error.getDefaultMessage());
        return error.getDefaultMessage();
    }

    /**
     * hibernate validator 数据绑定验证异常拦截
     *
     * @param e 绑定验证异常
     * @return 错误返回消息
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String validateErrorHandler(MethodArgumentNotValidException e) {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        log.info("MethodArgumentNotValidException 数据验证异常:{}", error.getDefaultMessage());
        return error.getDefaultMessage();
    }
}

方法参数验证

首先在要使用参数验证的类上一定要加上@Validated注解,否则无效

package org.example.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@RestController
@RequestMapping("/user2")
@Slf4j
@Validated //必须加上@Validated,也不能使用@Valid
public class UserController2 {


    /**
     * http://localhost:8081/user2/register?name=test&age=
     * {
     * "timestamp": "2020-09-22T13:44:45.806+0000",
     * "status": 500,
     * "error": "Internal Server Error",
     * "message": "register.age: 不能为null, register.name: 个数必须在7和2147483647之间",
     * "path": "/user2/register"
     * }
     * <p>
     * 校验不通过会报javax.validation.ConstraintViolationException:
     * register.age: 不能为null, register.name: 个数必须在7和2147483647之间
     *
     * @param name
     * @param age
     */
    @GetMapping("/register")
    public void register(@RequestParam(name = "name") @Size(min = 7) String name,
                         @RequestParam(name = "age") @NotNull Integer age) {
        log.info("register name:{},age:{}", name, age);
    }


    /**
     * 请求:http://localhost:8081/user2/register2
     * {
     * "timestamp": "2020-09-22T13:53:25.046+0000",
     * "status": 500,
     * "error": "Internal Server Error",
     * "message": "register2.<return value>: 不能为空",
     * "path": "/user2/register2"
     * }
     * <p>
     * 报错:javax.validation.ConstraintViolationException: register2.<return value>: 不能为空
     *
     * @return
     */
    @GetMapping("/register2")
    public @NotBlank String register2() {
        return "";
    }
}



参考

参数校验 Hibernate-Validator

https://hibernate.org/validator/documentation/

Hibernate Validator 6.1.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Spring 参数校验详解

深入了解数据校验:Java Bean Validation 2.0(JSR380)

分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题

Bean Validation和Hibernate Validator

使用spring validation完成数据后端校验

@Validated和@Valid区别:Spring validation验证框架对入参实体进行嵌套验证必须在相应属性(字段)加上@Valid而不是@Validated

如何优雅的做数据校验-Hibernate Validator详细使用说明

Spring 参数校验的异常处理

使用spring validation完成数据后端校验

源代码

(多个分支)
https://gitee.com/cckevincyh/validation-demo/tree/spring-web-validation/

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值