程序员新人在对外接口中使用枚举类型,竟然引发了生产事故
二哥的编程星球已经有 450 多名 小伙伴加入了,如果你也需要一个良好的学习氛围,戳链接加入我们吧!这是一个 Java 学习指南 + 编程实战的私密圈子,你可以向二哥提问、帮你制定学习计划、跟着二哥一起做项目、刷力扣,冲冲冲。
最近,我们的线上环境出现了一个问题,线上代码在执行过程中抛出了一个 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 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 包中,比较常见的就是阿里的交易相关的信息,订单分很多层次,每次引入一个包的同时都需要引入几十个包。
对于调用者来说,我肯定是不希望我的系统引入太多的依赖的,一方面依赖多了会导致应用的编译过程很慢,并且很容易出现依赖冲突问题。
所以,在调用下游接口的时候,如果参数中字段的类型是枚举的话,那我没办法,必须得依赖他的二方库。但是如果不是枚举,只是一个字符串,那我就可以选择不依赖。
所以,我们在定义接口的时候,会尽量避免使用枚举这种强类型。规范中规定在返回值中不允许使用,而我自己要求更高,就是即使在接口的入参中我也很少使用。
最后,我只是不建议在对外提供的接口的出入参中使用枚举,并不是说彻底不要用枚举,我之前很多文章也提到过,枚举有很多好处,我在代码中也经常使用。所以,切不可因噎废食。
当然,文中的观点仅代表我个人,具体是是不是适用其他人,其他场景或者其他公司的实践,需要读者们自行分辨下,建议大家在使用的时候可以多思考一下。
没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。
推荐阅读: