你必须非常努力,才能干起来毫不费力。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。

✍前言

你好,我是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 {

// 方法校验:参数+返回值

Set> validateParameters(T object, Method method, Object[] parameterValues, Class>... groups); Set> validateReturnValue(T object, Method method, Object returnValue, Class>... groups);

// 构造器校验:参数+返回值

Set> validateConstructorParameters(Constructor extends T> constructor, Object[] parameterValues, Class>... groups); Set> 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> 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> 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)`的判断嘛:

```java

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> 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. 不能为null: null

java.lang.IllegalArgumentException: 参数错误

...```

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

## 校验构造方法

这个,呃,(⊙o⊙)…...自己动手玩玩吧,记得牢~

## 加餐:Java Bean作为入参如何校验?

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

```java

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> 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方法做个例子,提取一个接口出来,并且写上**所有的**约束注解:

```java

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次的代码能不要每次都还需要重复去验证吗?

---------

# 关注A哥

Author | [A哥(YourBatman)](https://www.yourbatman.cn/about)

-------- | -----

个人站点 | [www.yourbatman.cn](https://www.yourbatman.cn)

E-mail | yourbatman@qq.com

**`活跃平台`** | [![](https://img-blog.csdnimg.cn/2020071015182276.png)](https://www.yourbatman.cn) [![](https://img-blog.csdnimg.cn/20200710152015895.png)](https://github.com/yourbatman)

每日文章推荐 | [每日文章推荐](https://github.com/yourbatman/reading/issues)

java输出结果校验_2. Bean Validation声明式校验方法的参数、返回值相关推荐

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

    你必须非常努力,才能干起来毫不费力.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习.关注公众 ...

  2. java方法带参数返回值_Java方法中的参数太多,第6部分:方法返回

    java方法带参数返回值 在当前的系列文章中,我正在致力于减少调用Java方法和构造函数所需的参数数量,到目前为止,我一直专注于直接影响参数本身的方法( 自定义类型 , 参数对象 , 构建器模式 , ...

  3. java 函数参数 返回值_java中如何用函数返回值作为post提交的参数?

    1.我想实现的功能是在java程序中导入HttpURLConnection类,然后将函数的值作为post方法要提交的参数,最后显示在显示台上. 2.要用到的函数是自己写的可以显示实时计算机cpu.内存 ...

  4. 学习大数据的第13天——Java面向对象(接口、分析参数返回值的类型不同时如何解决、包以及访问权限修饰符(public、protected、默认、private))

    学习大数据的第13天--Java面向对象(接口.分析参数返回值的类型不同时如何解决.包以及访问权限修饰符(public.protected.默认.private)) 接口 接口的基本定义: 1.1.语 ...

  5. JSR 303 - Bean Validation 简介及使用方法

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 一.JSR-303简介 JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Va ...

  6. springMVC重复扫描bean导致声明式事务失效

    文章目录 1 配置文件 1.1 加载spring容器配置 1.2 加载springMvc容器配置 1.3 spring声明式事务配置 2 声明式事务失效 2.1 事务失效的原因 2.2 解决方案 3 ...

  7. Java输出语句可能遇到的中文乱码问题及解决方法。

    当我们想用Java输出中文的时候,有可能会出现乱码问题,有两种方法可以解决. 1.(这里我用的是Visual Studio Code) 点击右下角的UTF-8 选择通过编码保存 选择Simplifie ...

  8. java spring 事务_java 事务与spring 声明式事务

    Spring声明式事务让我们从复杂的事务处理中得到解脱.使得我们再也无需要去处理获得连接.关闭连接.事务提交和回滚等这些操作.再也无需要我们在与事务相关的方法中处理大量的try-catch-final ...

  9. java中注解动态传参_SpringMVC之注解、传参、返回值及拦截器

    1. 注解式开发之annotation-driven解释 (1) mvc注解驱动在哪个文件中配置? Springmvc.xml (2) 配置mvc注解驱动使用哪个标签? 2. 注解式开发之视图解析器 ...

最新文章

  1. Zend Debugger 配置
  2. 【竞赛题解】Codeforces Round #715 (Div. 2) C
  3. C# httpcookie asp.net中cookie的使用
  4. 每个时代都有每个时代的风口
  5. shell学习之循环命令
  6. python logging打印终端_python中那些小众但有用的自带标准库
  7. 给定一个净值序列,计算年化收益、最大回撤、夏普比率
  8. 无人机成测绘界“全能战士”
  9. HTML站内搜索引擎
  10. Stata Journal 2001-2019年全部期刊目录及下载链接
  11. Arcgis10.8中将三维的高程点转换为二维的高程点
  12. 服务器常见协议,网吧影视服务器常见的网络协议
  13. websocket客户端与TCP服务器连接异常
  14. 淘宝API接口系列,获取购买到的商品订单列表,订单详情,订单物流,收货地址列表,买家信息,买家token,卖出的商品订单列表
  15. RecyclerView让置顶项置顶
  16. 给五子棋加个功能叫悔棋
  17. Python3,11行代码解密摩斯电码,真実はいつもひとつ。
  18. 云南大学软件测试,云南大学《软件测试》软件测试期末报告.pdf
  19. Throughput Latency
  20. php实训ppt,php实训ppt

热门文章

  1. python第十八天
  2. oracle plsql 无法连接 报 ORA-12560: TNS:protocol adapter error
  3. 【转】NodeJS教程--基于ExpressJS框架的文件上传
  4. 【API】短信通106端口验证短信的实现
  5. java之插入排序算法
  6. Python日志模块logging高级用法
  7. Python实现局域网内屏幕广播的技术要点分析
  8. 《Python程序设计基础》在亚马逊当当京东互动出版网淘宝等各大书店上架
  9. web前端三大主流框架_web前端三大主流框架
  10. 如何将android连接到pc,如何将Android屏幕镜像到PC