Spring Validation 介绍
环境搭建
在
pom.xml
中引入依赖:
Springboot-2.3
之前的版本只需引入 web 依赖即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
简单使用
对实体类
此处使用 Lombok
@Data
public class User {
@NotBlank(message = "用户名不能为空")
private String userName;
@NotNull(message = "性别不能为空")
private Integer sex;
@Length(max = 20, message = "备注长度不能超过20")
private String remark;
}
对接口
@RestController
public class UserController {
@GetMapping("/user")
public String hello(@Valid User user) {
return user.toString();
}
}
测试
访问该接口,传递不符合校验注解的数据
可以看到,没有通过校验的异常信息会在控制台打印出来
BindingResult
未使用
BindingResult
进行数据校验时,若校验不通过,系统会报异常,访问失败使用
BindingResult
进行数据校验时,若校验不通过,错误信息会存储在其中而系统不会报异常,访问成功注意:一个校验注解后面跟一个 BindingResult,若有多个校验注解,需要每个都对应一个 BindingResult
如下代码演示使用
BindingResult
并打印出其存储的校验失败信息:
@RestController
public class UserController {
@GetMapping("/user")
public String hello(@Valid User user, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
if (!fieldErrors.isEmpty()) {
//打印校验错误信息
fieldErrors.forEach(x-> System.out.println(x.getDefaultMessage()));
}
return user.toString();
}
}
Validated和Valid
简介
@Valid 是使用 Hibernate validation
@Validated 是使用 Spring Validator
说明:java 的JSR303声明了@Valid 这类接口,而 Hibernate-validator 对其进行了实现
@Validation 对 @Valid 进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。
区别
@Validated:可以用在在类上、方法上和方法参数上。但
不能用于成员属性(field)
@Validated 支持分组校验
@Valid:
可以用
在方法、构造函数、方法参数和成员属性(field)上
@Valid 支持嵌套校验
分组校验
在日常开发过程中,对于同一个参数,需要在不同场景下应用不同的校验规则,如:
- 新增时 code 可以为空,修改时 code 不能为空。
使用
@Validated
进行分组校验
实体类
@Data
public class User {
@NotBlank(message = "code不能为空", groups = {Update.class})
private String code;
@NotBlank(message = "用户名不能为空", groups = {Save.class, Update.class})
private String userName;
@NotNull(message = "性别不能为空", groups = {Save.class, Update.class})
private Integer sex;
@Length(max = 20, message = "备注长度不能超过20", groups = {Save.class, Update.class})
private String remark;
public interface Save {
}
public interface Update {
}
}
接口
@RestController
public class UserController {
@PostMapping("/user")
public String save(@Validated(User.Save.class) User user, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
List<String> list = new ArrayList<>();
if (!fieldErrors.isEmpty()) {
fieldErrors.forEach(x -> list.add(x.getDefaultMessage()));
}
return list.toString();
}
@PutMapping("/user")
public String update(@Validated(User.Update.class) User user, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
List<String> list = new ArrayList<>();
if (!fieldErrors.isEmpty()) {
fieldErrors.forEach(x -> list.add(x.getDefaultMessage()));
}
return list.toString();
}
}
测试
可以看出,修改比新增多校验了 code,分组校验成功
新增
修改
分组校验优化
使用校验注解时,若不指定其
groups
,会有一个默认的组Default
,从这个Default
入手,优化代码
实体类
新建 Update 组 并
继承
Default 组,只对code
指定为 Update 组,其他不指定此时不仅会校验 Update 组,未指定组(
即 Default 组
)的属性也会被校验
@Data
public class User {
@NotBlank(message = "code不能为空", groups = Update.class)
private String code;
@NotBlank(message = "用户名不能为空")
private String userName;
@NotNull(message = "性别不能为空")
private Integer sex;
@Length(max = 20, message = "备注长度不能超过20")
private String remark;
public interface Update extends Default {
}
}
接口
在编写接口时,如果 Update 继承了 Default,下面两个写法就是等效的:
- @Validated(Update.class)
- @Validated(Update.class,Default.class)
如果 @Validated 进行校验时没有指定组,等效于 @Validated(Default.class)
- @Validated
- @Validated(Default.class)
@RestController
public class UserController {
@PostMapping("/user")
public String save(@Validated User user, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
List<String> list = new ArrayList<>();
if (!fieldErrors.isEmpty()) {
fieldErrors.forEach(x -> list.add(x.getDefaultMessage()));
}
return list.toString();
}
@PutMapping("/user")
public String update(@Validated(User.Update.class) User user, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
List<String> list = new ArrayList<>();
if (!fieldErrors.isEmpty()) {
fieldErrors.forEach(x -> list.add(x.getDefaultMessage()));
}
return list.toString();
}
}
测试
嵌套校验
如果需要校验的对象中包含对象/集合,使用
@Valid
嵌套校验解决校验问题
实体类
Order
@Data
public class Order {
@NotBlank(message = "order: code 不能为空")
private String code;
@Length(max = 1, message = "order:test 最长为1")
private String test;
}
User
@Data
public class User {
@NotBlank(message = "user:code不能为空")
private String code;
@NotBlank(message = "user:用户名不能为空")
private String userName;
@Valid
@NotNull(message = "Order 对象不能为空")
private Order order;
}
接口
@RestController
public class UserController {
@PostMapping("/user")
public String save(@Validated User user, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
List<String> list = new ArrayList<>();
if (!fieldErrors.isEmpty()) {
fieldErrors.forEach(x -> list.add(x.getDefaultMessage()));
}
return list.toString();
}
}
测试
自定义校验
Spring Validation 允许用户自定义校验,分两步:
- 自定义校验注解
- 编写校验类
自定义校验手机号
实体类
@Data
public class User {
@Phone(message = "手机号不合法")
private String phoneNumber;
}
自定义注解
@Documented
@Constraint(validatedBy = {PhoneValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
//默认错误消息
String message() default "请输入合法的手机号码";
//分组
Class<?>[] groups() default {};
//负载
Class<? extends Payload>[] payloads() default {};
}
校验类
正则校验(未测试)
手机号
(^0[0-9]{2,3}[0-9]{8})|(^((\(\d{3}\))|(\d{3}\-))?(1[3456789]\d{9})$)|(^(400)(\d{3})(\d{4}$))|(^(800)(\d{3})(\d{4}$))
座机
^[0][1-9]{2,3}-[0-9]{5,10}$
400
^400-([0-9]){1}([0-9-]{6})([0-9]){1}$
public class PhoneValidator implements ConstraintValidator<Phone, String> {
/**
* 正则匹配手机号
* 校验手机号,号段主要有(不包括上网卡):130~139、150~153,155~159,180~189、170~171、176~178。14号段为上网卡专属号段
* 如有其他需求,修改此处正则即可
*/
private static final Pattern PATTERN = Pattern.compile("^((13[0-9])|(17[0-1,6-8])|(15[^4,\\\\D])|(18[0-9]))\\d{8}$");
private static final Integer PHONE_LENGTH = 11;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 不为空才进行校验
if (value != null) {
if (value.length() != PHONE_LENGTH) {
return false;
}
Matcher matcher = PATTERN.matcher(value);
return matcher.find();
}
return true;
}
}