文章目录
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
。
参考:
深入了解数据校验: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组序列
。默认情况下,不同组别的约束验证是无序的 在某些情况下,约束验证的顺序是非常的重要的,比如如下两个场景:
- 第二个组的约束验证依赖于第一个约束执行完成的结果(必须第一个约束正确了,第二个约束执行才有意义)
- 某个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 "";
}
}
参考
https://hibernate.org/validator/documentation/
Hibernate Validator 6.1.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide
深入了解数据校验:Java Bean Validation 2.0(JSR380)
分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题
Bean Validation和Hibernate Validator
@Validated和@Valid区别:Spring validation验证框架对入参实体进行嵌套验证必须在相应属性(字段)加上@Valid而不是@Validated
如何优雅的做数据校验-Hibernate Validator详细使用说明
源代码
(多个分支)
https://gitee.com/cckevincyh/validation-demo/tree/spring-web-validation/