千家信息网

蚂蚁金服宫孙:guava探究系列之优雅校验数据

发表于:2025-01-20 作者:千家信息网编辑
千家信息网最后更新 2025年01月20日,优雅校验数据-前置条件前言根据防御式编程的要求, 在日常的开发中, 总少不了对函数的各种入参做校验, 以便保证函数能按照预期的流程执行下去. 比如各种费率的值就没可能是负数, 如果费率出现负数, 所以
千家信息网最后更新 2025年01月20日蚂蚁金服宫孙:guava探究系列之优雅校验数据

优雅校验数据-前置条件

前言

根据防御式编程的要求, 在日常的开发中, 总少不了对函数的各种入参做校验, 以便保证函数能按照预期的流程执行下去. 比如各种费率的值就没可能是负数, 如果费率出现负数, 所以数据有问题, 我们需要做的事情就是把这些有问题的数据挑出来. 自己手写这些校验函数未免过于繁琐, 所幸的是我们需要的函数已经有现成的:

Guava 提供了一系列的静态方法用于校验函数和类的构造器是否符合预期, 并称其为前置条件(preconditions). 如果前置条件校验失败, 就会抛出一个指定的异常.

前置函数特征

目前的前置校验方法有如下特征:

须需要, 下面例子中的 checkArgument 函数可以替换成任何一个前置条件校验函数

  1. 这些前置方法一般接受一个布尔表达式作为入参,并判断表达是否为 true , 格式如:
Preconditions.checkArgument(a>1)// 如果表达式为false, 抛出IllegalArgumentException
  1. 除了用于判断的布尔表达式之外, 前置方法可以接受一个额外的 Object 作为入参, 在抛出异常的时候, 把 Object.toString() 作为异常信息, 如:
