Bean Validation入门篇----02

x33g5p2x  于2022-07-26 转载在 其他  
字(6.2k)|赞(0)|评价(0)|浏览(258)

上一篇介绍了JSR、Bean Validation、Hibernate Validator的联系和区别,本篇开始,来探究一下如何使用提供好的Bean Validation模块,来完成繁琐的数据校验功能。

声明式校验方法的参数、返回值

很多时候,我们只是一些简单的独立参数(比如方法入参int age),并不需要大动干戈的弄个Java Bean装起来,比如我希望像这样写达到相应约束效果:

public @NotNull Person getOne(@NotNull @Min(1) Integer id, String name) { ... };

下面就来探讨如何借助Bean Validation 优雅的、声明式的实现方法参数、返回值以及构造器参数、返回值的校验。

声明式除了有代码优雅、无侵入的好处之外,还有一个不可忽视的优点是:任何一个人只需要看声明就知道语义,而并不需要了解你的实现,这样使用起来也更有安全感。

Bean Validation 1.0版本只支持对Java Bean进行校验,到1.1版本就已支持到了对方法/构造方法的校验,使用的校验器便是1.1版本新增的ExecutableValidator:

public interface ExecutableValidator {

	// 方法校验:参数+返回值
	<T> Set<ConstraintViolation<T>> validateParameters(T object,
													   Method method,
													   Object[] parameterValues,
													   Class<?>... groups);
	<T> Set<ConstraintViolation<T>> validateReturnValue(T object,
														Method method,
														Object returnValue,
														Class<?>... groups);

	// 构造器校验:参数+返回值
	<T> Set<ConstraintViolation<T>> validateConstructorParameters(Constructor<? extends T> constructor,
																  Object[] parameterValues,
																  Class<?>... groups);
	<T> Set<ConstraintViolation<T>> validateConstructorReturnValue(Constructor<? extends T> constructor,
																   T createdObject,
																   Class<?>... groups);
}

其实我们对Executable这个字眼并不陌生,向JDK的接口java.lang.reflect.Executable它的唯二两个实现便是Method和Constructor,刚好和这里相呼应。

在下面的代码示例之前,先提供两个方法用于获取校验器(使用默认配置),方便后续使用:

public class ValidatorUtil {
    /**
     * 用于Java Bean校验的校验器
     */
    public static Validator obtainValidator() {
        // 1、使用【默认配置】得到一个校验工厂  这个配置可以来自于provider、SPI提供
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        // 2、得到一个校验器
        return validatorFactory.getValidator();
    }

    /**
     * 用于方法校验的校验器
     */
    public static ExecutableValidator obtainExecutableValidator() {
        return obtainValidator().forExecutables();
    }

}

因为Validator等校验器是线程安全的,因此一般来说一个应用全局仅需一份即可,因此只需要初始化一次。

校验Java Bean

先来回顾下对Java Bean的校验方式。书写JavaBean和校验程序(全部使用JSR标准API),声明上约束注解:

@Test
    public void testBeanValidator(){
        Stu stu = new Stu();
        stu.setNum(-1);
        Set<ConstraintViolation<Stu>> result = ValidatorUtil.obtainValidator().validate(stu);
        result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": "
                + v.getInvalidValue()).forEach(System.out::println);

    }

输出结果:

name 不能为null: null
num 最小不能小于0: -1

这是最经典的应用了。那么问题来了,如果你的方法参数就是个Java Bean,你该如何对它进行校验呢?

小贴士:有的人认为把约束注解标注在属性上,和标注在set方法上效果是一样的,其实不然,你有这种错觉全是因为Spring帮你处理了些东西,至于原因将在后面和Spring整合使用时展开

校验方法

我们现在来试试,使用上面提供的ExecutableValidator 完成对方法参数的校验:

public Boolean updateStu(@Min(value = 0) Integer num, @NotEmpty String name){
        System.out.println(num);
        System.out.println(name);
        return true;
    }
@Test
    public void testBeanValidator() throws NoSuchMethodException {
        Method updateStu = this.getClass().getMethod("updateStu", Integer.class, String.class);
        Set<ConstraintViolation<TestDataBinder>> validResult = ValidatorUtil.obtainExecutableValidator().validateParameters(this, updateStu, new Object[]{-1, ""});
        if (!validResult.isEmpty()) {
            // ... 输出错误详情validResult
            validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
            throw new IllegalArgumentException("参数错误");
        }
    }

