新云网站模版,免费制作微信小程序的软件,用户界面设计的重要性,在线天堂おっさんとわたし最近#xff0c;我们的线上环境出现了一个问题#xff0c;线上代码在执行过程中抛出了一个IllegalArgumentException#xff0c;分析堆栈后#xff0c;发现最根本的的异常是以下内容#xff1a; java.lang.IllegalArgumentException: No enum constant com.a.b.f.m.a.c.A…最近我们的线上环境出现了一个问题线上代码在执行过程中抛出了一个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 extends EnumT T valueOf(ClassT 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的接口中入参和出参都不要使用枚举。
一般我们要使用枚举都是有几个考虑 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包中比较常见的就是阿里的交易相关的信息订单分很多层次每次引入一个包的同时都需要引入几十个包。
对于调用者来说我肯定是不希望我的系统引入太多的依赖的一方面依赖多了会导致应用的编译过程很慢并且很容易出现依赖冲突问题。
所以在调用下游接口的时候如果参数中字段的类型是枚举的话那我没办法必须得依赖他的二方库。但是如果不是枚举只是一个字符串那我就可以选择不依赖。
所以我们在定义接口的时候会尽量避免使用枚举这种强类型。规范中规定在返回值中不允许使用而我自己要求更高就是即使在接口的入参中我也很少使用。
最后我只是不建议在对外提供的接口的出入参中使用枚举并不是说彻底不要用枚举我之前很多文章也提到过枚举有很多好处我在代码中也经常使用。所以切不可因噎废食。
当然文中的观点仅代表我个人具体是是不是适用其他人其他场景或者其他公司的实践需要读者们自行分辨下建议大家在使用的时候可以多思考一下。