GitHub项目地址:https://github.com/Sayi/swagger-dubbo
Dubbo是一种透明化的RPC调用方案和服务治理方案,对外暴露服务接口Provider。Swagger构建了符合Open Api规范的API文档,通过SwaggerUI提供了模拟HTTP请求的工具。
本文将探讨的是Dubbo服务接口文档化,以及如何通过HTTP请求访问服务接口,便于应用在单机接口测试、服务快速验证、扩展服务方式等场景。
服务接口与HTTP的映射,涉及到请求URL、HTTP方法、参数。
一个请求URL需要唯一确定一个接口和一个方法,我们先看看一个接口的定义:
package com.deepoove.swagger.dubbo.example.api.service;
import java.util.List;
import com.deepoove.swagger.dubbo.example.api.pojo.User;
public interface UserService {
List<User> query(String phone);
List<User> query(int areaCode);
User get(String id);
void save(User user);
User update(User user);
void delete(String id);
}
我们采用interfaceClass/method来生成地址(当然也必须是interfaceClass,谁会希望在URL看到你的实现类类名)。即访问地址为http://ip:port/context/com.deepoove.swagger.dubbo.example.api.service.UserService/get
,但是这种规则无法唯一确定一个重载的方法,比如代码中query方法。解决方案可能有下面两种:
1. 在http request中附带参数个数、参数类型等信息。
2. 对重载的方法设法指定一个别名,将请求地址映射为com.xxx.XxService/method/aliasName
方案1的优点是没有变更请求地址,缺点是对请求的信息做了附加,限制了请求的自由性。方案2的优缺点恰恰和方案1相反。
最终,我选择了方案2,我们需要找到一种生成别名的方法。本来考虑过通过参数个数和类型生成别名,但是我希望别名是可记忆性的,目前的方案是依赖swagger,在重载方法加上注解@ApiOperation,设置其nickname作为方法别名,这一步对重载方法来说,目前是必须的。代码可能变成了这样:
@ApiOperation(nickname = "byPhone", value = "查询用户", notes = "通过phone取用户信息")
List<User> query(String phone);
@ApiOperation(nickname = "byArea", value = "查询用户", notes = "通过城市地区取用户信息")
List<User> query(int areaCode);
默认所有请求的方法都是POST
,除非通过@ApiOperation指定了特别的httpMethod。对于不符合请求方法的HTTP调用,服务器都将返回404。
参数的映射是个很复杂的问题,Spring在参数类型转化时做了很多事。http请求参数的值默认都是string的,如果接口参数恰恰是string类型,这样的映射没有问题。但是对于我们来说,有以下两点需要另处理:
参数类型为原生类型,参数值为string
我们需要将string类型的参数值转化为原生类型的值。
参数类型为JavaBean
第一次我会想到用request body去容纳JavaBean,但是当参数为多个JavaBean时,将多个参数值组合容纳在body中会导致无法理解参数的含义(body只有一个)。
如果我们将JavaBean拆解成field作为参数,这样可能会让参数个数泛滥。
最终我们把JavaBean类型的参数定义成表单参数,类型为json string,即将JavaBean序列化成json字符串作为参数。这样就解决了多个JavaBean作为参数的问题,尽管这一方案在模拟参数值时是复杂的。
参数类型为原生类型,参数值为null
Spring不允许null赋值给原生类型的,因为我们不知道null代表原生类型的什么值。所以对于原生类型的参数,都应该为必填。
定义好了映射关系,我们就可以让Swagger扫描Dubbo服务了。有以下几点需要做:
1. 通过Spring上下文获取到所有注册的Dubbo服务接口和实现类
2. 根据interfaceClass/method/{aliasName}规则,生成服务URL
3. 根据方法参数生成请求参数
这里有一个问题,就是Swagger的文档注解是写在接口上,还是实现类上?
个人认为在一个公司内部,可以写在接口上,方便公司内部阅读。如果是对外使用,强烈不建议污染服务接口,应该写在实现类上。swagger-dubbo没有限制注解的位置,既可以写在接口上,也可以写在实现类上。
HTTP模拟调用和Swagger两者之间本身不该有任何依赖和关联。
上文中提到的别名和请求方法是http模拟调用与Swagger的唯一依赖关系,如果不考虑自定义请求方法和重载方法的别名,可以不写任何swagger注解,就可以模拟HTTP调用,因为写swagger注解应该只是为了文档,而不该越权去定义HTTP请求。
但是,现在这些唯一的依赖关系,可能是最简单的实现。