基于Spring boot的Java Bean Validation详解及国际化集成
针对web项目,对外接口的参数校验是必不可少的。如果接口参数比较少,还可以通过ifelse进行逐个校验,但如果参数比较多,这种方式来进行编写代码会变得非常冗余。
作为程序员,抽象和统一处理的能力是也是编程能力的重要指标。本篇文章就带大家基于Java Bean Validation来完成web项目参数校验的统一处理。
Bean Validation
JSR303规范是Java EE 6中的一项子规范:Bean Validation,官方参考实现是Hibernate Validator,JSR303 用于对Java Bean中的字段的值进行验证。本文也是基于Hibernate的实现来完成参数的校验。
Bean Validation为JavaBean验证定义了相应的元数据模型和API。缺省的元数据是Java Annotations,通过使用XML可以对原有的元数据信息进行覆盖和扩展。
在应用程序中,通过使用Bean Validation或是自定义的 约束(constraint),例如@NotNull,@Max,@ZipCode等来确保数据模型(JavaBean)的正确性。
constraint可以附加到字段、getter方法、类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
Hibernate Validator提供了JSR303规范中所有内置constraint 的实现,除此之外还有一些附加的constraint。常见的注解比如有@Null、@NotNull、@Min(value)、@Max(value)、@Size(max, min)等,我们这里不一一举例,关于注解的详细使用规则可参考官方文档。
基于Hibernate Validator的校验
首先在依赖文件中引入validation-api和具体实现hibernate-validator。如果是Spring Boot项目,只需引入Spring Boot Web对应的starter便都引入了。
在使用的过程中如果换其他版本则需注意hibernate-validator的高版本可能会引起找不到对应javax.el相关类的异常,此时可针对此方面进行排查。
<!-- 接口 --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <!-- 实现 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.3.6.Final</version> </dependency> <!-- EL Expression --> <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>2.2.4</version> </dependency> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>javax.el</artifactId> <version>2.2.4</version> </dependency>
此时便可以进行Java Bean的校验了。在此,通常有一个误区,以为参数的校验必须在Web层请求时,其实不然,在业务的任何一个层级都可以进行Java Bean的校验。只不过校验之后,需要进行对应的处理。
此处我们通过一个简单的测试方法即可使用这套校验机制。首先在Java Bean上添加对应约束的注解。
public class RequestParam { private String username; @Max(value = 60, message = "最大岁数不超过{value}岁") private int age; // 省略getter/setter方法 }
对应校验的工具类:
public class ValidatorUtils { private static Validator validator; static { validator = Validation.buildDefaultValidatorFactory().getValidator(); } /** * 校验对象 * * @param object 待校验对象 * @param groups 待校验的组 */ public static void validateEntity(Object object, Class<?>... groups) { Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); if (!constraintViolations.isEmpty()) { ConstraintViolation<Object> constraint = constraintViolations.iterator().next(); System.out.println(constraint.getMessage()); } } public static void main(String[] args) { RequestParam param = new RequestParam(); param.setUsername("zhangsan"); param.setAge(80); ValidatorUtils.validateEntity(param); } }
执行上述工具类中的main方法便可对Java Bean进行校验,执行之后打印结果如下:
最大岁数不超过60岁
通过上述注解及工具类的形式,我们可以在任何业务逻辑层对参数或Java Bean进行校验。同时,如果想统一处理,可在上述打印校验信息的地方抛出业务异常,然后通过拦截器统一处理。
其中注解的message中使用了{value}的占位符形式,而value的值便是该注解前面的value=60的值。
关于其他类型的注解使用方法与此相同,就不再赘述。
注解源码解析
这里以@Max注解的部分源码来解析一下校验注解的构成。
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) @Documented @Constraint(validatedBy = { }) public @interface Max { String message() default "{javax.validation.constraints.Max.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; long value(); // 省略内部的List }
其中该注解上面的一些基础注解是用限制Max的使用场景的,最主要的是@Constraint注解,用来对该注解进行约束、检查的扩展,后面自定义注解时会看到它的作用。
这种拓展方法是基于已有的标准注解,因此无需指定@Constraint(validatedBy={}),而加上该注解的作用是,让验证器实现将该注解作为一个标准约束来解析,而不是作为普通注解给忽略掉。
message属性:关于message的语法中{}为hibernate validator支持的参数化表达式。有两种使用场景,场景一:默认如源代码中,会直接获取hibernate定义的国际化消息内容,后面集成spring国际化时也使用该方式;场景二:像上面实例中演示的那样,可以自己定义message内容,同时可以使用{}来达到占位符获取对应value()属性的值。
比如这里可以定义信息为:”字符串的长度不能小于{value}。”
最终会转换为(根据min实际值替换):”字符串的长度不能小于1。”
groups属性:用于分组验证,不配置为Default组。可以根据不同的应用场景来定义一些group,比如用于执行save和update操作的Bean,在update时很多参数是非必须的,这时就可以定义两个接口,分别表示Save.class组与Update.class组。在save操作时,使用@valid(group={Save.class}),只对在Save.class标记的注解中进行约束验证。
payload属性:应用并不多,可以通过它来携带给验证器一些元数据信息,比如自定义验证器时,验证对象可以是String、也可以是Optional\<String>,这时仅仅只用@NotNull就无法正确验证了,这时候可以通过payload来标记一些需要特殊处理的操作。
value属性:该属性主要是用来传递匹配的边界值,也就是要校验的数字的最大值。可以通过{}占位符的形式来获取。
自定义注解
上面讲解了注解的使用和源代码,当基础的注解没办法满足我们的需求时,可以进行自定义注解,然后通过@Constraint约束来进行具体功能的实现。
这里以检查指定字段是否是指定类型之一为例,来实现一个注解。通常会接收到type类型,要检查type类型是否为合法的取值。
注解@In的定义:
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Constraint(validatedBy = InValidator.class) @Target({java.lang.annotation.ElementType.FIELD}) @Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @Documented public @interface In { String message() default "参数值不在指定范围内"; int[] values() default {}; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
其中用到了约束InValidator,该类需要实现接口ConstraintValidator。
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; /** * @author sec * @version 1.0 * @date 2020/7/9 10:56 上午 **/ public class InValidator implements ConstraintValidator<In, Integer> { private final Set<Integer> values = new HashSet<>(); private String msg = null; @Override public void initialize(In constraintAnnotation) { for (int value : constraintAnnotation.values()) { this.values.add(value); } String msg = values.stream().map(Object::toString).collect(Collectors.joining(",", "[", "]")); this.msg = String.format("只能取值%s", msg); } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { if (value == null) { return true; } boolean contains = values.contains(value); if (contains) { return true; } if (context.getDefaultConstraintMessageTemplate().isEmpty()) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(this.msg).addConstraintViolation(); } return false; } }
其中涉及到两个接口方法的实现,第一个initialize方法用来初始化一些校验所需的参数值,这里重点获取通过注解传入的参数值范围。第二个方法isValid用来对参数进行校验,返回false时表示校验未通过。
自定义注解的具体使用与其他注解类型,以下为在类中对应字段上的使用示例:
@In(values = {1, 2}) private Integer type;
执行之前的测试程序,当传入的参数值不为1或2时则打印如下日志:
INFO: HV000001: Hibernate Validator 5.3.6.Final 参数值不在指定范围内
说明自定义注解已生效,针对此种情况还可以采用枚举的形式来进行验证,通过在约束实现类的isValid中利用反射机制,获得到枚举的具体项的值或定义的方法,来进行比对。关于这种方式如有需要可自行尝试。
Hibernate Validation的国际化
在源代码中我们已经看到message属性可以通过占位符的形式获取对应的值,而对应的值同时又是支持国际化操作的。
如果在校验时,约束条件没有通过,那么配置的MessageInterpolator插值器会被用来当成解析器来解析这个约束中定义的消息模版, 从而得到最终的验证失败提示信息。
默认使用的插值器是org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator,它借助org.hibernate.validator.spi.resourceloading.ResourceBundleLocator来获取到国际化资源属性文件从而填充模版内容。
资源解析器默认使用的实现是PlatformResourceBundleLocator,在配置Configuration初始化的时候默认被赋值,Configuration的实现类为ConfigurationImpl,通过该类的构造方法可以看出默认会读取名称为ValidationMessages的配置文件。
private ConfigurationImpl() { // ... this.defaultResourceBundleLocator = new PlatformResourceBundleLocator("ValidationMessages"); // ... }
默认情况下读取该配置文件的路径和顺序为:从当前项目的classpath目录读取、依赖jar包中读取、去/org/hibernate/validator中获取。
上面是加载资源文件的顺序,但并不代表获取了第一个文件之后就不进行其他文件的加载了。无论怎么样,这三处的资源文件都会加载进内存的。进行占位符匹配的时候,依旧遵守这规律:最先用当前项目classpath下的资源去匹配资源占位符,若没匹配上再用下一级别的资源。
这样,当我们需要国际化支持时,只需在当前类路径下创建名称为ValidationMessages.properties的文件进行message的key和value值的定义即可。如果需支持多种语言,那么同样的命令ValidationMessages_zh_CN.properties等语言版本即可。
也就是说Hibernate Validation提供了Locale国际化的支持。
基本使用实例如:
@Min(value = 10, message = "{com.secbro.min.message}") private Integer age;
在ValidationMessages.properties的文件中定义:
com.secbro.min.message=最小值必须是{value}
Spring Boot国际化支持
默认情况下采用Hibernate Validator的国际化即可使用,但如果项目是基于Spring Boot(即spring)时,同样可以基于Spring的国际化配置。关于Spring Boot的国际化配置在上篇文章《Spring Boot实现国际化i18n功能》已经完成了集成,可以进行参考。
在此基础上,只用添加一个针对Validation的配置即可:
@Configuration public class ValidatorConfig { @Bean public LocalValidatorFactoryBean localValidatorFactoryBean() { LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("statics/i18n/messages"); messageSource.setCacheSeconds(120); // 缓存时长 localValidatorFactoryBean.setValidationMessageSource(messageSource); return localValidatorFactoryBean; } }
上述配置基于Spring Boot的国际化文件创建了LocalValidatorFactoryBean。其中Basename便是国际化文件的相对路径(注意没有.properties后缀)。
同时可以封装一个校验的工具类:
@Component public class ValidatorUtils { private static Validator validator; public ValidatorUtils(LocalValidatorFactoryBean localValidatorFactoryBean) { if (localValidatorFactoryBean != null) { validator = localValidatorFactoryBean.getValidator(); } } /** * 校验对象 * * @param object 待校验对象 * @param groups 待校验的组 * @throws RRException 校验不通过,则报RRException异常 */ public static void validateEntity(Object object, Class<?>... groups) throws RRException { Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); if (!constraintViolations.isEmpty()) { ConstraintViolation<Object> constraint = constraintViolations.iterator().next(); // throw new RRException(constraint.getMessage()); } } }
该工具类是通过注解获取到上述配置中实例化的LocalValidatorFactoryBean类,并使用其创建一个Validator,然后通过Validator来进行校验,并抛出业务异常。
至此,在使用的过程中,如果遇到对应的国际化语言变化了,那么对应的错误文案也会进行变化。
最后,关注微信公众号“程序新视界”,回复“010”,获取完整源代码。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接
本文链接:https://choupangxia.com/2020/07/09/spring-boot-java-bean-validation/