GitHub 18k Star 的Java工程师成神之路,不来了解一下吗!java
GitHub 18k Star 的Java工程师成神之路,真的不来了解一下吗!git
GitHub 18k Star 的Java工程师成神之路,真的真的不来了解一下吗!程序员
最近,咱们的线上环境出现了一个问题,线上代码在执行过程当中抛出了一个IllegalArgumentException,分析堆栈后,发现最根本的的异常是如下内容:github
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这个枚举项。code
可是下游系统发布时,并未通知到咱们负责的这个系统进行升级,因此就报错了。对象
咱们来分析下为何会发生这样的状况。blog
首先,下游系统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 extends Enum<T>> T valueOf(Class<T> 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开发手册》中也有相似的约定:

这里面规定"对于二方库的参数可使用枚举,可是返回值不容许使用枚举"。这背后的思考就是本文上面提到的内容。
为何参数中能够有枚举?
不知道你们有没有想过这个问题,其实这个就和二方库的职责有点关系了。
通常状况下,A系统想要提供一个远程接口给别人调用的时候,就会定义一个二方库,告诉其调用方如何构造参数,调用哪一个接口。
而这个二方库的调用方会根据其中定义的内容来进行调用。而参数的构造过程是由B系统完成的,若是B系统使用到的是一个旧的二方库,使用到的枚举天然是已有的一些,新增的就不会被用到,因此这样也不会出现问题。
好比前面的例子,B系统在调用A系统的时候,构造参数的时候使用到AType的时候就只有P_T和A_B两个选项,虽然A系统已经支持P_M了,可是B系统并无使用到。
若是B系统想要使用P_M,那么就须要对该二方库进行升级。
可是,返回值就不同了,返回值并不受客户端控制,服务端返回什么内容是根据他本身依赖的二方库决定的。
可是,其实相比较于手册中的规定,我更加倾向于,在RPC的接口中入参和出参都不要使用枚举。
通常,咱们要使用枚举都是有几个考虑:
一、枚举严格控制下游系统的传入内容,避免非法字符。
二、方便下游系统知道均可以传哪些值,不容易出错。
不能否认,使用枚举确实有一些好处,可是我不建议使用主要有如下缘由:
一、若是二方库升级,而且删除了一个枚举中的部分枚举项,那么入参中使用枚举也会出现问题,调用方将没法识别该枚举项。
二、有的时候,上下游系统有多个,如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包中,比较常见的就是阿里的交易相关的信息,订单分不少层次,每次引入一个包的同时都须要引入几十个包。
对于调用者来讲,我确定是不但愿个人系统引入太多的依赖的,一方面依赖多了会致使应用的编译过程很慢,而且很容易出现依赖冲突问题。
因此,在调用下游接口的时候,若是参数中字段的类型是枚举的话,那我没办法,必须得依赖他的二方库。可是若是不是枚举,只是一个字符串,那我就能够选择不依赖。
因此,咱们在定义接口的时候,会尽可能避免使用枚举这种强类型。规范中规定在返回值中不容许使用,而我本身要求更高,就是即便在接口的入参中我也不多使用。
最后,我只是不建议在对外提供的接口的出入参中使用枚举,并非说完全不要用枚举,我以前不少文章也提到过,枚举有不少好处,我在代码中也常用。因此,切不可因噎废食。
固然,文中的观点仅表明我我的,具体是是否是适用其余人,其余场景或者其余公司的实践,须要读者们自行分辨下,建议你们在使用的时候能够多思考一下。
关于做者:Hollis,一个对Coding有着独特追求的人,阿里巴巴技术专家,《程序员的三门课》联合做者,《Java工程师成神之路》系列文章做者。
若是您有任何意见、建议,或者想与做者交流,均可以关注公众号【Hollis】,直接后台给我留言。
本文由博客一文多发平台 OpenWrite 发布!