欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

高效、准确的联调接口的一种方式(让bug飞一会儿)

程序员文章站 2024-03-26 09:10:17
...

本文重点:

  1. 通过声明式简化接口请求的使用。
  2. 通过代码生成降低开发者的重复的工作。
  3. 提高开发者的开发体验,效率,Happy Coding。

在日常的开发中,不论是web项目,移动端/服务端,或者是微服务,总是会涉及到多人协作或者跨团队甚至跨部门的配合,如何高效的联调接口就会很大程度的上影响项目的进度和总体质量。

痛点(针对http接口)

  1. 由于文档描述不够准确或者看文档的人不够细心引起的问题
   1.1:参数是否必填、字段类型
   1.2:http 请求方法、 请求头(content-type)、路径(有路径参数)
   1.3:响应字段格式类型,例如:枚举
   1.4:低效的沟通导致相互扯皮的问题
  1. 由于接口变更引起的问题
由于在开发阶段因为设计或者需求的变更导致接口变更,对接接口的人在变更接口调用的时候可能会遗漏
  1. 效率问题,如果接口有上百个甚至更多,就单纯些接口的模型对象就是一个很大的工作量

从上面列的几点可以发现这些问题:

  1. 对接接口的人需要关系太多的细节,例如:请求url,请求头,Content-Type,请求方法等
  2. 需要而外的工作,编写接口的模型对象,定义请求方法等

除了这些,如果是js端,还要面对多个运行是环境的问题,例如:浏览器、小程序(多个平台)、weex、react nateive,这无疑又是一个很重复的工作量(当然开源社区有提供跨可以js平台的请求库,例如:umi-requestaxios等)。

一种解决思路

  1. 通过提供声明式的请求工具,屏蔽接口的一些细节,让开发员专注业务。
  2. 通过代码生成,减少接口对接人员的工作
  3. 这条针对js:使用typescript,增强接口方法的可用性和安全性typescirpt中文网

声明式的接口请求

引用一段百度百科上对声明式编程的定义

声明式编程(英语:Declarative programming)是一种编程范式,与命令式编程相对立。它描述目标的性质,让计算机明白目标,而非流程。声明式编程不用告诉计算机问题领域,从而避免随之而来的副作用。而命令式编程则需要用算法来明确的指出每一步该怎么做

简单的说就是只需要定义要做什么,而不是怎么做。

社区开源的库

  1. feign
  2. spring-cloud-openfeign
  3. retrofit

来一例子

@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-feignflutter-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)
    })
}


  1. common-codegen是一个通过java class生成api sdk的代码生成工具,目前支持spring mvc相关的注解(通过控制器生成sdk,支持扩展其他注解)、默认swagger2和3的注解和部分的javax验证注解。支持生成retrofit\spring cloud openfeign\typescript-feign\umi-request\dart_feign 5种请求工具的代码。
  2. flutter-feign flutter的http请求工具
  3. typescript-feign支持所有的js运行环境的http请求工具,一套sdk适配所有的js平台