2. Bean Validation声明式校验方法的参数、返回值
你必须非常努力,才能干起来毫不费力。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。
✍前言
你好,我是YourBatman。
上篇文章 完整的介绍了JSR、Bean Validation、Hibernate Validator的联系和区别,并且代码演示了如何进行基于注解的Java Bean校验,自此我们可以在Java世界进行更完美的契约式编程了,不可谓不方便。
但是你是否考虑过这个问题:很多时候,我们只是一些简单的独立参数(比如方法入参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;
}
}
现在对该方法的执行,有如下约束要求:
- id是必传(不为null)且最小值为1,但对name没有要求
- 返回值不能为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;
}
这么写固然是没毛病的,但是它的弊端也非常明显:
- 这类代码没啥营养,如果校验逻辑稍微多点就会显得臭长臭长的
- 不看你的执行逻辑,调用者无法知道你的语义。比如它并不知道id是传还是不传也行,没有形成契约
- 代码侵入性强
优化方案
既然学习了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方式供以使用:
- 基于Java EE的@Inteceptors实现
- 基于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;
}
同样的,这种代码依旧有如下三个问题:
- 这类代码没啥营养,如果校验逻辑稍微多点就会显得臭长臭长的
- 不看你的执行逻辑,调用者无法知道你的语义。比如调用者不知道返回是是否可能为null,没有形成契约
- 代码侵入性强
优化方案
话不多说,直接上代码。
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)
的判断了呢?这就是契约编程的力量,在团队内能指数级的提升编程效率,试试吧~
校验构造方法
这个,呃,(⊙o⊙)…...自己动手玩玩吧,记得牢~
加餐:Java Bean作为入参如何校验?
如果一个Java Bean当方法参数,你该如何使用Bean Validation校验呢?
public void save(Person person) {
}
约束上可以提出如下合理要求:
- person不能为null
- 是个合法的person模型。换句话说:person里面的那些校验规则你都得遵守喽
对save方法加上校验如下:
public void save(@NotNull Person person) throws NoSuchMethodException {
Method currMethod = this.getClass().getMethod("save", Person.class);
Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateParameters(this, currMethod, new Object[]{person});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
throw new IllegalArgumentException("参数错误");
}
}
书写测试程序:
@Test
public void test3() throws NoSuchMethodException {
// save.arg0 不能为null: null
// new PersonService().save(null);
new PersonService().save(new Person());
}
运行程序,控制台没有输出,也就是说校验通过。很明显,刚new出来的Person不是一个合法的模型对象,所以可以断定没有执行模型里面的校验逻辑,怎么办呢?难道仍要自己用Validator去用API校验麽?
好拉,不卖关子了,这个时候就清楚大名鼎鼎的@Valid
注解喽,标注如下:
public void save(@NotNull @Valid Person person) throws NoSuchMethodException { ... }
再次运行测试程序,控制台输出:
save.arg0.name 不能为null: null
save.arg0.age 不能为null: null
java.lang.IllegalArgumentException: 参数错误
...
这才是真的完美了。
小贴士:
@Valid
注解用于验证级联的属性、方法参数或方法返回类型。比如你的属性仍旧是个Java Bean,你想深入进入校验它里面的约束,那就在此属性头上标注此注解即可。另外,通过使用@Valid可以实现递归验证,因此可以标注在List上,对它里面的每个对象都执行校验
题外话一句:相信有小伙伴想问@Valid和Spring提供的@Validated有啥区别,我给的答案是:完全不是一回事,纯巧合而已。至于为何这么说,后面和Spring整合使用时给你讲得明明白白的。
加餐2:注解应该写在接口上还是实现上?
这是之前我面试时比较喜欢问的一个面试题,因为我认为这个题目的实用性还是比较大的。下面我们针对上面的save方法做个例子,提取一个接口出来,并且写上所有的约束注解:
public interface PersonInterface {
void save(@NotNull @Valid Person person) throws NoSuchMethodException;
}
子类实现,一个注解都不写:
public class PersonService implements PersonInterface {
@Override
public void save(Person person) throws NoSuchMethodException {
... // 方法体代码同上,略
}
}
测试程序也同上,为:
@Test
public void test3() throws NoSuchMethodException {
// save.arg0 不能为null: null
// new PersonService().save(null);
new PersonService().save(new Person());
}
运行程序,控制台输出:
save.arg0.name 不能为null: null
save.arg0.age 不能为null: null
java.lang.IllegalArgumentException: 参数错误
...
符合预期,没有任何问题。这还没完,还有很多组合方式呢,比如:约束注解全写在实现类上;实现类比接口少;比接口多......
限于篇幅,文章里对试验过程我就不贴出来了,直接给你扔结论吧:
- 如果该方法是接口方法的实现,那么可存在如下两种case(这两种case的公用逻辑:约束规则以接口为准,有几个就生效几个,没有就没有):
- 保持和接口方法一毛一样的约束条件(极限情况:接口没约束注解,那你也不能有)
- 实现类一个都不写约束条件,结果就是接口里有约束就有,没约束就没有
- 如果该方法不是接口方法的实现,那就很简单了:该咋地就咋地
值得注意的是,在和Spring整合使用中还会涉及到一个问题:@Validated注解应该放在接口(方法)上,还是实现类(方法)上?你不妨可以自己先想想呢,答案那必然是后面分享喽。
✍总结
本文讲述的是Bean Validation又一经典实用场景:校验方法的参数、返回值。后面加上和Spring的AOP整合将释放出更大的能量。
另外,通过本文你应该能再次感受到契约编程带来的好处吧,总之:能通过契约约定解决的就不要去硬编码,人生苦短,少编码多行乐。
最后,提个小问题哈:你觉得是代码量越多越安全,还是越少越健壮呢?被验证过100次的代码能不要每次都还需要重复去验证吗?
推荐阅读:
2. Bean Validation声明式校验方法的参数、返回值的更多相关文章
- 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类
1024,代码改变世界.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习.关注公众号[BAT的 ...
- SpringMVC之声明式校验
1.在http://www.cnblogs.com/wtzl/p/8830678.html编程式校验基础上 2.新增jar包三个 3.StudentModel.java(声明式) package 声明 ...
- 单元测试时候使用[ClassInitialize]会该方法必须是静态的公共方法,不返回值并且应采用一个TestContext类型的参数报错的解决办法
using Microsoft.VisualStudio.TestTools.UnitTesting; 如果该DLL应用的是 C:\Program Files\Microsoft Visual Stu ...
- Java学习笔记13---如何理解“子类重写父类方法时,返回值若为类类型,则必须与父类返回值类型相同或为其子类”
子类重新实现父类的方法称重写:重写时可以修改访问权限修饰符和返回值,方法名和参数类型及个数都不可以修改:仅当返回值为类类型时,重写的方法才可以修改返回值类型,且必须是父类方法返回值的子类:要么就不修改 ...
- 创建一个接口Shape,其中有抽象方法area,类Circle 、Rectangle实现area方法计算其面积并返回。又有Star实现Shape的area方法,其返回值是0,Star类另有一返回值boolean型方法isStar;在main方法里创建一个Vector,根据随机数的不同向其中加入Shape的不同子类对象(如是1,生成Circle对象;如是2,生成Rectangle对象;如是3,生成S
题目补充: 创建一个接口Shape,其中有抽象方法area,类Circle .Rectangle实现area方法计算其面积并返回. 又有Star实现Shape的area方法,其返回值是0,Star类另 ...
- mybatis返回list很智能很简答的,只需要配置resultmap进行类型转换,你dao方法直接写返回值list<对应的object>就行了啊
mybatis返回list很智能很简答的,只需要配置resultmap进行类型转换,你dao方法直接写返回值list<对应的object>就行了啊 dao方法 public List< ...
- 获取的ajax方法return的返回值的问题解析
今天刚上班就偶遇关于获取Ajax方法return的返回值的问题,这里小记一下. 在使用jquery中,如果获取不到ajax返回值,原因有二: 一.ajax未使用同步 ajax未使用同步,导致数据未加载 ...
- JSF页面中使用js函数回调后台bean方法并获取返回值的方法
由于primefaces在国内使用的并不是太多,因此,国内对jsf做系统.详细的介绍的资料很少,即使有一些资料,也仅仅是对国外资料的简单翻译或者是仅仅讲表面现象(皮毛而已),它们的语句甚至还是错误的, ...
- 用jquery的ajax方法获取return返回值的正确姿势
如果jquery中,想要获取ajax的return返回值,必须注意两方面,ajax的同步异步问题,在ajax方法里面还是外面进行return返回值. 下面列举了三种写法,如果想成功获取到返回值,参考第 ...
随机推荐
- SSM三大框架的整合
好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 在Java后端开发领域,Spri ...
- Vue组件注册
全局注册方法 Vue.component('my-component-name', { // ... 选项 ... }) Vue.component('component-a', { /* ... * ...
- 代码生成器插件与Creator预制体文件解析
前言 之前写过一篇自动生成脚本的工具,但是我给它起名叫半自动代码生成器.之所以称之为半自动,因为我觉得全自动代码生成器应该做到两点:代码生成+自动绑定.之前的工具只做了代码生成,并没有做自动绑定,所以 ...
- PMP各种图比较记忆
1.控制图:监控过程是否稳定,是否具有可预测的绩效,在问题还未发生时解决.需要关注控制图中的平均值.控制界限.规格界限的含义.控制上.下限一般设为±3个西格玛.过程失控的情况包括数据点在控制界限外,以 ...
- HttpClient 模拟用户操作
首先模拟用户登录: /** * 模拟用户登录 * */ private void login() throws HttpException, IOException { PostMethod logi ...
- scss @mixin & @include
定义一个带参数和默认值的mixin class // demo.scss @mixin button($background:#606266) { font-size: 1em; padding: 0 ...
- ES6语法学习(一)-let和const
1.let 和 const 变量提升: 在声明变量或者函数时,被声明的变量和函数会被提升到函数最顶部: 但是如果声明的变量或者函数被初始化了,则会失去变量提升: 示例代码: param2 = &quo ...
- 一步一步讲解如何安装Ubuntu18.04,零基础
在一块空的硬盘上安装Ubuntu是最为简单的,我接下将介绍如何进行安装 1.准备 Ubuntu镜像,b( ̄▽ ̄)d 这个是肯定yaod Rufus,一个写入镜像的工具' U盘一个 2.开始 2.1.写 ...
- 准确率99.9%的离线IP地址定位库
Ip2region是什么? ip2region - 准确率99.9%的离线IP地址定位库,0.0x毫秒级查询,ip2region.db数据库只有数MB,提供了java,php,c,python,nod ...
- 浏览器自动化的一些体会11 webclient的异步操作
原来的代码大致如下: private void foo(string url) { using (WebClient client = new WebClient()) { client.Downlo ...