上一篇介绍了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的校验方式。书写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是什么鬼?
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当方法参数,你该如何使用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整合将释放出更大的能量。
另外,通过本文你应该能再次感受到契约编程带来的好处吧,总之:能通过契约约定解决的就不要去硬编码,人生苦短,少编码多行乐。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://cjdhy.blog.csdn.net/article/details/125940295
内容来源于网络,如有侵权,请联系作者删除!