- 环境
- 简介
- 内置约束定义
- 使用依赖
- 内置约束注释详解
- 内置约束使用
- 约束验证快速失败设置
- 约束分组与排序设置
- 手动触发约束验证
- 自定义提示消息资源文件
- 自定义验证
- 总结
1 环境
Spring Boot v2.4.1, Spring v5.3.1,IntelliJ IDEA v2019.3.5,Maven v3.5, JDK 1.8, MySQL 5.6 2 简介
在大多数应用程序中,验证用户输入是一个非常常见的要求。验证数据是贯穿整个应用程序的一项常见任务,从表示层到持久层。
通常在每一层中实现相同的验证逻辑,但是这样即耗时间又易出错。为了避免在每一层都重复这些验证,并且避免验证代码与域类代码、业务逻辑代码混淆在一块,所以提出了Java Bean 验证规范与框架实现等。目前最新的规范是JSR 380,也就是我们俗称的Bean Validation 2.0。
这个JSR为Java Bean验证定义了一个元数据模型和API。默认元数据源是注释,能够通过使用XML验证覆盖和扩展元数据描述符。
Bean Validation 2.0规范使用Java8语言特性,不要求实现与8之前的Java语言版本兼容。
3 内置约束定义
Bean Validation 2.0规范定义了一小部分内置约束,全部位于javax.validation.constraints包下,包括:
- AssertFalse
- AssertTrue
- DecimalMax
- DecimalMin
- Digits
- Future
- FutureOrPresent
- Max
- Min
- Negative
- NegativeOrZero
- NotBlank
- NotEmpty
- NotNull
- Null
- Past
- PastOrPresent
- Pattern
- Positive
- PositiveOrZero
- Size
4 使用依赖
我们展示通过Maven引入相关依赖来使用,当然我们也可以通过其他的多种途径使用。
A、非Spring Boot场景下
导入标准验证API依赖项:
javax.validation validation-api 2.0.1.Final
导入验证API的参考实现依赖项:
<dependency> <groupId>org.hibernate.validatorgroupId> <artifactId>hibernate-validatorartifactId> <version>6.0.13.Finalversion>dependency>
导入表达式语言依赖项:
org.glassfish javax.el 3.0.0
B、Spring Boot场景下
导入验证starter依赖项:
org.springframework.boot spring-boot-starter-validation
在该starter中已经替开发人员将相关的依赖都包含进去了,不再需要开发人员单个引入。
5 内置约束注释详解
1、@NotNull
验证属性值是否不为null,如果是字符串时可以为空或是空白等。
2、@NotEmpty
验证属性是否为null或空;可以应用于字符串、集合、映射或数组值。如果是字符串时可以为空白。
3、@NotBlank
只能应用于文本值,并验证属性是否为null或空白。
4、@Min
验证带批注属性的值是否不小于value属性。
5、@Max
验证带注释属性的值是否不大于value属性。
6、@Size
验证带注释的属性值的大小是否介于属性min和max之间;可以应用于String、Collection、Map和array属性。
7、@Email
验证带批注的属性是否为有效的电子邮件地址。
8、@Positive和@PositiveOrZero
应用于数值,并验证它们是否严格为正数,或包含0的正数。
9、@Negative和@NegativeOrZero
应用于数值,并验证它们是否严格为负数,或包含0的负数。
10、@AssertTrue
验证带注释的属性值是否为true。
11、@AssertFalse
验证带注释的属性值是否为false。
12、@Past和@PastOrPresent
验证日期值是否在过去或过去(包括现在);可以应用于日期类型,包括java8中添加的日期类型。
13、@Future和@FutureOrPresent
验证日期值是在未来,还是在未来(包括现在);可以应用于日期类型,包括java8中添加的日期类型。
14、@Null
验证属性值是否为null。
15、@Digits
验证属性值是否为数字,可以是任意类型的数字,包括正负数、整数、浮点数等。
16、@DecimalMax和@DecimalMin
验证BigDecimal类型的属性值的最大值和最小值
17、@Pattern
验证属性值是否符合指定的正则表达式。
6 内置约束使用
我们先定义一个简单的实体类,为其每个字段添加上不同的约束注释。
package demo.model;import lombok.Data;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.validation.constraints.*;@Entity@Datapublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "姓名不能为空") private String name; @NotEmpty(message = "电话不能为空") private String phone; @NotNull(message = "地址不能为null") private String address; @Email(message = "email无效") private String email; @Min(value = 18, message = "年龄不能小于18岁") @Max(value = 150, message = "年龄不能大于150岁") @Positive(message = "年龄不能是负数") @NotNull(message = "年龄不能为null") private Integer age; @AssertTrue(message = "用户必须是活人") private Boolean isLive; @Pattern(regexp = "(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$", message = "身份证号不合法") private String idCardNo;}
再定义添加与编辑的jsp页面,在表单里输入这些字段,体验提交不同的值时各属性的约束验证结果,jsp页面如下:
再编辑用户控制器类UserController,在保存方法save中通过注释@Validated声明对接收的User对象进行验证,并接收验证的结果对象BindingResult,代码示例如:
@Slf4j@Controller@RequestMapping("user")public class UserController { private final UserJpaRepository userJpaRepository; public UserController(UserJpaRepository userJpaRepository) { this.userJpaRepository = userJpaRepository; } @PostMapping("save") public String save(@Validated User user, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { List errorMessages = new ArrayList<>(); for (FieldError fieldError : bindingResult.getFieldErrors()) { errorMessages.add(fieldError.getDefaultMessage()); } String failMessage = "用户保存失败,原因:" + StringUtils.join(errorMessages, "; "); log.info(failMessage); model.addAttribute("message", failMessage); model.addAttribute("user", user); return (null == user.getId()) ? "user/user-add" : "user/user-edit"; } else { userJpaRepository.save(user); return "redirect:/user/list"; } } //......}
当有属性的值验证不通过时,我们将所有验证不通过消息拼装成字符串返回给页面做展示。
约束验证不通过时效果如下:
其他诸如:
- 用户保存失败,原因:年龄不能是负数; 年龄不能小于18岁
- 用户保存失败,原因:email无效
约束验证不通过时的提示语,即message值。这是每一种约束注释都具备的属性,如果我们不设置将默认使用系统预设值。message的值除了直接写在注释里还可以通过绑定自定义的ValidationMessages.properties的key来展示我们配置的内容。
例如@NotNull注释,通过其定义我们得知它的message默认使用"{javax.validation.constraints.NotNull.message}"定义的内容。而这个key的定义我们可以在hibernate-validator对应的版本jar里找到,并且它还提供了国际化的语言资源包,所有提示语语言包如下所示:
中文语言资源文件部分内容如下:
此时框架会自动根据你当前环境的语言选择对应的提示语资源文件内容做展示,当然默认的提示语可能不太符合我们的业务需求以及用户体验不好,所以一般还是自定义较好。默认提示示例如下:
用户保存失败,原因:不能为空; 只能为true; 不能为null; 需要匹配正则表达式"(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$"; 不能为空
7 约束验证快速失败设置
通过第6节示例我们可看到约束验证默认情况下会去验证所有添加了约束注释的属性,即使已经存在约束验证失败的情况下也会继续验证余下的属性。
如果此时你希望有任意一个验证不通过就退出不再继续其他的属性验证了该怎么办呢?此时,我们可以通过配置Validator的快速失败返回属性来实现,示例如下:
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;@Configurationpublic class ValidatorConfiguration { @Bean public Validator validator() { /*设置validator模式为快速失败返回*/ return Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) //.addProperty("hibernate.validator.fail_fast", "true") .buildValidatorFactory().getValidator(); }}
示例代码中展示两种方式来实现,第1种是直接设置failFast(true),第2种是添加属性设置addProperty("hibernate.validator.fail_fast", "true")。HibernateValidator支持的所有属性可在
org.hibernate.validator.BaseHibernateValidatorConfiguration类中查看源代码得到。
设置快速失败返回模式后的效果:
可以看到只验证了一个电话号码,发现失败后就不再继续验证其余的属性了。
8 约束分组与排序设置
在某些情况下,我们需要对某些bean的某一组属性进行约束验证,然后再对bean的另一组属性进行约束验证,只有前一组的验证通过了才继续另一组的约束验证。
或者某些bean在添加时我们验证的是某一组属性,而在编辑时验证的又是另一组属性。
例如,假设我们有一个用户两步注册表单。在第一步中,我们要求用户提供基本信息,如名字、年龄等。当用户提交此数据时,我们只想验证这些信息。在下一步中,我们要求用户提供一些其他信息,比如地址、电话、身份证号等,我们还想验证这些信息。
(考虑用户、商品、企业等都可能存在类似情况)
如果不对属性约束进行分组我们就无法通过约束验证注释来实现,除非我们手动一个一个属性的写验证逻辑。
先定义分组的接口,假设我们定义了基本信息分组、高级信息分组以及两者合一起的完整的分组,并在完整的分组里设置好验证的顺序,先基本信息再高级信息。示例如下:
public interface BaseInfo {}public interface AdvanceInfo {}@GroupSequence({BaseInfo.class, AdvanceInfo.class})public interface CompleteInfo {}
再在我们的bean中各约束注释里设置好对应的分组,示例如下:
@Entity@Datapublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "姓名不能为空", groups = BaseInfo.class) private String name; @Min(value = 18, message = "年龄不能小于18岁", groups = BaseInfo.class) @Max(value = 150, message = "年龄不能大于150岁", groups = BaseInfo.class) @Positive(message = "年龄不能是负数", groups = BaseInfo.class) @NotNull(message = "年龄不能为null", groups = BaseInfo.class) private Integer age; @AssertTrue(message = "用户必须是活人", groups = BaseInfo.class) private Boolean isLive; @Pattern(regexp = "(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$", message = "身份证号不合法", groups = AdvanceInfo.class) private String idCardNo; @NotEmpty(message = "电话不能为空", groups = AdvanceInfo.class) private String phone; @NotNull(message = "地址不能为null", groups = AdvanceInfo.class) private String address; @Email(message = "email无效", groups = AdvanceInfo.class) private String email;}
再在我们的控制器方法上设置需要验证的分组,示例如下:
public String save(@Validated({CompleteInfo.class}) User user, BindingResult bindingResult, Model model)
@Validated({BaseInfo.class})
可以是
@Validated({AdvanceInfo.class})
或
@Validated({CompleteInfo.class})
也可以是多个分组数组
@Validated({BaseInfo.class, AdvanceInfo.class})。
实际的根据需求进行设置。并且这些分组的定义我们可以复用到其他类似的bean,像上面提到的诸如商品、企业等。
我们以@Validated({CompleteInfo.class})为例,此时会验证所有分组的属性,并且按CompleteInfo.class中定义的@GroupSequence顺序来进行验证。
假设我们把第7章中的"约束验证快速失败设置"关闭掉,再进行试验,可以看到当BaseInfo分组验证不通过时,不会再继续验证AdvanceInfo分组下的属性。如:
注意:如果bean的属性做了分组定义,那么在验证时就必须传递对应的分组名称,否则将不会进行任何验证或是只对未分组的属性进行验证。
9 手动触发约束验证
考虑一种情况,当我们的数据通过excel批量导入时,我们需要对每一条导入的数据做约束验证,此时我们可以通过手动的方式来触发验证,效果等同于通过注释。示例如下:
SetUser>> violationSet = validator.validate(if (!CollectionUtils.isEmpty(violationSet)) { List errorMessages = new ArrayList<>(); violationSet.forEach(each -> { errorMessages.add(each.getMessage()); }); String failMessage = "用户保存失败,原因:" + StringUtils.join(errorMessages, "; "); log.info(failMessage); //...}
通过给相应的组件注入我们在ValidatorConfiguration中配置的Validator,再调用它的validate方法,传递对应的分组(可选,请注意第8章末尾的红色字体内容)即可。
10 自定义提示消息资源文件
在第6章中我们提到约束验证不通过时的提示语可以通过message进行自定义,未自定义时将默认使用验证实现框架自带的,并根据当前环境语言选择对应local的ValidationMessages资源文件。
如果自定义,我们可以直接在代码里定义,但是这种方式不利于我们统一管理以及修改,如果某个提示语需要修改的话我们还得编译对应的类、打包、部署等,比较麻烦。
因此我们可以定义一个相同的资源文件在classpath下即可,例如我们定义一个ValidationMessages.properties文件,在该文件中我们定义了:
user.name.NotBlank.message=朋友用户名称不得为空白,请填写有效的名称喔
在属性约束注释上我们通过{}来引用该message key即可:
@NotBlank(message = "{user.name.NotBlank.message}", groups = BaseInfo.class)
如果name约束不通过将使用我们自定义的消息进行提示,而其他未自定义的提示语还将继续使用代码里编写的或是系统默认的。
11 自定义验证
当有时候内嵌的约束无法满足我们的约束需求、或需要复杂的约束组合、或想重用某种约束组合时,我们可以考虑自定义验证来实现。
创建自定义验证器需要我们推出自己的注释,并在bean中使用它来执行验证规则。
我们假设用户有一个类型的字段,类型必须是某些特定的值才能通过验证。
先通过@interface定义一个新的约束注释:
import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.*;/** * 用户类型自定义约束注释 * * @author Alex */@Documented@Constraint(validatedBy = UserTypeValidator.class)@Target({ElementType.METHOD, ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface UserTypeConstraint { String message() default "无效的用户类型"; Class>[] groups() default {}; Class extends Payload>[] payload() default {};}
再定义一个新的验证器,对应注释里的validatedBy:
import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;/** * 用户类型自定义约束验证器 * * @author Alex */public class UserTypeValidator implements ConstraintValidator<UserTypeConstraint, Integer> { @Override public void initialize(UserTypeConstraint constraintAnnotation) { } @Override public boolean isValid(Integer userType, ConstraintValidatorContext constraintValidatorContext) { return null != userType && (userType == User.TYPE_ADMIN || userType == User.TYPE_NORMAL); }}
最后应用到我们的bean属性上执行约束验证:
@UserTypeConstraint(groups = BaseInfo.class)private Integer type;
效果如下:
只有当输入的值为1或2时才能通过验证。
12 总结本文主要讨论标准Java验证API的简单使用,我们展示了使用javax.validation注解和API