乐趣区

关于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 的接口中入参和出参都不要应用枚举。

个别,咱们要应用枚举都是有几个思考:

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

对于调用者来说,我必定是不心愿我的零碎引入太多的依赖的,一方面依赖多了会导致利用的编译过程很慢,并且很容易呈现依赖抵触问题。

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

所以,咱们在定义接口的时候,会尽量避免应用枚举这种强类型。标准中规定在返回值中不容许应用,而我本人要求更高,就是即便在接口的入参中我也很少应用。

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

当然,文中的观点仅代表我集体,具体是是不是实用其他人,其余场景或者其余公司的实际,须要读者们自行分辨下,倡议大家在应用的时候能够多思考一下。

退出移动版