From 88a1ceaf8e59c3502412d62ab7237d8c569d2e7c Mon Sep 17 00:00:00 2001 From: "hollis.zhl" Date: Mon, 15 Mar 2021 17:43:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=83=A8=E5=88=86=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_sidebar.md | 6 +- .../basics/java-basic/stop-use-enum-in-api.md | 175 ++++++++++++++++++ .../stop-using-equlas-in-bigdecimal.md | 174 +++++++++++++++++ docs/menu.md | 6 +- 4 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 docs/basics/java-basic/stop-use-enum-in-api.md create mode 100644 docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 9ae46b74..7f1def2a 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -172,7 +172,7 @@ * [枚举的线程安全性问题](/basics/java-basic/enum-thread-safe.md) - * 为什么不建议在对外接口中使用枚举 + * [为什么不建议在对外接口中使用枚举](/basics/java-basic/stop-use-enum-in-api.md) * IO @@ -396,7 +396,9 @@ * 为什么0.1+0.2不等于0.3 - * 为什么不能使用BigDecimal的equals比较大小 + * [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md) + + * 为什么不能直接使用double创建一个BigDecimal * Java 8 diff --git a/docs/basics/java-basic/stop-use-enum-in-api.md b/docs/basics/java-basic/stop-use-enum-in-api.md new file mode 100644 index 00000000..95f54aff --- /dev/null +++ b/docs/basics/java-basic/stop-use-enum-in-api.md @@ -0,0 +1,175 @@ +最近,我们的线上环境出现了一个问题,线上代码在执行过程中抛出了一个IllegalArgumentException,分析堆栈后,发现最根本的的异常是以下内容: + + java.lang.IllegalArgumentException: + No enum constant com.a.b.f.m.a.c.AType.P_M + + +大概就是以上的内容,看起来还是很简单的,提示的错误信息就是在AType这个枚举类中没有找到P_M这个枚举项。 + +于是经过排查,我们发现,在线上开始有这个异常之前,该应用依赖的一个下游系统有发布,而发布过程中是一个API包发生了变化,主要变化内容是在一个RPC接口的Response返回值类中的一个枚举参数AType中增加了P_M这个枚举项。 + +但是下游系统发布时,并未通知到我们负责的这个系统进行升级,所以就报错了。 + +我们来分析下为什么会发生这样的情况。 + +### 问题重现 + +首先,下游系统A提供了一个二方库的某一个接口的返回值中有一个参数类型是枚举类型。 + +> 一方库指的是本项目中的依赖 二方库指的是公司内部其他项目提供的依赖 三方库指的是其他组织、公司等来自第三方的依赖 + + public interface AFacadeService { + + public AResponse doSth(ARequest aRequest); + } + + public Class AResponse{ + + private Boolean success; + + private AType aType; + } + + public enum AType{ + + P_T, + + A_B + } + + +然后B系统依赖了这个二方库,并且会通过RPC远程调用的方式调用AFacadeService的doSth方法。 + + public class BService { + + @Autowired + AFacadeService aFacadeService; + + public void doSth(){ + ARequest aRequest = new ARequest(); + + AResponse aResponse = aFacadeService.doSth(aRequest); + + AType aType = aResponse.getAType(); + } + } + + +这时候,如果A和B系统依赖的都是同一个二方库的话,两者使用到的枚举AType会是同一个类,里面的枚举项也都是一致的,这种情况不会有什么问题。 + +但是,如果有一天,这个二方库做了升级,在AType这个枚举类中增加了一个新的枚举项P_M,这时候只有系统A做了升级,但是系统B并没有做升级。 + +那么A系统依赖的的AType就是这样的: + + public enum AType{ + + P_T, + + A_B, + + P_M + } + + +而B系统依赖的AType则是这样的: + + public enum AType{ + + P_T, + + A_B + } + + +这种情况下**,在B系统通过RPC调用A系统的时候,如果A系统返回的AResponse中的aType的类型位新增的P_M时候,B系统就会无法解析。一般在这种时候,RPC框架就会发生反序列化异常。导致程序被中断。** + +### 原理分析 + +这个问题的现象我们分析清楚了,那么再来看下原理是怎样的,为什么出现这样的异常呢。 + +其实这个原理也不难,这类**RPC框架大多数会采用JSON的格式进行数据传输**,也就是客户端会将返回值序列化成JSON字符串,而服务端会再将JSON字符串反序列化成一个Java对象。 + +而JSON在反序列化的过程中,对于一个枚举类型,会尝试调用对应的枚举类的valueOf方法来获取到对应的枚举。 + +而我们查看枚举类的valueOf方法的实现时,就可以发现,**如果从枚举类中找不到对应的枚举项的时候,就会抛出IllegalArgumentException**: + + public static > T valueOf(Class enumType, + String name) { + T result = enumType.enumConstantDirectory().get(name); + if (result != null) + return result; + if (name == null) + throw new NullPointerException("Name is null"); + throw new IllegalArgumentException( + "No enum constant " + enumType.getCanonicalName() + "." + name); + } + + +关于这个问题,其实在《阿里巴巴Java开发手册》中也有类似的约定: + +![-w1538][1] + +这里面规定"**对于二方库的参数可以使用枚举,但是返回值不允许使用枚举**"。这背后的思考就是本文上面提到的内容。 + +### 扩展思考 + +**为什么参数中可以有枚举?** + +不知道大家有没有想过这个问题,其实这个就和二方库的职责有点关系了。 + +一般情况下,A系统想要提供一个远程接口给别人调用的时候,就会定义一个二方库,告诉其调用方如何构造参数,调用哪个接口。 + +而这个二方库的调用方会根据其中定义的内容来进行调用。而参数的构造过程是由B系统完成的,如果B系统使用到的是一个旧的二方库,使用到的枚举自然是已有的一些,新增的就不会被用到,所以这样也不会出现问题。 + +比如前面的例子,B系统在调用A系统的时候,构造参数的时候使用到AType的时候就只有P_T和A_B两个选项,虽然A系统已经支持P_M了,但是B系统并没有使用到。 + +如果B系统想要使用P_M,那么就需要对该二方库进行升级。 + +但是,返回值就不一样了,返回值并不受客户端控制,服务端返回什么内容是根据他自己依赖的二方库决定的。 + +但是,其实相比较于手册中的规定,**我更加倾向于,在RPC的接口中入参和出参都不要使用枚举。** + +一般,我们要使用枚举都是有几个考虑: + +* 1、枚举严格控制下游系统的传入内容,避免非法字符。 + +* 2、方便下游系统知道都可以传哪些值,不容易出错。 + +不可否认,使用枚举确实有一些好处,但是我不建议使用主要有以下原因: + +* 1、如果二方库升级,并且删除了一个枚举中的部分枚举项,那么入参中使用枚举也会出现问题,调用方将无法识别该枚举项。 + +* 2、有的时候,上下游系统有多个,如C系统通过B系统间接调用A系统,A系统的参数是由C系统传过来的,B系统只是做了一个参数的转换与组装。这种情况下,一旦A系统的二方库升级,那么B和C都要同时升级,任何一个不升级都将无法兼容。 + +**我其实建议大家在接口中使用字符串代替枚举**,相比较于枚举这种强类型,字符串算是一种弱类型。 + +如果使用字符串代替RPC接口中的枚举,那么就可以避免上面我们提到的两个问题,上游系统只需要传递字符串就行了,而具体的值的合法性,只需要在A系统内自己进行校验就可以了。 + +**为了方便调用者使用,可以使用javadoc的@see注解表明这个字符串字段的取值从那个枚举中获取。** + + public Class AResponse{ + + private Boolean success; + + /** + * @see AType + */ + private String aType; + } + + +对于像阿里这种比较庞大的互联网公司,**随便提供出去的一个接口,可能有上百个调用方**,而接口升级也是常态,**我们根本做不到每次二方库升级之后要求所有调用者跟着一起升级**,这是完全不现实的,并且对于有些调用者来说,他用不到新特性,完全没必要做升级。 + +还有一种看起来比较特殊,但是实际上比较常见的情况,就是有的时候一个接口的声明在A包中,而一些枚举常量定义在B包中,比较常见的就是阿里的交易相关的信息,订单分很多层次,每次引入一个包的同时都需要引入几十个包。 + +对于调用者来说,我肯定是不希望我的系统引入太多的依赖的,**一方面依赖多了会导致应用的编译过程很慢,并且很容易出现依赖冲突问题。** + +所以,在调用下游接口的时候,如果参数中字段的类型是枚举的话,那我没办法,必须得依赖他的二方库。但是如果不是枚举,只是一个字符串,那我就可以选择不依赖。 + +所以,我们在定义接口的时候,会尽量避免使用枚举这种强类型。规范中规定在返回值中不允许使用,而我自己要求更高,就是即使在接口的入参中我也很少使用。 + +最后,我只是不建议在对外提供的接口的出入参中使用枚举,并不是说彻底不要用枚举,我之前很多文章也提到过,枚举有很多好处,我在代码中也经常使用。所以,切不可因噎废食。 + +当然,文中的观点仅代表我个人,具体是是不是适用其他人,其他场景或者其他公司的实践,需要读者们自行分辨下,建议大家在使用的时候可以多思考一下。 + + [1]: https://www.hollischuang.com/wp-content/uploads/2020/11/16066271055035-scaled.jpg \ No newline at end of file diff --git a/docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md b/docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md new file mode 100644 index 00000000..15895f69 --- /dev/null +++ b/docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md @@ -0,0 +1,174 @@ +BigDecimal,相信对于很多人来说都不陌生,很多人都知道他的用法,这是一种java.math包中提供的一种可以用来进行精确运算的类型。 + +很多人都知道,在进行金额表示、金额计算等场景,不能使用double、float等类型,而是要使用对精度支持的更好的BigDecimal。 + +所以,很多支付、电商、金融等业务中,BigDecimal的使用非常频繁。而且不得不说这是一个非常好用的类,其内部自带了很多方法,如加,减,乘,除等运算方法都是可以直接调用的。 + +除了需要用BigDecimal表示数字和进行数字运算以外,代码中还经常需要对于数字进行相等判断。 + +关于这个知识点,在最新版的《阿里巴巴Java开发手册》中也有说明: + +![][1] + +这背后的思考是什么呢? + +我在之前的CodeReview中,看到过以下这样的低级错误: + + if(bigDecimal == bigDecimal1){ + // 两个数相等 + } + + +这种错误,相信聪明的读者一眼就可以看出问题,**因为BigDecimal是对象,所以不能用`==`来判断两个数字的值是否相等。** + +以上这种问题,在有一定的经验之后,还是可以避免的,但是聪明的读者,看一下以下这行代码,你觉得他有问题吗: + + if(bigDecimal.equals(bigDecimal1)){ + // 两个数相等 + } + + +可以明确的告诉大家,以上这种写法,可能得到的结果和你预想的不一样! + +先来做个实验,运行以下代码: + + BigDecimal bigDecimal = new BigDecimal(1); + BigDecimal bigDecimal1 = new BigDecimal(1); + System.out.println(bigDecimal.equals(bigDecimal1)); + + + BigDecimal bigDecimal2 = new BigDecimal(1); + BigDecimal bigDecimal3 = new BigDecimal(1.0); + System.out.println(bigDecimal2.equals(bigDecimal3)); + + + BigDecimal bigDecimal4 = new BigDecimal("1"); + BigDecimal bigDecimal5 = new BigDecimal("1.0"); + System.out.println(bigDecimal4.equals(bigDecimal5)); + + +以上代码,输出结果为: + + true + true + false + + +### BigDecimal的equals原理 + +通过以上代码示例,我们发现,在使用BigDecimal的equals方法对1和1.0进行比较的时候,有的时候是true(当使用int、double定义BigDecimal时),有的时候是false(当使用String定义BigDecimal时)。 + +那么,为什么会出现这样的情况呢,我们先来看下BigDecimal的equals方法。 + +在BigDecimal的JavaDoc中其实已经解释了其中原因: + + Compares this BigDecimal with the specified Object for equality. Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method) + + +大概意思就是,**equals方法和compareTo并不一样,equals方法会比较两部分内容,分别是值(value)和标度(scale)** + + +对应的代码如下: + +![][2] + +所以,我们以上代码定义出来的两个BigDecimal对象(bigDecimal4和bigDecimal5)的标度是不一样的,所以使用equals比较的结果就是false了。 + +尝试着对代码进行debug,在debug的过程中我们也可以看到bigDecimal4的标度时0,而bigDecimal5的标度是1。 + +![][3] + +到这里,我们大概解释清楚了,之所以equals比较bigDecimal4和bigDecimal5的结果是false,是因为标度不同。 + +那么,为什么标度不同呢?为什么bigDecimal2和bigDecimal3的标度是一样的(当使用int、double定义BigDecimal时),而bigDecimal4和bigDecimal5却不一样(当使用String定义BigDecimal时)呢? + +### 为什么标度不同 + +这个就涉及到BigDecimal的标度问题了,这个问题其实是比较复杂的,由于不是本文的重点,这里面就简单介绍一下吧。大家感兴趣的话,后面单独讲。 + +首先,BigDecimal一共有以下4个构造方法: + + BigDecimal(int) + BigDecimal(double) + BigDecimal(long) + BigDecimal(String) + + +以上四个方法,创建出来的的BigDecimal的标度是不同的。 + +#### BigDecimal(long) 和BigDecimal(int) + +首先,最简单的就是**BigDecimal(long) 和BigDecimal(int),因为是整数,所以标度就是0** : + + public BigDecimal(int val) { + this.intCompact = val; + this.scale = 0; + this.intVal = null; + } + + public BigDecimal(long val) { + this.intCompact = val; + this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null; + this.scale = 0; + } + + +#### BigDecimal(double) + +而对于BigDecimal(double) ,**当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是整好等于0.1的,而是0.1000000000000000055511151231257827021181583404541015625 。这是因为doule自身表示的只是一个近似值。** + +那么,无论我们使用new BigDecimal(0.1)还是new BigDecimal(0.10)定义,他的近似值都是0.1000000000000000055511151231257827021181583404541015625这个,那么他的标度就是这个数字的位数,即55。 + +![][4] + +其他的浮点数也同样的道理。对于new BigDecimal(1.0)这样的形式来说,因为他本质上也是个整数,所以他创建出来的数字的标度就是0。 + +所以,因为BigDecimal(1.0)和BigDecimal(1.00)的标度是一样的,所以在使用equals方法比较的时候,得到的结果就是true。 + +#### BigDecimal(string) + +而对于BigDecimal(double) ,**当我们使用new BigDecimal("0.1")创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。那么他的标度也就是1。** + +如果使用new BigDecimal("0.10000"),那么创建出来的数就是0.10000,标度也就是5。 + +所以,因为BigDecimal("1.0")和BigDecimal("1.00")的标度不一样,所以在使用equals方法比较的时候,得到的结果就是false。 + +### 如何比较BigDecimal + +前面,我们解释了BigDecimal的equals方法,其实不只是会比较数字的值,还会对其标度进行比较。 + +所以,当我们使用equals方法判断判断两个数是否相等的时候,是极其严格的。 + +那么,如果我们只想判断两个BigDecimal的值是否相等,那么该如何判断呢? + +**BigDecimal中提供了compareTo方法,这个方法就可以只比较两个数字的值,如果两个数相等,则返回0。** + + BigDecimal bigDecimal4 = new BigDecimal("1"); + BigDecimal bigDecimal5 = new BigDecimal("1.0000"); + System.out.println(bigDecimal4.compareTo(bigDecimal5)); + + +以上代码,输出结果: + + 0 + + +其源码如下: + +![][5] + +### 总结 + +BigDecimal是一个非常好用的表示高精度数字的类,其中提供了很多丰富的方法。 + +但是,他的equals方法使用的时候需要谨慎,因为他在比较的时候,不仅比较两个数字的值,还会比较他们的标度,只要这两个因素有一个是不相等的,那么结果也是false、 + +如果读者想要对两个BigDecimal的数值进行比较的话,可以使用compareTo方法。 + + + [1]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004945569932.jpg + [2]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004955317132.jpg + [3]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004956382289.jpg + [4]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004965161081.jpg + [5]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004972460075.jpg + [6]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004976158870.jpg \ No newline at end of file diff --git a/docs/menu.md b/docs/menu.md index b309a49d..a5569048 100644 --- a/docs/menu.md +++ b/docs/menu.md @@ -229,7 +229,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) * [枚举的线程安全性问题](/basics/java-basic/enum-thread-safe.md) - * 为什么不建议在对外接口中使用枚举 + * [为什么不建议在对外接口中使用枚举](/basics/java-basic/stop-use-enum-in-api.md) * IO @@ -453,7 +453,9 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer) * 为什么0.1+0.2不等于0.3 - * 为什么不能使用BigDecimal的equals比较大小 + * [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md) + + * 为什么不能直接使用double创建一个BigDecimal * Java 8