高效、准确的联调接口的一种方式(让bug飞一会儿)
本文重点:
- 通过声明式简化接口请求的使用。
- 通过代码生成降低开发者的重复的工作。
- 提高开发者的开发体验,效率,Happy Coding。
在日常的开发中,不论是web项目,移动端/服务端,或者是微服务,总是会涉及到多人协作或者跨团队甚至跨部门的配合,如何高效的联调接口就会很大程度的上影响项目的进度和总体质量。
痛点(针对http接口)
- 由于文档描述不够准确或者看文档的人不够细心引起的问题
1.1:参数是否必填、字段类型
1.2:http 请求方法、 请求头(content-type)、路径(有路径参数)
1.3:响应字段格式类型,例如:枚举
1.4:低效的沟通导致相互扯皮的问题
- 由于接口变更引起的问题
由于在开发阶段因为设计或者需求的变更导致接口变更,对接接口的人在变更接口调用的时候可能会遗漏
- 效率问题,如果接口有上百个甚至更多,就单纯些接口的模型对象就是一个很大的工作量
从上面列的几点可以发现这些问题:
- 对接接口的人需要关系太多的细节,例如:请求url,请求头,Content-Type,请求方法等
- 需要而外的工作,编写接口的模型对象,定义请求方法等
除了这些,如果是js端,还要面对多个运行是环境的问题,例如:浏览器、小程序(多个平台)、weex、react nateive,这无疑又是一个很重复的工作量(当然开源社区有提供跨可以js平台的请求库,例如:umi-request、axios等)。
一种解决思路
- 通过提供声明式的请求工具,屏蔽接口的一些细节,让开发员专注业务。
- 通过代码生成,减少接口对接人员的工作
- 这条针对js:使用typescript,增强接口方法的可用性和安全性typescirpt中文网
声明式的接口请求
引用一段百度百科上对声明式编程的定义
声明式编程(英语:Declarative programming)是一种编程范式,与命令式编程相对立。它描述目标的性质,让计算机明白目标,而非流程。声明式编程不用告诉计算机问题领域,从而避免随之而来的副作用。而命令式编程则需要用算法来明确的指出每一步该怎么做
简单的说就是只需要定义要做什么,而不是怎么做。
社区开源的库
来一例子
@FeigenCleint(url="http://example.com/api")
public interface ExampleFeignClient{
@GetMapping(value="/example/{id}",consumes={"application/json"})
Example getExample(@PathVariable("id") Long id);
@PostMapping(value="/example",produces={"application/json"})
void createExample(@RequestBody CreateExampleReq req);
}
这样对于使用的人只需要关心方法的入参和返回值,而不需要关心其他的,就像调用一个普通的方法一样,最大程度的简单了接口调用了复杂度(当然事物都是两面性,当我们解决了一个问题以后,很可能会来带来新的问题,例如上面这个例子,会带来而外的学习成本和一些配置等)。
来一个对比
const getExample=(id:number):Promise<Example>=>{
return fetch(`http://example.com/api/example/${id}`,{
method:'GET',
}).then((response)=>{
return response.json();
})
}
const createExample=(req:CreateExampleReq):Promise<void>=>{
return fetch(`http://example.com/api/example`,{
method:'POST',
headers:{
'Content-Type': 'application/json'
},
body:JSON.stringify(req)
})
}
可以看到第二种方式开发者需要关心更多的细节,例如,参数的序列化,返回值的类型(Content-Type)处理等,这些应该交由框架去处理。当然,上面这些代码如果都让开发者手写的话,接口数量一多工作量还是很大,这个时候就可以考虑祭出代码生成这个工具。
来一个参照着spring cloud openfeign的typescript的例子
@Feign({
value: '/v1/example',
})
class ExampleService {
/**
* 1:接口方法:POST
* 2:描述的文字
* 3:返回值在java中的类型为:ApiResp
* 4:返回值在java中的类型为:Long
**/
@PostMapping({
value: '/create',
produces: [HttpMediaType.FORM_DATA],
})
create: (req: CreateExampleEntityReq, option?: FeignRequestOptions) => Promise<number>;
/**
* 1:接口方法:PUT
* 2:描述的文字
* 3:返回值在java中的类型为:ApiResp
* 4:返回值在java中的类型为:Void
**/
@PutMapping({
value: '/edit',
produces: [HttpMediaType.FORM_DATA],
})
edit: (req: EditExampleEntityReq, option?: FeignRequestOptions) => Promise<void>;
/**
* 1:接口方法:GET
* 2:描述的文字
* 3:返回值在java中的类型为:ApiResp
* 4:返回值在java中的类型为:Void
**/
@GetMapping({
value: '/delete',
produces: [HttpMediaType.FORM_DATA],
})
delete: (req: DeleteExampleEntityReq, option?: FeignRequestOptions) => Promise<void>;
/**
* 1:接口方法:GET
* 2:描述的文字
* 3:返回值在java中的类型为:ApiResp
* 4:返回值在java中的类型为:Pagination
* 5:返回值在java中的类型为:ExampleEntityInfo
**/
@GetMapping({
value: '/query',
produces: [HttpMediaType.FORM_DATA],
})
query: (req: QueryExampleEntityReq, option?: FeignRequestOptions) => Promise<PageInfo<ExampleEntityInfo>>;
/**
* 1:接口方法:GET
* 2:描述的文字
* 3:返回值在java中的类型为:ApiResp
* 4:返回值在java中的类型为:ExampleEntityInfo
**/
@GetMapping({
value: '/{id}',
produces: [HttpMediaType.FORM_DATA],
})
detail: (req: ExampleServiceDetailReq, option?: FeignRequestOptions) => Promise<ExampleEntityInfo>;
}
export default new ExampleService();
dart的例子
import 'dart:io';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:fengwuxp_dart_basic/index.dart';
import 'package:fengwuxp_dart_openfeign/index.dart';
import '../domain/order.dart';
import '../resp/page_info.dart';
import '../evt/query_order_evt.dart';
import '../evt/create_order_evt.dart';
import '../../../../../serializers.dart';
/// 订单服务
/// 接口:GET
@Feign
@FeignClient(value: "/order",)
class OrderFeignClient extends FeignProxyClient {
OrderFeignClient() : super() {
}
/// 1:获取订单列表
/// 2:接口方法:GET
/// 3:返回值在java中的类型为:List
/// 4:返回值在java中的类型为:Order
@GetMapping(value: "get_order",)
Future<BuiltList<Order>> getOrder(List<String> names,
List<int> ids,
Set<Order> moneys,
[UIOptions feignOptions]) {
return this.delegateInvoke<BuiltList<Order>>("getOrder",
[names, ids, moneys,],
feignOptions: feignOptions,
serializer: BuiltValueSerializable(
specifiedType: FullType(BuiltList, [FullType(Order)])
)
);
}
/// 1:获取订单列表
/// 2:接口方法:POST
/// 3:返回值在java中的类型为:PageInfo
/// 4:返回值在java中的类型为:Order
@PostMapping()
Future<PageInfo<Order>> queryOrder(@RequestBody(true) QueryOrderEvt evt,
[UIOptions feignOptions]) {
return this.delegateInvoke<PageInfo<Order>>("queryOrder",
[evt,],
feignOptions: feignOptions,
serializer: BuiltValueSerializable(
serializer: PageInfo.serializer,
specifiedType: FullType(PageInfo, [FullType(Order)])
)
);
}
/// 1:获取订单列表
/// 2:接口方法:POST
/// 3:返回值在java中的类型为:ServiceQueryResponse
/// 4:返回值在java中的类型为:Order
@PostMapping(produces: [HttpMediaType.MULTIPART_FORM_DATA],)
Future<PageInfo<Order>> queryOrder2(@RequestParam("order_id") int oderId,
String sn,
[UIOptions feignOptions]) {
return this.delegateInvoke<PageInfo<Order>>("queryOrder2",
[oderId, sn,],
feignOptions: feignOptions,
serializer: BuiltValueSerializable(
serializer: PageInfo.serializer,
specifiedType: FullType(PageInfo, [FullType(Order)])
)
);
}
/// 1:查询分页
/// 2:接口方法:POST
/// 3:<pre>
///参数列表:
///参数名称:id,参数说明:属性名称:id,属性说明:订单,示例输入:
///</pre>
/// 4:返回值在java中的类型为:ServiceResponse
/// 5:返回值在java中的类型为:PageInfo
/// 6:返回值在java中的类型为:Order
@PostMapping()
Future<PageInfo<Order>> queryPage(String id,
[UIOptions feignOptions]) {
return this.delegateInvoke<PageInfo<Order>>("queryPage",
[id,],
feignOptions: feignOptions,
serializer: BuiltValueSerializable(
serializer: PageInfo.serializer,
specifiedType: FullType(PageInfo, [FullType(Order)])
)
);
}
/// 1:创建订单
/// 2:接口方法:GET
/// 3:返回值在java中的类型为:ServiceResponse
/// 4:返回值在java中的类型为:Long
@GetMapping()
Future<int> createOrder(CreateOrderEvt evt,
[UIOptions feignOptions]) {
return this.delegateInvoke<int>("createOrder",
[evt,],
feignOptions: feignOptions,
serializer: BuiltValueSerializable(
specifiedType: FullType(int)
)
);
}
/// 1:test hello
/// 2:接口方法:POST
/// 3:返回值在java中的类型为:ServiceResponse
@PostMapping()
Future<dynamic> hello([UIOptions feignOptions]) {
return this.delegateInvoke<dynamic>("hello",
[],
feignOptions: feignOptions,
);
}
}
final orderFeignClient = OrderFeignClient();
以上的代码是通过代码生成codegen工具生成的,结合typescript-feign、 flutter-feign这个2个http请求库,从加强接口调用的准确性和易用性以及减少开发者工作量2个维度协助开发人员降低接口对接的难度、出错率,提高开发体验。
spring cloud openfeign的生成例子:
import io.reactivex.Observable;
import org.springframework.cloud.openfeign.*;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import com.wuxp.codegen.swagger2.domain.Order;
import com.wuxp.codegen.swagger2.resp.PageInfo;
import com.wuxp.codegen.swagger2.resp.ServiceResponse;
import com.wuxp.codegen.swagger2.evt.CreateOrderEvt;
import com.wuxp.codegen.swagger2.example.evt.QueryOrderEvt;
import java.util.Date;
import java.util.Map;
/**
* 订单服务
* 接口:GET
**/
@FeignClient(
decode404 = false,
name = "exampleService",
path = "/order",
url = "${test.feign.url}"
)
public interface OrderFeignClient {
/**
* 1:获取订单列表
* 2:接口方法:GET
* 3:返回值在java中的类型为:List
* 4:返回值在java中的类型为:Order
**/
@GetMapping(value = "get_order")
List<Order> getOrder(
String[] names,
List<Integer> ids,
Set<Order> moneys
);
/**
* 1:获取订单列表
* 2:接口方法:GET
* 3:返回值在java中的类型为:PageInfo
* 4:返回值在java中的类型为:Order
**/
@GetMapping()
PageInfo<Order> queryOrder(
@SpringQueryMap(value = true) QueryOrderEvt evt
);
/**
* 1:获取订单列表
* 2:接口方法:POST
* 3:返回值在java中的类型为:ServiceQueryResponse
* 4:返回值在java中的类型为:Order
**/
@PostMapping(produces = {MediaType.MULTIPART_FORM_DATA_VALUE})
ServiceResponse<PageInfo<Order>> queryOrder2(
@RequestParam(name = "order_id") Long oderId,
String sn
);
/**
* 1:查询分页
* 2:接口方法:POST
* 3:<pre>
* 参数列表:
* 参数名称:id,参数说明:属性名称:id,属性说明:订单,示例输入:
* </pre>
* 4:返回值在java中的类型为:ServiceResponse
* 5:返回值在java中的类型为:PageInfo
* 6:返回值在java中的类型为:Order
**/
@PostMapping()
ServiceResponse<PageInfo<Order>> queryPage(
String id
);
/**
* 1:创建订单
* 2:接口方法:POST
* 3:<pre>
* 参数列表:
* 参数名称:evt,参数说明:属性名称:evt,属性说明:创建订单,示例输入:
* </pre>
* 4:返回值在java中的类型为:ServiceResponse
* 5:返回值在java中的类型为:Long
**/
@PostMapping()
ServiceResponse<Long> createOrder(
@RequestBody(required = true) CreateOrderEvt evt
);
/**
* 1:test hello
* 2:接口方法:POST
* 3:返回值在java中的类型为:ServiceResponse
**/
@PostMapping()
ServiceResponse<Object> hello(
);
}
retrofit的生成例子
import io.reactivex.Observable;
import retrofit2.http.*;
import java.util.List;
import com.wuxp.codegen.swagger2.domain.Order;
import com.wuxp.codegen.swagger2.resp.PageInfo;
import com.wuxp.codegen.swagger2.resp.ServiceResponse;
import com.wuxp.codegen.swagger2.evt.CreateOrderEvt;
import com.wuxp.codegen.swagger2.evt.QueryOrderEvt;
import java.util.Date;
import java.util.Map;
/**
* 订单服务
* 接口:GET
**/
public interface OrderService {
/**
* 1:获取订单列表
* 2:接口方法:GET
* 3:返回值在java中的类型为:List
* 4:返回值在java中的类型为:Order
**/
@GET(value = "/order/get_order")
List<Order> getOrder(
String[] names,
List<Integer> ids,
Set<Order> moneys
);
/**
* 1:获取订单列表
* 2:接口方法:GET
* 3:返回值在java中的类型为:PageInfo
* 4:返回值在java中的类型为:Order
**/
@GET(value = "/order")
PageInfo<Order> queryOrder(
QueryOrderEvt evt
);
/**
* 1:获取订单列表
* 2:接口方法:POST
* 3:返回值在java中的类型为:ServiceQueryResponse
* 4:返回值在java中的类型为:Order
**/
@POST(value = "/order/queryOrder2")
@Headers(value = {"Content-Type: application/json"})
ServiceResponse<PageInfo<Order>> queryOrder2(
@Field(value = "order_id") Long oderId,
String sn
);
/**
* 1:查询分页
* 2:接口方法:POST
* 3:<pre>
* 参数列表:
* 参数名称:id,参数说明:属性名称:id,属性说明:订单,示例输入:
* </pre>
* 4:返回值在java中的类型为:ServiceResponse
* 5:返回值在java中的类型为:PageInfo
* 6:返回值在java中的类型为:Order
**/
@POST(value = "/order/queryPage")
@Headers(value = {"Content-Type: application/json"})
ServiceResponse<PageInfo<Order>> queryPage(
String id
);
/**
* 1:创建订单
* 2:接口方法:POST
* 3:<pre>
* 参数列表:
* 参数名称:evt,参数说明:属性名称:evt,属性说明:创建订单,示例输入:
* </pre>
* 4:返回值在java中的类型为:ServiceResponse
* 5:返回值在java中的类型为:Long
**/
@POST(value = "/order/createOrder")
ServiceResponse<Long> createOrder(
@Body CreateOrderEvt evt
);
/**
* 1:test hello
* 2:接口方法:POST
* 3:返回值在java中的类型为:ServiceResponse
**/
@POST(value = "/order/hello")
ServiceResponse<Object> hello(
);
}
umi-request的生成例子
/* tslint:disable */
import request, {RequestOptionsInit} from 'umi-request';
import {Order} from "../../domain/Order";
import {OrderServiceGetOrderReq} from "../../req/OrderServiceGetOrderReq";
import {QueryOrderEvt} from "../../evt/QueryOrderEvt";
import {PageInfo} from "../../resp/PageInfo";
import {OrderServiceQueryOrder2Req} from "../../req/OrderServiceQueryOrder2Req";
import {OrderServiceQueryPageReq} from "../../req/OrderServiceQueryPageReq";
import {CreateOrderEvt} from "../../evt/CreateOrderEvt";
import {OrderServiceHelloReq} from "../../req/OrderServiceHelloReq";
/**
* 订单服务
* 接口:GET
**/
/*================================================分割线,以下为接口列表===================================================*/
/**
* 1:获取订单列表
* 2:接口方法:GET
* 3:返回值在java中的类型为:List
* 4:返回值在java中的类型为:Order
**/
export const getOrder = (req: OrderServiceGetOrderReq, options?: RequestOptionsInit): Promise<Array<Order>> => {
return request<Array<Order>>(`/order/get_order`, {
method: 'get',
params: req,
...(options || {} as RequestOptionsInit)
})
}
/**
* 1:获取订单列表
* 2:接口方法:GET
* 3:返回值在java中的类型为:PageInfo
* 4:返回值在java中的类型为:Order
**/
export const queryOrder = (req: QueryOrderEvt, options?: RequestOptionsInit): Promise<PageInfo<Order>> => {
return request<PageInfo<Order>>(`/order`, {
method: 'get',
params: req,
...(options || {} as RequestOptionsInit)
})
}
/**
* 1:获取订单列表
* 2:接口方法:POST
* 3:返回值在java中的类型为:ServiceQueryResponse
* 4:返回值在java中的类型为:Order
**/
export const queryOrder2 = (req: OrderServiceQueryOrder2Req, options?: RequestOptionsInit): Promise<PageInfo<Order>> => {
return request<PageInfo<Order>>(`/order/queryOrder2`, {
method: 'post',
requestType: 'json',
data: req,
responseType: 'json',
...(options || {} as RequestOptionsInit)
})
}
/**
* 1:查询分页
* 2:接口方法:POST
* 3:<pre>
*参数列表:
*参数名称:id,参数说明:属性名称:id,属性说明:订单,示例输入:
*</pre>
* 4:返回值在java中的类型为:ServiceResponse
* 5:返回值在java中的类型为:PageInfo
* 6:返回值在java中的类型为:Order
**/
export const queryPage = (req: OrderServiceQueryPageReq, options?: RequestOptionsInit): Promise<PageInfo<Order>> => {
return request<PageInfo<Order>>(`/order/queryPage`, {
method: 'post',
requestType: 'json',
data: req,
responseType: 'json',
...(options || {} as RequestOptionsInit)
})
}
/**
* 1:创建订单
* 2:接口方法:POST
* 3:<pre>
*参数列表:
*参数名称:evt,参数说明:属性名称:evt,属性说明:创建订单,示例输入:
*</pre>
* 4:返回值在java中的类型为:ServiceResponse
* 5:返回值在java中的类型为:Long
**/
export const createOrder = (req: CreateOrderEvt, options?: RequestOptionsInit): Promise<number> => {
return request<number>(`/order/createOrder`, {
method: 'post',
requestType: 'form',
data: req,
responseType: 'json',
...(options || {} as RequestOptionsInit)
})
}
/**
* 1:test hello
* 2:接口方法:POST
* 3:返回值在java中的类型为:ServiceResponse
**/
export const hello = (req: OrderServiceHelloReq, options?: RequestOptionsInit): Promise<any> => {
return request<any>(`/order/hello`, {
method: 'post',
requestType: 'form',
data: req,
responseType: 'json',
...(options || {} as RequestOptionsInit)
})
}
- common-codegen是一个通过java class生成api sdk的代码生成工具,目前支持spring mvc相关的注解(通过控制器生成sdk,支持扩展其他注解)、默认swagger2和3的注解和部分的javax验证注解。支持生成retrofit\spring cloud openfeign\typescript-feign\umi-request\dart_feign 5种请求工具的代码。
- flutter-feign flutter的http请求工具
- typescript-feign支持所有的js运行环境的http请求工具,一套sdk适配所有的js平台