本文的“接口”
本文的-"接口",等同于 RPC 类协议/框架中的接口,例如SOAP, Apache Thrift, Apache Avro, Microsoft DCOM, WCF 及集团内最常用的 HSF, Dubbo 等等。此外,还有一些 RESTful 规范/框架(如Jax-RS),在使用体验上也非常类似于 RPC,虽然通常不被归属于RPC, 但本文中的提到某些原则可能同样适用。
一次典型的RPC 请求/响应包含以下几个步骤:
- 1) 客户端发起一次方法调用
- 2) 客户端将调用(接口+方法+参数)进行打包
- 3) 客户端将打包后的内容发送到服务器端
- 4) 服务器端收到数据并解析为一次方法调用
- 5) 服务器端在某对象上执行方法(参数)调用
- 6) 服务器端将得到调用返回值,并对其打包
- 7) 服务器端将打包后的返回值发送给客户端
- 8) 客户端收到响应并解析响应数据
- 9) 客户端得到方法的返回值
由于 2)~8) 的步骤对客户端是透明的,看似乎是本地方法调用,但远程方法调用是不同于本地调用的,使用时也不应该忽视他们存在着不同。
虽然不少框架没有对接口作出更严格的语法限制,但实际使用起来也不能太过任性,适当遵循一些规则习俗,会减少一些不必要的麻烦。以下就过往使用的经验,列举一些较常见的问题。
“接口”的定义
远程接口调用过程,发生了一次数据交换,即使用参数换得一个返回值或异常,一个普通的接口可能是这样的
public interface DemoService {
public ResultType doAnything(FooType arg1, BarType arg2) throws MyException;
}
不同于本地方法调用的是,远程方法对参数、返回值、异常的定义限制的越“严格”越好-从某种意义上说,参数、返回值、异常的类型都是应该是 Struct
而非 Class
,那么区别在哪、又为什么这么说呢?
- 1) Struct 侧重于字段-值,不可被扩展 - 这意味着Client/Server 每一端都不可能在协商好接口定义后单方面对数据进行“画蛇添足”,也不可能发送对方也许会不知道的数据类型,保证双方对收到的数据不存在产生歧义的可能。
- 2) Class 侧重于功能-方法,通常允许扩展-这意味着 Client/Server 都有可能向对方发送一个对方并不知道的类。比如Client 向 Server 发送了参数 FooType 的扩展类 FooTypeExt, 而 Server 可能因无法解释 FooTypeExt 而产生意外异常。
一些很可能产生歧义的接口定义:
- 1) 参数类型限定太宽泛,想返回什么都人合乎语法,无法保证 Server 一定会理解该参数
public int saveData(Object data);
- 2) 返回类型限定太宽泛,想返回什么都人合乎语法,无法保证 Client 一定会理解返回值
public Object saveData(int id);
- 3) 使用基础/抽象类型,一方可以随意 override 掉 BaseType 的某些行为而使用对方产生某些意外的效果
public int saveData(BaseType data);
public AbstractType getData(int id);
- 4) 泛型接口,不应当使用
public interface IMyAPI<T extends BaseType> {
pulic saveData(T data);
public T getData(int key);
}
这样的接口定义了啥?
- 5) 泛型方法,不应使用
public <T> T getData(int key);
这样的接口定义了啥?
由于 Java 不支持 Struct,在实际开发中,理论上应该只使用 final
修饰的 POJO 类作为参数/返回值类型,即使参数/返回类型不是密封类,也要避免使用它们的扩展类;
在需要使用 List, Map 等时,虽然语法允许使用它们的任意扩展类,但最好只使用JRE 包含的类而不要随意扩展。
“接口”的发布
“接口” 是Client/Server 数据交换的契约,接口所在包要被 Client/Server 所共享,因此这个包中最好仅是包含接口以及与接口相关的参数类型、返回值类型、异常类型,及其它公用的常量、枚举、资源等等, 而不应该包含Client或Server功能的具体实现 。
一个常见的现象是接口类被打包在 xxxx-client.jar 中而发布,这种方式存在以下多种问题。
- 1) server 上仅为了获得接口类,就需要部署 client.jar 及其全部依赖项,导致引用了很多冗余的包。
- 2) 接口定义经过双方协商后不会轻易变更,但 xxxx-client.jar 因包含有功能实现而会经常更新,继而引起接口使用方的连锁更新,而实际上又是不必要的。
- 3) 一些接口测试/分析工具,为了获得接口定义,同样也不得不部署整个的 client.jar 及其依赖项。
另一个常见现在是接口类被打包在 xxxx-common.jar 中发布,如果 common 库中包含有复杂的功能实现,同样也会引发上面两个问题。
小结:接口定义与期所在的包是同命运的,接口不更新则包不应该更新,包若更新则是表示接口已变。因此,在打包和发布接口时,尽量遵循以下
- a) 把接口类及期附属的参数/返回值/常量/资源等发布在一个 xxxx-api.jar (xxx-service/interface.jar等)包中;
- b) 不要在这个包中发布Client/Server 上的功能实现类;
- c) 如果有 Client/Server 共同的功能类,那么把它打包在另一个 xxxx-common.jar(xxxx-shared.jar等) 中;