唯一真实


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

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

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

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

版本约定

  • Bean Validation版本:2.0.2
  • Hibernate Validator版本:6.1.5.Final

✍正文

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,刚好和这里相呼应。

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

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

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

校验Java Bean

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

@ToString @Setter @Getter public class Person { @NotNull public String name; @NotNull @Min(0) public Integer age; } @Test public void test1() { Validator validator = obtainValidator(); Person person = new Person(); person.setAge(-1); Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出校验结果 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); }

运行程序,控制台输出:

name 不能为null: null age 需要在1和18之间: -1

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

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

校验方法

对方法的校验是本文的重点。比如我有个Service如下:

public class PersonService { public Person getOne(Integer id, String name) { return null; } }

现在对该方法的执行,有如下约束要求:

  1. id是必传(不为null)且最小值为1,但对name没有要求
  2. 返回值不能为null

下面分为校验方法参数和校验返回值两部分分别展开。

校验方法参数

如上,getOne方法有两个入参,我们需要对id这个参数做校验。如果不使用Bean Validation的话代码就需要这么写校验逻辑:

public Person getOne(Integer id, String name) { if (id == null) { throw new IllegalArgumentException("id不能为null"); } if (id < 1) { throw new IllegalArgumentException("id必须大于等于1"); } return null; }

这么写固然是没毛病的,但是它的弊端也非常明显:

  1. 这类代码没啥营养,如果校验逻辑稍微多点就会显得臭长臭长的
  2. 不看你的执行逻辑,调用者无法知道你的语义。比如它并不知道id是传还是不传也行,没有形成契约
  3. 代码侵入性强

优化方案

既然学习了Bean Validation,关于校验方面的工作交给更专业的它当然更加优雅:

public Person getOne(@NotNull @Min(1) Integer id, String name) throws NoSuchMethodException { // 校验逻辑 Method currMethod = this.getClass().getMethod("getOne", Integer.class, String.class); Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateParameters(this, currMethod, new Object[]{id, name}); if (!validResult.isEmpty()) { // ... 输出错误详情validResult validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); throw new IllegalArgumentException("参数错误"); } return null; }

测试程序就很简单喽:

@Test public void test2() throws NoSuchMethodException { new PersonService().getOne(0, "A哥"); }

运行程序,控制台输出:

getOne.arg0 最小不能小于1: 0 java.lang.IllegalArgumentException: 参数错误 ...

完美的符合预期。不过,arg0是什么鬼?如果你有兴趣可以自行加上编译参数-parameters再运行试试,有惊喜哦~

通过把约束规则用注解写上去,成功的解决上面3个问题中的两个,特别是声明式约束解决问题3,这对于平时开发效率的提升是很有帮助的,因为契约已形成

此外还剩一个问题:代码侵入性强。是的,相比起来校验的逻辑依旧写在了方法体里面,但一聊到如何解决代码侵入问题,相信不用我说都能想到AOP。一般来说,我们有两种AOP方式供以使用:

  1. 基于Java EE的@Inteceptors实现
  2. 基于Spring Framework实现

显然,前者是Java官方的标准技术,而后者是实际的标准,所以这个小问题先mark下来,等到后面讲到Bean Validation和Spring整合使用时再杀回来吧。

校验方法返回值

相较于方法参数,返回值的校验可能很多人没听过没用过,或者接触得非常少。其实从原则上来讲,一个方法理应对其输入输出负责的:有效的输入,明确的输出,这种明确就最好是有约束的。

上面的getOne方法题目要求返回值不能为null。若通过硬编码方式校验,无非就是在return之前来个if(result == null)的判断嘛:

public Person getOne(Integer id, String name) throws NoSuchMethodException { // ... 模拟逻辑执行,得到一个result结果,准备返回 Person result = null; // 在结果返回之前校验 if (result == null) { throw new IllegalArgumentException("返回结果不能为null"); } return result; }

同样的,这种代码依旧有如下三个问题:

  1. 这类代码没啥营养,如果校验逻辑稍微多点就会显得臭长臭长的
  2. 不看你的执行逻辑,调用者无法知道你的语义。比如调用者不知道返回是是否可能为null,没有形成契约
  3. 代码侵入性强

优化方案

话不多说,直接上代码。

public @NotNull Person getOne(@NotNull @Min(1) Integer id, String name) throws NoSuchMethodException { // ... 模拟逻辑执行,得到一个result Person result = null; // 在结果返回之前校验 Method currMethod = this.getClass().getMethod("getOne", Integer.class, String.class); Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateReturnValue(this, currMethod, result); if (!validResult.isEmpty()) { // ... 输出错误详情validResult validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); throw new IllegalArgumentException("参数错误"); } return result; }

书写测试代码:

@Test public void test2() throws NoSuchMethodException { // 看到没 IDEA自动帮你前面加了个notNull @NotNull Person result = new PersonService().getOne(1, "A哥"); }

运行程序,控制台输出:

getOne.<return value> 不能为null: null java.lang.IllegalArgumentException: 参数错误 ...

这里面有个小细节:当你调用getOne方法,让IDEA自动帮你填充返回值时,前面把校验规则也给你显示出来了,这就是契约。明明白白的,拿到这样的result你是不是可以非常放心的使用,不再战战兢兢的啥都来个if(xxx !=null)的判断了呢?这就是契约编程的力量,在团队内能指数级的提升编程效率,试试吧~


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM