最近,我们的网络环境出现了问题。在线代码在执行过程中抛出IllegalArgumentException。分析堆栈后,我们发现最根本的异常是:java.lang.IllegalArgumentException:Noenumconstantcom。a.b.f.m.a.c.AType.P_M大概就是上面的内容,看起来很简单,报错信息是在枚举类AType中找不到枚举项P_M。所以经过排查,我们发现在异常开始出现在线上之前,应用所依赖的一个下游系统被发布了,发布过程中有一个API包发生了变化。主要变化是RPC接口的Response返回值类。枚举项P_M被添加到一个枚举参数AType中。但是下游系统发布的时候,我们负责的系统没有通知升级,所以报错。下面我们来分析一下为什么会这样。问题复现首先,下游系统A为二方库提供接口,其返回值的参数类型为枚举类型。一方库是指本项目中的依赖。第二方库是指公司内部其他项目提供的依赖。第三方库是指来自第三方的其他组织、公司等的依赖。privateATypeaType;}publicenumAType{P_T,A_B}然后B系统依赖这个二方库,会通过RPC远程调用调用AFAcadeService的doSth方法。publicclassBService{@AutowiredAFacadeServiceaFacadeService;publicvoiddoSth(){ARequestaRequest=newARequest();AResponseaResponse=aFacadeService.doSth(aRequest);ATypeaType=aResponse.getAType();}}这时候如果A和B系统依赖相同的两方库的情况,两者使用的枚举AType会是同一个类,里面的枚举项也是一致的,所以这种情况下不会有问题。但是,如果有一天,这个二方库升级了,在枚举类AType中增加了一个新的枚举项P_M,此时只有系统A升级了,而系统B还没有升级。那么系统A所依赖的AType是这样的:publicenumAType{P_T,A_B,P_M}而系统B所依赖的AType是这样的:publicenumAType{P_T,A_B}这种情况下,系统B通过RPC调用系统A有时,如果系统A返回的AResponse中aType的类型是新添加的P_M,系统B将无法解析。一般这个时候RPC框架都会出现反序列化异常。导致程序中断。原理分析我们已经把这个问题的现象分析清楚了,那么我们来看看原理以及为什么会出现这样的异常。其实这个原理并不难。这些RPC框架大多会使用JSON格式进行数据传输,即客户端将返回值序列化为JSON字符串,服务端将JSON字符串反序列化为Java对象。在反序列化JSON的过程中,对于一个枚举类型,它会尝试调用对应枚举类的valueOf方法来获取对应的枚举。我们查看枚举类的valueOf方法的实现可以发现,如果从枚举类中找不到对应的枚举项,则会抛出IllegalArgumentException:publicstatic>TvalueOf(ClassenumType,Stringname){Tresult=enumType.enumConstantDirectory().get(name);if(result!=null)returnresult;if(name==null)thrownewNullPointerException("Nameisnull");thrownewIllegalArgumentException("Noenumconstant"+enumType.getCanonicalName()+"."+name);}关于这个问题,《阿里巴巴Java开发手册》中其实也有类似约定:规定“二方库的参数可以使用枚举,但返回值为不允许枚举”。这背后的思考就是本文上面提到的内容。延伸思考为什么参数中可以有枚举?不知道你有没有想过这个问题。其实这跟二方库的职责有关系。一般情况下,系统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注解来表示这个字符串字段的值是从那个枚举中获取的。publicClassAResponse{privateBooleansuccess;/***@seeAType*/privateStringaType;}对于阿里这样比较大的互联网公司来说,随便提供一个接口可能有几百个调用者,接口升级也很正常。我们基本上做的是要求所有调用者每次升级二方库后一起升级是完全不现实的,而且对于一些调用者来说,因为不能使用新特性,所以不需要升级。还有一种情况看起来比较特殊,其实很常见,就是有时候A包里有一个接口声明,而B包里定义了一些枚举常量,比较常见的是阿里交易相关的信息,订单分为很多层级,每次引入一个包,需要导入几十个包。对于调用者来说,我绝对不希望我的系统引入太多的依赖。一方面,过多的依赖会导致应用程序编译过程非常缓慢,容易出现依赖冲突。所以在调用下游接口的时候,如果参数中字段的类型是枚举,那我就只好依赖他的二方库了。但如果它不是一个枚举,只是一个字符串,那么我可以选择不依赖。因此,我们在定义接口时尽量避免使用枚举等强类型。规范中规定不允许在返回值中使用,但是我有更高的要求,就是即使在接口的入参中我也很少使用。最后,我只是不建议在对外提供的接口的输入输出参数中使用枚举,并不是说完全不应该使用枚举。正如我在之前的很多文章中提到的,枚举有很多好处,我经常在代码中使用它们。因此,我们千万不能因为噎住了而放弃进食。当然,本文观点仅代表本人。是否适用于其他人、其他场景或其他公司的做法,需要读者自行区分。我建议大家在使用的时候多考虑一下。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文