求你了,不要再在对外接口中使用枚举类型了!

LogicCat
• 阅读 729

最近,我们的线上环境出现了一个问题,线上代码在执行过程中抛出了一个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的接口中入参和出参都不要使用枚举。

一般,我们要使用枚举都是有几个考虑:

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包中,比较常见的就是阿里的交易相关的信息,订单分很多层次,每次引入一个包的同时都需要引入几十个包。

对于调用者来说,我肯定是不希望我的系统引入太多的依赖的,一方面依赖多了会导致应用的编译过程很慢,并且很容易出现依赖冲突问题。

所以,在调用下游接口的时候,如果参数中字段的类型是枚举的话,那我没办法,必须得依赖他的二方库。但是如果不是枚举,只是一个字符串,那我就可以选择不依赖。

所以,我们在定义接口的时候,会尽量避免使用枚举这种强类型。规范中规定在返回值中不允许使用,而我自己要求更高,就是即使在接口的入参中我也很少使用。

最后,我只是不建议在对外提供的接口的出入参中使用枚举,并不是说彻底不要用枚举,我之前很多文章也提到过,枚举有很多好处,我在代码中也经常使用。所以,切不可因噎废食。

当然,文中的观点仅代表我个人,具体是是不是适用其他人,其他场景或者其他公司的实践,需要读者们自行分辨下,建议大家在使用的时候可以多思考一下。

点赞
收藏
评论区
推荐文章
橘子橙 橘子橙
4年前
vue-element-admin项目打包后,iconfont图标出现乱码
使用vueelementadmin或者vueelementtemplate开发的项目,打包到线上,就出现了图标乱码,f12后能看到icon元素为.eliconclose:before{content:"□"}的情况(如下)
Souleigh ✨ Souleigh ✨
4年前
前端 - 常见的异常捕获方法
前端异常捕获在ES3之前js代码执行的过程中,一旦出现错误,整个js代码都会停止执行,这样就显的代码非常的不健壮。从ES3开始,js也提供了类似的异常处理机制,从而让js代码变的更健壮,程序执行的过程中出现了异常,也可以让程序具有了一部分的异常恢复能力。js异常的特点是,出现不会导致JS引擎崩溃,最多只会终止当前执行的任务。回归正题,我们该如何在程序异常发生
Stella981 Stella981
3年前
JFR定位由于可能的JDK11的bug导致Log4j2 CPU占用100%的问题
本文基于OpenJDK11最近使用SpringCloudGateway的时候,遇到了一个奇怪的问题:线上有3个API网关实例,压力均衡,平稳运行3天后,突然有一个实例,CPU飚高,并且响应时间增加很多,从几十毫秒涨到了几分钟。线上是k8s管理容器,立刻停掉了这个pod,重建,恢复正常。线上我们开启了JFR记录(
Stella981 Stella981
3年前
Executors使用不当引起的内存泄漏
线上服务内存溢出这周刚上班突然有一个项目内存溢出了,排查了半天终于找到问题所在,在此记录下,防止后面再次出现类似的情况。先简单说下当出现内存溢出之后,我是如何排查的,首先通过jstack打印出堆栈信息,然后通过分析工具对这些文件进行分析,根据分析结果我们就可以知道大概是由于什么问题引起的。关于jstack如何使用,大家可以先看看这篇文章
Stella981 Stella981
3年前
Hibernate纯sql查询结果和该sql在数据库直接查询结果不一致
问题:今天在做一个查询的时候发现一个问题,我先在数据库实现了我需要的sql,然后我在代码中代码:selectdistinctd.id,d.name,COALESCE(c.count_num,0),COALESCE(c.count_fix,0),COALESCE(c
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
京东云开发者 京东云开发者
1个月前
提高IT运维效率,深度解读京东云AIOps落地实践(异常检测篇)
作者:京东科技张静基于深度学习对运维时序指标进行异常检测,快速发现线上业务问题时间序列的异常检测是实际应用中的一个关键问题,尤其是在IT行业。我们没有采用传统的基于阈值的方法来实现异常检测,而是通过深度学习提出了一种无阈值方法:基于LSTM网络的基线(一个
京东云开发者|提高IT运维效率,深度解读京东云AIOps落地实践
基于深度学习对运维时序指标进行异常检测,快速发现线上业务问题时间序列的异常检测是实际应用中的一个关键问题,尤其是在IT行业。我们没有采用传统的基于阈值的方法来实现异常检测,而是通过深度学习提出了一种无阈值方法:基于LSTM网络的基线(一
消失的死锁:从 JSF 线程池满到 JVM 初始化原理剖析 | 京东云技术团队
在一次上线时,按照正常流程上线后,观察了线上报文、接口可用率十分钟以上,未出现异常情况,结果在上线一小时后突然收到jsf线程池耗尽的报警,并且该应用一共有30台机器,只有一台机器出现该问题,迅速下线该机器的jsf接口,恢复线上。然后开始排查问题。
如何从消失的异常堆栈定位线上问题 | 京东云技术团队
在618保障大促稳定性过程中,消失的异常堆栈可能会给我们带来严重的麻烦,因为这些堆栈信息是我们解决线上问题的关键之一。如何快速定位问题?想必大家心中都有自己的答案,当然最简单直接的办法还是查找异常堆栈信息。
京东云开发者 京东云开发者
6个月前
记录一次SQL慢查询优化
作者:京东物流赫占星一、慢SqL发现在一次需求UAT上线后,本来在测试环境没问题的接口,UAT环境出现了接口超时,通过查询接口日志发现是SQL查询超时了,原因是UAT环境的数据量比测试环境大得多。一般来说,我们可以通过数据库本身的慢查询日志去定位出问题的慢