完美的符合预期。不过,arg0是什么鬼?

  • Java class字节码文件中默认没有记录真实的方法参数名,而是用默认给出的arg0,arg1代替
  • 在JDK 8之后,我们可以通过在编译时指定“-parameters”选项,来实现将方法的参数名写入class文件,并在运行时通过反射机制获取对应的参数名。

Maven 工程可在pom中指定

<build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>utf8</encoding>
          <compilerArgs>
            <arg>-parameters</arg>
          </compilerArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>

对于构造方法的检验和方法返回值的校验,也是同理,这里不多展开

Java Bean作为入参如何校验?

如果一个Java Bean当方法参数,你该如何使用Bean Validation校验呢?

public  Boolean updateStu(Stu stu){
        return false;
    }
@Data
public class Stu {
    @Min(value = 0)
    Integer num;
    @NotNull
    String name;
}

进行测试:

@Test
    public void testBeanValidator() throws NoSuchMethodException {
        Method updateStu = this.getClass().getMethod("updateStu", Stu.class);
        Stu stu = new Stu();
        stu.setNum(-1);
        Set<ConstraintViolation<TestDataBinder>> validResult = ValidatorUtil.obtainExecutableValidator().validateParameters(this, updateStu, new Object[]{stu});
        if (!validResult.isEmpty()) {
            validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
            throw new IllegalArgumentException("参数错误");
        }
    }

运行程序,控制台没有输出,也就是说校验通过。很明显,刚new出来的Stu不是一个合法的模型对象,所以可以断定没有执行模型里面的校验逻辑,怎么办呢?难道仍要自己用Validator去用API校验么?

这里涉及到对级联属性的校验,因此需要使用@Valid注解,告诉校验器需要进行级联属性校验,因为默认是不会去检查级联属性的:

public  Boolean updateStu(@Valid Stu stu){
        return false;
    }

再次运行测试程序,控制台输出:

小贴士:@Valid注解用于验证级联的属性、方法参数或方法返回类型。比如你的属性仍旧是个Java Bean,你想深入进入校验它里面的约束,那就在此属性头上标注此注解即可。另外,通过使用@Valid可以实现递归验证,因此可以标注在List上,对它里面的每个对象都执行校验

题外话一句:相信有小伙伴想问@Valid和Spring提供的@Validated有啥区别,我给的答案是:完全不是一回事,纯巧合而已。至于为何这么说,后面和Spring整合使用时给你讲得明明白白的。

注解应该写在接口上还是实现上?

我们先将注解加在接口上,看看是否会生效:

public interface IStuService {
    public void update(@Valid Stu stu);
}

public class StuServiceImpl implements IStuService{
    @Override
    public void update(Stu stu) {

    }
}

测试:

@Test
    public void testBeanValidator() throws NoSuchMethodException {
        Method updateStu = IStuService.class.getMethod("update",Stu.class);
        Stu stu = new Stu();
        stu.setNum(-1);
        IStuService stuService=new StuServiceImpl();
        Set<ConstraintViolation<IStuService>> validResult = ValidatorUtil.obtainExecutableValidator().validateParameters(stuService, updateStu, new Object[]{stu});
        if (!validResult.isEmpty()) {
            validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
            throw new IllegalArgumentException("参数错误");
        }
    }

符合预期,没有任何问题.

注意

如果该方法是接口方法的实现,就必须保持和接口方法一样的约束条件(极限情况:接口没约束注解,那你也不能有),如果像下面这样使用,就会抛出异常:

public interface IStuService {
    public void update(Stu stu);
}

public class StuServiceImpl implements IStuService{
    @Override
    public void update(@NotNull Stu stu) {

    }
}

错误:

javax.validation.ConstraintDeclarationException: HV000151:
 A method overriding another method must not 
 redefine the parameter constraint configuration, 
 but method StuServiceImpl#update(Stu) redefines the configuration of IStuService#update(Stu).

对于override的方法,约束注解要加在父接口方法上

值得注意的是,在和Spring整合使用中还会涉及到一个问题:@Validated注解应该放在接口(方法)上,还是实现类(方法)上?

总结

本文讲述的是Bean Validation又一经典实用场景:校验方法的参数、返回值。后面加上和Spring的AOP整合将释放出更大的能量。

另外,通过本文你应该能再次感受到契约编程带来的好处吧,总之:能通过契约约定解决的就不要去硬编码,人生苦短,少编码多行乐。

参考

2. Bean Validation声明式校验方法的参数、返回值

相关文章