【珍藏】Java Bean Validation详解及国际化集成

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【珍藏】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)@Documentedpublic @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的配置即可:
















@Configurationpublic 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后缀)。

同时可以封装一个校验的工具类:




























@Componentpublic 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来进行校验,并抛出业务异常。

至此,在使用的过程中,如果遇到对应的国际化语言变化了,那么对应的错误文案也会进行变化。

目录
相关文章
|
4月前
|
Java Maven Windows
使用Java创建集成JACOB的HTTP服务
本文介绍了如何在Java中创建一个集成JACOB的HTTP服务,使Java应用能够调用Windows的COM组件。文章详细讲解了环境配置、动态加载JACOB DLL、创建HTTP服务器、实现IP白名单及处理HTTP请求的具体步骤,帮助读者实现Java应用与Windows系统的交互。作者拥有23年编程经验,文章来源于稀土掘金。著作权归作者所有,商业转载需授权。
使用Java创建集成JACOB的HTTP服务
|
10天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
68 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
6天前
|
Java API Apache
java集成stable diffusion
通过REST API和JNI两种方法,我们可以在Java应用程序中集成Stable Diffusion模型。REST API方法更加简单和易于维护,而JNI方法则提供更高的性能。根据具体应用场景和需求,选择合适的集成方法,可以充分利用Stable Diffusion的强大功能,实现高效的图像生成和处理。
37 15
|
5月前
|
IDE Java 开发工具
Java系统中的错误码设计问题之为Java项目中的错误消息提供国际化支持如何解决
Java系统中的错误码设计问题之为Java项目中的错误消息提供国际化支持如何解决
76 0
|
3月前
|
JSON Java 开发工具
Java服务端集成Google FCM推送的注意事项和实际经验
本文分享了作者在公司APP海外发布过程中,选择Google FCM进行消息推送的集成经验。文章详细解析了Java集成FCM推送的多种实现方式,包括HTTP请求和SDK集成,并指出了通知栏消息和透传消息的区别与应用场景。同时,作者还探讨了Firebase项目的创建、配置和服务端集成的注意事项,帮助读者解决文档混乱和选择困难的问题。
138 1
|
3月前
|
安全 算法 Java
数据库信息/密码加盐加密 —— Java代码手写+集成两种方式,手把手教学!保证能用!
本文提供了在数据库中对密码等敏感信息进行加盐加密的详细教程,包括手写MD5加密算法和使用Spring Security的BCryptPasswordEncoder进行加密,并强调了使用BCryptPasswordEncoder时需要注意的Spring Security配置问题。
232 0
数据库信息/密码加盐加密 —— Java代码手写+集成两种方式,手把手教学!保证能用!
|
3月前
|
JSON Java 开发工具
Java服务端集成Google FCM推送的注意事项和实际经验
公司的app要上海外,涉及到推送功能,经过综合考虑,选择Google FCM进行消息推送。 查看一些集成博客和官方文档,看的似懂非懂,迷迷惑惑。本篇文章除了将我实际集成的经验分享出来,也会对看到的博客及其中产生的疑惑、注意事项一一评论。 从官方文档和众多博客中,你会发现Java集成FCM推送有多种实现方式,会让生产生文档很乱,不知作何选择的困惑。
123 0
|
3月前
|
XML Java 数据库
Java与XQuery在BaseX集成中的实践指南
Java与XQuery在BaseX集成中的实践指南
21 0
|
5月前
|
前端开发 Java Maven
【前端学java】全网最详细的maven安装与IDEA集成教程!
【8月更文挑战第12天】全网最详细的maven安装与IDEA集成教程!
119 2
【前端学java】全网最详细的maven安装与IDEA集成教程!
|
5月前
|
Java Devops 持续交付
探索Java中的Lambda表达式:简化代码,提升效率DevOps实践:持续集成与部署的自动化之路
【8月更文挑战第30天】本文深入探讨了Java 8中引入的Lambda表达式如何改变了我们编写和管理代码的方式。通过简化代码结构,提高开发效率,Lambda表达式已成为现代Java开发不可或缺的一部分。文章将通过实际例子展示Lambda表达式的强大功能和优雅用法。