说明
在《spring mvc 接收参数注解对比及最佳使用方案推荐》章节中作者已经整理了RESTful接口的传参方案。而在日常的项目开发中,我们需要严格控制参数规范,以避免不合规的参数导致程序处理异常。
spring validation为我们提供了基于POJO类的参数验证解决方案,本文将详细整理相关知识。
一. 依赖添加
在我们的tysite-service
搭建时依赖使用的是org.springframework.boot:spring-boot-starter-web
,该依赖中已经引用spring-boot-starter-validation
,无需额外添加。
二. 常用的约束验证注解
面对服务器接口参数校验问题,J2EE 6 定义了一项子规范:Bean Validation
,官方实现是Hibernate Validator
,Spring 4.0 之后的版本均已经支持Bean Validation功能。
常用的约束验证注解,如下表所示:
约束注解 | 描述 | 适用场景 |
---|---|---|
@null | 被注释元素必须为NULL | |
@NotNull | 被注释元素必须不为NULL,无法检查长度为0的字符串 | |
@NotEmpty | 被注释元素不能为 NULL或 EMPTY | |
@NotBlank | 被注释元素不能为 NULL 且 字符串trim 后长度必须大于0 | 适用于验证字符串字段非空 |
@AssertTrue | 被注释元素必须为Boolean类型的 true | 适用于属性必须为true的场景 |
@AssertFalse | 被注释元素必须为Boolean类型的 false | 适用于属性必须为false的场景 |
@Min | 被注释元素必须大于指定数值,格式为 long 类型 | 适用于属性限制long 类型数据最小值的场景 |
@Max | 被注释元素必须小于指定数值,格式为 long 类型 | 适用于属性限制long 类型数据最大值的场景 |
@DecimalMin | 被注释元素必须大于指定数值,参数为BigDecimal定义的小数 | 适用于属性限制小数 数据最小值的场景 |
@DecimalMax | 被注释元素必须大于指定数值,参数为BigDecimal定义的小数 | 适用于属性限制小数 数据最大值的场景 |
@Digits | 被注释元素必须为指定格式的数字,integer 指定整数精度,fraction 指定小数精度 | 适用于属性限制金额格式,字段类型必须使用 Double |
@Size | 被注释元素必须的大小必须在指定的范围内 | 适用于属性限制字符串长度的场景 |
@Length | 被注释元素必须的大小必须在指定的范围内 | |
@Past | 被注释元素必须是过去的一个日期 | 适用于属性限制必须为过去日期的场景 |
@Future | 被注释元素必须是未来的一个日期 | 适用于属性限制必须为未来日期的场景 |
@Pattern | 被注释元素必须符合正则表达式regexp 的规范 | 适用于属性为密码、身份证号等需要正则限制的场景 |
被注释元素必须符合Email格式 | 适用于属性限制为Email格式的场景 | |
@Range | 被注解的元素(可以是数字或者表示数字的字符串)必须在给定的范围内 | |
@URL | 被注释元素必须符合URL格式规范 | 适用于属性限制为网址的场景 |
备注:spring validation
提供默认的错误提示信息,在使用时也可以通过注解的message
自定义错误提示信息。
三. spring validation 常规用法
spring validation
的使用需要完成两步操作:
1、以POJO类接收参数,在该POJO类的属性上通过约束验证注解,限定参数格式。
以下为作者根据个人经验,整理的常用验证规范的使用样例
……
private enum SexEnum {
/** 男 */
MALE,
/** 女 */
FEMALE
}
/** 主键ID */
private Integer id;
/** 用户名 */
@NotBlank(message = "请输入用户名")
@Size(min = 3, max = 20, message = "用户名长度必须在3~20之间")
private String username;
/** 名称 */
@NotBlank(message = "请输入姓名")
@Size(min = 2, max = 20, message = "姓名长度必须在2~20之间")
private String name;
/** 资源名称 */
@NotBlank(message = "请输入资源名称")
@Size(max = 50, message = "资源名称长度必须在1~50之间")
private String resourceName;
/** 文本域 */
@NotBlank(message = "请输入资源简介")
@Size(max = 1000, message = "文本域长度必须在1~1200之间")
private String textField;
/** 密码 */
@NotBlank(message = "请输入密码")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[$@$!%*?&])[A-Za-z\\d$@$!%*?&]{8,20}",
message = "密码长度8~20位,必须包含数字、字母(大小写各一个)、特殊字符")
private String password;
/** 性别 */
private SexEnum genders;
/** 邮箱 */
@Email(message = "请输入正确的邮箱地址")
private String email;
/** 手机号码 */
@Pattern(regexp = "^1[3|4|5|8][0-9]\\d{8}$", message = "请输入正确的手机号码")
private String phone;
/** 存款 */
@Digits(integer = 14, fraction = 2, message = "请输入有效的金额")
private Double deposit;
/** 出生日期 */
@Past(message = "请输入正确的出生日期")
private Date birthday;
/** 身高 */
@NotNull(message = "请填写您的身高")
@DecimalMin(value = "44.7", message = "请填写有效的身高")
private Float stature;
/** 爱好 */
@Valid
private List<AmateurDTO> amateur;
/** 博客 */
@URL
private String blogs;
……
源代码地址:UserInfoDemoDTO
2、在 Controller 中,使用添加属性验证注解的POJO类接收参数,并且在POJO类前使用@Validated 注解。
……
@PostMapping("")
public UserInfoDemoDTO addDemo(@Validated() @RequestBody UserInfoDemoDTO dto) {
return dto;
}
……
源代码地址:ValidationController
四. 枚举类型验证
细心的朋友会看到,作者对 枚举类型
的 性别字段,并未使用格式验证注解。
这是因为spring mvc 中,在spring validation
验证调用之前,会优先进行反序列化,如果请求参数不符合枚举类型规范,则会直接抛出HttpMessageConversionException
异常。
五. 嵌套注解的应用
在参数接收的实际业务场景中,我们经常会遇到需要接收对象属性的场景,如下图所示:
在这种业务场景下,仅使用@Validated
注解,是无法验证对象列表中属性值的合法性的。
这时候我们需要使用@Valid
注解的嵌套验证
功能。
首先,我们在作为列表对象的POJO类 AmateurDTO 中设置属性验证规范。
……
@NotNull(message = "兴趣爱好的id不能为空")
private Integer id;
@NotBlank(message = "兴趣爱好的名称不能为空")
@Size(max = 50, message = "兴趣爱好的长度必须在1~50之间")
private String name;
……
然后,我们在UserInfoDemoDTO示例类的 List<AmateurDTO>
类型属性amateur
上,添加@Valid
注解,如下所示:
……
/** 爱好 */
@Valid
private List<AmateurDTO> amateur;
……
最后,我们采用不合规的爱好名称请求演示API,验证提示信息如下:
六. @Validated 分组功能
@Validated
注解还支持参数验证的分组功能,可以根据分组接口类,判断验证注解是否生效。
这里作者以 “创建操作,允许ID为空;更新操作,不允许ID为空” 的场景作为演示示例
首先,我们在org.tysite.tyservice.example.validation.group
包下创建两个接口类 AddOperation
和ModifyOperation
,用于判断添加和修改操作。
/** @Validated 分组类 - 新增数据 */
public interface AddOperation {
}
/** @Validated 分组类 - 修改数据 */
public interface ModifyOperation {
}
然后,我们在POJO类GroupDemoDTO的 id
属性设置验证分组
……
@NotNull(message = "编辑操作ID不能为空", groups = ModifyOperation.class)
private Integer id;
……
最后,在Controller类ValidationGroupController中,分别传入AddOperation.class
和ModifyOperation.class
分组接口
……
@PostMapping("/add")
public GroupDemoDTO insertDemo(@Validated({AddOperation.class}) @RequestBody GroupDemoDTO dto) {
return dto;
}
@PutMapping("/update")
public GroupDemoDTO modifyDemo(@Validated({ModifyOperation.class}) @RequestBody GroupDemoDTO dto) {
return dto;
}
……
下面我们分表以添加和修改接口的调用,演示请求结果。
调用添加接口:
调用修改接口:
七. 自定义校验注解
上面的 Spring Validation
参数验证方案,基本上可以解决我们日常工作中遇到的参数验证场景,如果有其他特殊的验证需求,我们也可以自定义验证注解来实现。
这里我们可以模仿validation
官方注解的语法,实现自定义注解。
首先,通过阅读@NotBlank
、@Email
、@Digits
、@Past
等注解的源代码,我们会发现validation
的官方注解,都使用了@Constraint(validatedBy = { })
注解。
打开该注解的源代码,通过阅读源码注释,我们发现该注解要求必须包含 message()
、groups()
、payload()
三个属性。
继续阅读源代码,我们会发现源代码说明中给我们提供了Validation
自定义注解的样例代码,如下图所示:
根据以上示例代码中 @Constraint(validatedBy = OrderNumberValidator.class)
的语法规则,我们找到@NotBlank
注解的注解解析器
类NotBlankValidator
,我们会发现该注解实现了ConstraintValidator
接口。
……
public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
……
然后,阅读ConstraintValidator
接口的源代码,我们发现该接口包含 initialize
和 isValid
方法。
- initialize 方法为初始化方法,其参数为
constraintAnnotation
,我们可以通过该参数获取到注解的相关信息,如注解设置的属性值等。 - isValid 方法为验证方法,用于判断验证对象是否合规。也就是说我们可以通过实现该方法完成验证逻辑。 其参数包含验证对象
value
和约束验证器的上下文context
。
注解基本语法可参考作者博客:https://blog.csdn.net/tysite/article/details/90691368
了解了验证注解和其注解解析器的语法规则,我们就可以根据自己的业务需要,开发自己的验证注解了,如下所示:
首先,我们编写注解类 @DemoName
@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {DemoNameValidator.class})
@Documented
public @interface DemoName {
String message() default "名称的长度必须在1~20之间";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
然后,我们编写 注解解析器类 DemoNameValidator
public class DemoNameValidator implements ConstraintValidator<DemoName, CharSequence> {
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if ( value == null ) {
return false;
} else {
Integer charLength = value.toString().trim().length();
if (charLength >= 1 && charLength <= Constant.NAME_MAX) {
return true;
} else {
return false;
}
}
}
}
最后,编写验证代码并调用验证入口API ,使用不合规的参数,可以得到如下验证效果。
详细示例代码,可以通过作者开源项目tysite-service
的org.tysite.tyservice.example.validation
包中找到
https://gitee.com/tysite-web/tysite-service