针对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”,获取完整源代码。



基于Spring boot的Java Bean Validation详解及国际化集成插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2020/07/09/spring-boot-java-bean-validation/