public enum ErrorDetail {    SC_NOT_FOUND("404", "Resource could not be fount");    // 省略部分内容    @Override    public String toString() {        return "ErrorDetail{" + "code='" + code + '\'' + ", description='" + description + '\'' + '}';    }}@Testpublic void testCheckArgument() {    Preconditions.checkArgument(1 > 2, ErrorDetail.SC_NOT_FOUND);}// 结果如下:// java.lang.IllegalArgumentException: ErrorDetail{code='404', description='Resource could not be fount'}
  1. Guava的前置表达式还支持类似 printf 函数那样的格式化输出错误信息, 只不过出于兼容性和性能的考虑, 只支持使用 %s 指示符格式化字符串, 不支持其他类型. 如:
int i=-1;checkArgument(i >= 0, "Argument was %s but expected nonnegative", i);// 结果如下:// java.lang.IllegalArgumentException: Argument was -1 but expected nonnegative

前置条件函数介绍

须注意的是, 下面介绍的 checkArgument , checkArgument , checkState 函数都有三个对应的重载函数,分别对应前文所述的三种特征, 下文不会三种函数都介绍, 只介绍标准格式的前置条件函数. 以 checkArgument 函数为例, 三个重载函数分别是(忽略函数体):

public static void checkArgument(boolean expression);public static void checkArgument(boolean expression, @Nullable Object errorMessage);public static void checkArgument(boolean expression,@Nullable String errorMessageTemplate,@Nullable Object... errorMessageArgs)

checkArgument

函数的签名如下:

public static void checkArgument(boolean expression);

入参是一个布尔表达式, 函数校验这个表达式是否为 true , 如果为 false , 抛出 IllegalArgumentException . 例子如下:

@Testpublic void testCheckArgument() {    Preconditions.checkArgument(1 > 2);}

checkNotNull

这是个泛型函数, 函数签名如下:

public static  T checkNotNull(T reference);

入参是个任意类型的对象, 函数校验这个对象是否为 null , 如果为空, 抛出 NullPointerException , 否则直接返回该对象, 所以 checkNotNull 的用法就比较有趣, 可以在调用 setter 方法前作前置校验. 例子如下:

PreconditionTest caller = new PreconditionTest();caller.setErrorDetail(Preconditions.checkNotNull(ErrorDetail.SC_INTERNAL_SERVER_ERROR));

checkState

函数签名如下:

public static void checkState(boolean expression);

看着这个函数, 我个人感觉很奇怪: 这个函数和 checkNotNull 函数功能非常相似, 实现也基本一样, 都是判断表达式是否为 true , 只是抛出的异常不一样而已, 是否有必要开发这个函数. 两个函数的实现如下:

public static void checkArgument(boolean expression) {  if (!expression) {    throw new IllegalArgumentException();  }}public static void checkState(boolean expression) {  if (!expression) {    throw new IllegalStateException();  }}

此外, 因为这两个函数相当类似, 就不展示相应例子了.

checkElementIndex

函数签名如下:

public static int checkElementIndex(int index, int size);

这个函数用于判断指定数组, 列表, 字符串的下标是否越界, index 是下标, size 是数组, 列表或字符串的长度, 下标的有效范围是 [0,数组长度)0<=index . 如果数组下标越界(即 index <0 或者 index >= size ), 那么抛出 IndexOutOfBoundsException 异常, 否则返回数组的下标, 也就是 index . 例子如下:

Preconditions.checkElementIndex("test".length(), "test".length());// 运行结果:// 抛出异常: java.lang.IndexOutOfBoundsException: index (4) must be less than size (4)Assert.assertEquals(3, Preconditions.checkElementIndex("test".length() - 1, "test".length()));// 运行结果:// 通过

checkPositionIndex

函数的签名如下:

public static int checkPositionIndex(int index, int size);

这个函数和 checkElementIndex 非常类似, 连Guava wiki的说明也基本一致(只有一个单词不同), 除了一点, checkElementIndex 函数的下标有效范围是 [0, 数组长度) , 而 checkPositionIndex 函数的下标有有效范围是 [0, 数组长度] , 即 0<=index<=size . 例子如下:

Preconditions.checkPositionIndex("test".length() + 1, "test".length());// 运行结果:// 抛出异常: java.lang.IndexOutOfBoundsException: index (5) must be less than size (4)Assert.assertEquals(4, Preconditions.checkPositionIndex("test".length(), "test".length()));// 运行结果:// 通过

checkPositionIndexes

函数的签名如下:

public static void checkPositionIndexes(int start, int end, int size);

这个函数是用于判断 [start,end] 这个范围是否是个有效范围, 即 [start, end] 是否在 [0, size] 范围内(如果 [start, end][0, size] 相同, 也认为在范围内), 如果不在, 则抛出 IndexOutOfBoundsException 异常. 例子如下:

Preconditions.checkPositionIndexes(1, 3, 2);// 运行结果:// 抛出异常: java.lang.IndexOutOfBoundsException: end index (3) must not be greater than size (2)Preconditions.checkPositionIndexes(0, 2, 2);// 运行结果:// 校验通过

前置条件在实际项目的应用

前置条件在检验条件不成交的时候抛的异常类型虽说是合情合理(比如, checkArgument 函数抛出 IllegalArgumentException ), 但是对于业务系统来说, 你抛出个 IllegalArgumentException 或者 NullPointerException , 接口调用方对于这个异常摸不着头脑, 虽说只是正常的数据问题, 还是很容易觉得接口提供方服务出了问题, 甚至还会被质疑技术不过硬. 咱们又不是底层组件, 抛个 NPE , 着实是不成体统. 基于各种有的没的的原因, 我们的业务系统在使用前置条件的时候进行了封装, 将前置条件抛出的异常进行了转换, 换成正常的业务异常, 提供完整的异常信息, 代码如下:

// 封装代码:public final class AssertUtils {        /**        * 检查条件表达式是否为真        *        * @param expression 条件表达式        * @param errDetailEnum 错误码        * @param msgTemplate 错误消息模板        * @param vars 占位符对应变量        * @throws BkmpException 条件表达式结果为假        */    public static void checkArgument(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate,                                        Object... vars) {        try {            Preconditions.checkArgument(expression);        } catch (IllegalArgumentException e) {            throw new BkmpException(errDetailEnum, msgTemplate, vars);        }    }        /**        * 检查条件表达式是否为假        *        * @param expression 条件表达式        * @param errDetailEnum 错误码        * @param msgTemplate 错误消息模板        * @param vars 占位符对应变量        * @throws BkmpException 条件表达式结果为假        */    public static void checkArgumentNotTrue(boolean expression, ErrDetailEnum errDetailEnum, String msgTemplate,                                            Object... vars) {        try {            Preconditions.checkArgument(!expression);        } catch (IllegalArgumentException e) {            throw new BkmpException(errDetailEnum, msgTemplate, vars);        }    }}// 省略其他部分的封装// 调用例子:AssertUtils.checkArgument(merchantEntity.exist(), ErrDetailEnum.DATA_NOT_EXIT, "商户不存在");

Guava Precondition vs Apache Common Validate

自古文无第一, 武无第二, 文人之间的口水战总是少不了的. 没想到这不是国人的专利, 原来国外也有文人相轻的风气: Guava wiki 在介绍完preconditions之后, 还踩了一波竞品Apache Common Validate, 认为Guava的preconditions 比Apache Common 更加清晰明了, 也更加美观, 我个人对Apache Common Validate 了解不深, 也不好随意置喙. 除了踩竞品之外, Guava wiki 还提了两点最佳实践(best practice):

  1. 使用前置条件校验的时候, 推荐每个校验条件单独一行, 这样即更了然, 出问题也更方便调试.
  2. 使用前置条件校验的时候, 尽量提供有用的错误信息, 这样可以更快地定位问题.

参考资料

  • PreconditionsExplained

总结

代码大全一书有一章是关于防御式编程的, 用于提高程序的健壮性, 主要思想是子程序应该不因传入错误数据而被破坏,要保护程序免遭非法输入数据的破坏. 而Guava的preconditions 就是实现防御式编程的有力工具呢. oh yeah!


函数 条件 表达式 结果 例子 下标 数组 范围 数据 错误 问题 运行 方法 时候 有效 格式 长度 业务 代码 信息 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 清苑职教中心计算机网络技术二 网络安全内容怎么写的又简单 徐州一站式网络技术有哪些 开源分析型数据库 中国如何维护网络安全参考文献 csharp 数据库 数据库序列起始值 qq免费电子邮箱发信服务器 如何做好全国网络安全宣传 莫纳什网络安全硕士含金量 网络技术对口升学考试题 中央网络安全和信息化的职责包括 软件开发方法以及特点 数据库字典中的位数是什么意思 安徽现代软件开发性价比 数据库聚合查询优化 国内ftp服务器 服务器如何防护工作 缅甸网络安全 华为重启服务器失败解决方法 地区 数据库 设计 视屏剪辑软件开发 怀旧服60级服务器哪个服人多 餐厅点餐系统数据库登陆 wind并购数据库导出是空的 平安银行软件开发面试 营口团市委开展网络安全宣传 方正证券 方闻千 网络安全 tcga数据库有随访信息吗 河南浪潮服务器虚拟化解决方案
0