微服务接口定义实践

在进行SOA系统设计,设计微服务的接口形式,会遇到一些问题,

  • 入参是多个入参还是包装在一个对象中
  • 异常返回是通过Exception还是返回码

以下是开发过程中的一些思考与实践:

1、实践

定义成 GetUserInfoResp getUserInfo(GetUserInfoReq req)的形式,

  • 入参

    • GetUserInfoReq继承了AbstractReq
    • AbstractReq保存了公用的请求参数成员变量,如username、ip
  • 出参

    • GetUserInfoResp继承了AbstractResp
    • AbstractResp保存了公用的返回码成员变量,如retCode

代码设计

interface UserProvider {
    GetUserInfoResp getUserInfo(GetUserInfoReq req);
}

class GetUserInfoReq extends AbstractReq {
}

class AbstractReq {
    private String username;
    private String ip;
}

class GetUserInfoResp extends AbstractResp {
    private String school;
    private double weight;
}

class AbstractResp {
    private String retCode;
    private String message;
}

2、入参设计

为什么要把各种成员变量包装在一个Req对象中,而不是做成getUserInfo(String username, String disctrict)的形式呢?

2.1 方便拓展

  • 案例一:

如果这个接口有两个调用方,某一天这个接口新添加了一个入参int gender,其中一个调用方会传这个参数,另一个调用方不传。

如果是当前这个方案,只需要在Req对象中添加一个成员变量int gender即可,然后两个消费方都可以兼容的进行调用。

如果是采用getUserInfo(String username, String disctrict)的方案,那么就只能新加一个方法getUserInfo(String username, String disctrict, int gender)了,造成了代码的冗余。

  • 案例二:

如果某一天,这个系统要支持不同国家的业务,所有的接口都要添加一个参数String nation

那么基于当前的方案,只需要在AbstractReq中添加一个String nation即可,非常方便。

如果基于getUserInfo(String username, String disctrict)的形式,那么可能需要批量修改几百个接口,花费了大量的时间,同时也提高了错误发生的概率。

2.2 方便映射

在SOA的系统设计中,往往会在内部系统的前面添加一个gateway系统来做一些公共逻辑,网关对外提供http服务,而网关和内部系统通过专有协议进行通信,

想想这样一个场景,/user/getUserInfo这个接口提供获取用户信息的服务,用户的请求body可能有以下几种形式:

  • 形式一:
{
    "name": "jason",
    "nation": "america"
}
  • 形式二:
{
    "nation": "america",
    "name": "jason"
}

要把这些形式的请求映射到后端接口上:

2.2.1 如果是当前最佳实践

即如果是getUserInfo(GetUserInfoReq req)的形式

那么网关很好设计,

  1. 提前记录url和接口的映射规则,
  2. 请求来的时候,把url映射成接口,直接把body反序列化成GetUserInfoReq对象即可。
2.2.2 如果是另一种形式

即如果getUserInfo(String name, String nation)的形式

那么网关很不好设计,

  1. 提前记录url和接口的映射规则,

    这个记录会很麻烦,因为需要记录 body中的每个参数和接口中的第几个参数是对应的

  2. 映射也很麻烦,

    需要遍历body中的变量,把body中的某个变量映射成接口第n个参数

究其原因,是因为通过反射可以获取到一个对象的成员变量的名称,却无法获取到一个方法的参数名称。

2.3 方便封装

封装成Req对象,可以把相关的函数封装在Req对象内部,比如validateParam()方法。

更进一步,可以定义一个BaseReq,在其内部定义一个 abstract validateParam() 方法,来强制所有的Req对象定义validateParam()方法。

public 

3、出参设计

为什么通过通过retCode来包装异常情况,而不是直接抛出异常?

3.1 降低系统间的耦合

如果getUserInfo通过异常来表达业务异常情况,那调用方势必要接触到 提供方的各种Exception类型,大大提高了系统的耦合性。

3.2 避免延迟 && 避免占用带宽

Exception会带着一个调用栈,
如果通过网络抛出异常,一方面会占用带宽,一方面会造成延迟。

4、总结

基于以上原因,应该采用GetUserInfoResp getUserInfo(GetUserInfoReq req)的形式定义接口。

/** * RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS. * LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/ /* var disqus_config = function () { this.page.url = PAGE_URL; // Replace PAGE_URL with your page's canonical URL variable this.page.identifier = PAGE_IDENTIFIER; // Replace PAGE_IDENTIFIER with your page's unique identifier variable }; */ (function() { // DON'T EDIT BELOW THIS LINE var d = document, s = d.createElement('script'); s.src = 'https://chenzz.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })();