Spring Validation
使用 Spring Validation 优雅地校验参数
Constraint | 详细信息 |
---|---|
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
Hibernate Validator 附加的 constraint:
Constraint | 详细信息 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
基本校验
以一个User
实体为例
public class User {
@NotNull(message = "用户名不能为空")
private String userName;
@NotNull(message = "密码不能为空")
@Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成")
@Size(min = 6, max = 12, message = "密码长度必须在6-12字符之间")
private String passWord;
@Range(min = 1, max = 150, message = "年龄必须在1-150区间")
private Integer age;
@NotEmpty(message = "用户的兴趣不能为空")
private List<String> interest;
// 省略get/set
}
假设要求传入的Json
字段(@RequestBody
)中,用户名、密码、年龄都有特定的规则
对应的Controller
应该为
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/validate")
public String test(@Valid @RequestBody User user) {
System.out.println(1);
return "success";
}
@PostMapping("/validate2")
public String test2(@Validated @RequestBody User user) {
System.out.println(1);
return "success";
}
}
使用@Valid
或@Validated
均可
嵌套校验
嵌套校验支持用户将@Valid
和@Validated
混合使用,可用于更复杂的校验
还是以User
为例,新增一个friends
字段,代表用户的朋友们,同时加上@Valid
注解代表如果friends
入参有传,则需要对Friend
类的内部字段进行校验,如果没有传递则无需校验。
public class User {
@NotNull(message = "用户名不能为空")
private String userName;
@NotNull(message = "密码不能为空")
@Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成")
@Size(min = 6, max = 12, message = "密码长度必须在6-12字符之间")
private String passWord;
@Range(min = 1, max = 150, message = "年龄必须在1-150区间")
private Integer age;
@Valid
private List<Friend> friends;
// 省略get/set
}
Friend
类
public class Friend {
@NotNull(message = "朋友名称不能为空")
private String userName;
@Range(min = 1, max = 150, message = "年龄必须在1-150区间")
private Integer age;
// 省略get/set
}
假设此时参数传递为
{
"userName" : "11",
"passWord" : "test123_2",
"age" : 11,
"friends" : [
{
"age" : "22"
},
{
"userName" : "33"
}
]
}
表示该用户有2个friend
,其中一个只写了名字,其中一个只写了年龄,由代码可知年龄是非必填字段,对应的控制台日志为
符合校验预期,当此时friend
字段没有传递时,则不进行校验
分组校验
分组校验是Spring Validation
的特性,校验时在Controller层
对实体的书写必须使用@Validated
,分组校验提高了实体校验注解的可复用能力,只需要指定校验分组即可让同一实体适配多种场景。
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/validate2")
public String test2(@Validated @RequestBody User user) {
System.out.println(1);
return "success";
}
}
首先需要定义常见的CRUD分组场景,取任意名字均可,接口无需实现
public class ValidatedAction {
public interface Insert {
}
public interface Update {
}
public interface Search {
}
public interface Delete {
}
}
为刚才的User
内的字段增加分组,如在新增时需要填写用户名、密码,在删除时需要填写id和密码
public class User {
@NotNull(message = "id不能为空", groups = {ValidatedAction.Delete.class})
private String id;
@NotNull(message = "用户名不能为空", groups = {ValidatedAction.Insert.class})
private String userName;
@NotNull(message = "密码不能为空", groups = {ValidatedAction.Insert.class, ValidatedAction.Delete.class})
@Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成")
@Size(min = 6, max = 12, message = "密码长度必须在6-12字符之间")
private String passWord;
@Range(min = 1, max = 150, message = "年龄必须在1-150区间")
private Integer age;
@Valid
private List<Friend> friends;
// 省略get/set
}
修改Controller
接口,指定校验分组,一个为新增分组校验,一个为删除分组校验,同时需要加上javax
中自带的Default
分组,避免实体中没有写group的校验注解失效
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/validate")
public String test(@Validated({ValidatedAction.Insert.class, Default.class}) @RequestBody User user) {
System.out.println(1);
return "success";
}
@PostMapping("/validateDelete")
public String test2(@Validated({ValidatedAction.Delete.class, Default.class}) @RequestBody User user) {
System.out.println(1);
return "success";
}
}
新增时Postman
传参为
由于新增时,非空参数仅有用户名和密码,所以正常返回,此时分组为Delete
的id
并没有参与非空校验
删除时Postman
传参为
由于删除时,非空参数包含id,此时传参中没有id则会在控制台输出对应提示
自定义校验
hibernate
提供的校验注解在简单字段的场景已经基本够用了,如果提供的校验注解无法满足要求,这个时候可以考虑自定义注解,将校验与Controller
完全隔离。
本文主要考虑4种较为通用的场景下自定义注解的实现方法
场景1:字段为
基础类型
,约束传递的字段只能在枚举code的约束范围内,虽然定义字段为枚举字段可以简单实现传输枚举对象名完成枚举约束,但通常我们不将字段本身定义为枚举直接暴露给前端。期望能够通过直接引用枚举类,达成约束。场景2:字段为
String
,约束传递的字段只能是一组特定的String字符串场景3:字段为
Integer
,约束传递的字段只能是一组特定的Integer值场景4:字段为
List<String>
,约束传递的字段只能是一组特定的String字符串
自定义的过程比较简单
第一步:新增一个你的自定义注解,这里为SpecifiesValueValidator
自定义注解的写法可完全照搬@NotNull
等注解,稍微改动下@Constraint
的validatedBy
属性为当前自定义注解类,同时加上可重复性校验注解(非必须)@Repeatable(SpecifiesValueValidator.List.class)
,用于支持多个自定义注解使用在同一字段。
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {SpecifiesValueValidatorImpl.class})
@Repeatable(SpecifiesValueValidator.List.class)
public @interface SpecifiesValueValidator {
/**
* 默认校验消息
*
* @return String
*/
String message() default "入参必须为指定值";
/**
* 分组校验
*
* @return Class<?>[]
*/
Class<?>[] groups() default {};
/**
* 负载
*
* @return Class<? extends Payload>[]
*/
Class<? extends Payload>[] payload() default {};
/**
* 指定特定String值
*
* @return String[]
*/
String[] strGroup() default {};
/**
* 指定特定int值
*
* @return int[]
*/
int[] intGroup() default {};
/**
* 指定枚举类型
*
* @return Class<?>
*/
Class<?> enumClass() default Class.class;
/**
* 可重复校验
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
SpecifiesValueValidator[] value();
}
}