Skip to content

第31讲:如何设计与实现API组合

从现在开始,我们将进入到 API 组合这一模块,该模块分为 3 个课时,分别介绍 API 组合的相关概念和具体的实现技术。而此次课时主要介绍 API 组合的设计与实现。

在介绍 API 组合之前,首先介绍一下 API 网关(Gateway)。

API 网关

这里先区分一下外部 API 和内部 API。外部 API 是提供给 Web 应用、移动客户端和第三方客户端来调用的;而内部 API 是提供给其他微服务来调用的。

如果把微服务架构的后台当成一个黑盒子,那么外部 API 就是外部用户与这个黑盒子交互的方式,这两者之间交互的桥梁,就是 API 网关。所有外部的 API 访问请求,都需要通过这个网关进入到后台。

下图给出了 API 网关的示意图,其本质上是一个反向代理(Reverse Proxy)。

作为外部访问请求的唯一入口,API 网关所能提供的功能非常丰富,具体如下。

API 网关负责把外部的访问请求路由到具体的服务,在进行路由时,通常根据访问请求的路径、查询参数和 HTTP 头来确定。

比如,可以把路径以 /trip/ 开头的请求路由到行程管理服务,而把以 /passenger/ 开头的请求路由到乘客管理服务。而当服务支持多个版本同时运行时,请求路由的逻辑会更加复杂一些,比如路径前缀 /trip/v1/ 和 /trip/v2/ 的请求分别路由到不同版本的行程管理服务。API 网关的路由通过静态或动态的方式进行配置,有些 API 网关支持通过开放 API 在运行时动态修改路由配置。

API 组合把来自不同服务的数据组合在一起,形成新的组合 API,这也是本模块需要介绍的内容。如果把不同服务 API 所提供的数据当成是数据库的表格,那么 API 组合就是表之间的连接操作。在组合 API 时,可以对 API 返回的结果进行投影、转换和充实。

  • 边界功能用来在接收到请求之后,在请求发送给后台服务之前,对请求进行处理。边界功能通常满足应用的一些横切需要,包括身份认证、授权管理、请求速率限制、缓存、性能指标数据收集、请求日志等。由于 API 网关实现了这些功能,可以简化后台服务的开发,这也是大部分 API 网关产品的卖点所在。

  • 协议翻译指的是把外部 API 的协议转换成内部服务之间使用的协议。外部 API 为了兼容性,一般使用 REST API 作为协议,而应用内部的服务之间,出于性能的考虑,可能使用 gRPC,甚至是私有的协议。API 网关负责在两种 API 协议之间进行转换。

由于 API 网关的重要性,云平台通常都提供了相关的服务。除此之外,还有很多开源和商用的产品,比较流行的产品包括 WSO2、Netflix Zuul、Spring Cloud Gateway、Kong 和 SwaggerHub 等。

从上述说明中可以看到,API 组合也属于 API 网关的一种功能。只不过 API 组合与应用的逻辑紧密相关,无法通过简单的配置来实现,一般需要编写代码或脚本来完成。

API 组合

在微服务架构的应用中,应用的功能被分散到多个微服务中。来自一个微服务的 API 并不能满足外部使用者的需求,因为一个微服务只能提供部分数据。比如,示例应用中的乘客管理界面需要使用来自乘客管理服务、地址管理服务、行程管理服务和行程派发服务的数据。因此,需要一种方式来提供给使用者所需要的全部数据。

第一种做法是由客户端根据需要来直接调用不同微服务的 API,这种做法在客户端和微服务之间建立了紧密的耦合关系,增加了客户端使用 API 的难度。当展示一个页面时,可能需要调用多次 API,相应的性能也会比较差。

另外一种做法是使用第 22 课时介绍的 CQRS 技术,针对客户端的不同需求,创建相应的查询服务,这种做法可以避免多次 API 调用的性能问题。不过 CQRS 技术使用的范围较窄,技术的门槛较高,在实践中的应用也比较少。

相比前两种做法,更好的做法是使用 API 组合,在应用内部创建进行 API 组合的服务。对客户端发送的 API 请求,该组合服务调用后台的多个微服务的 API,并把得到的数据进行整合,再返回给客户端。API 组合的好处是对微服务 API 的调用发生在系统内部,调用的延迟很小,也免去了客户端的多次调用。

Backend For Frontend 模式

当应用所要支持的客户端种类变多时,使用单一的通用 API 变得不再适用,这是由于不同客户端的差异性造成的。

桌面客户端的屏幕大、一般使用的是高速的 ADSL 或光纤网络;移动客户端屏幕小、网络速度较慢,而且对电池消耗有要求。

这就意味着在移动客户端上需要严格控制 API 请求的数量和响应的大小。移动客户端上的用户体验也与 Web 界面有很大差异,满足用户界面需求的 API 也相应地存在很大不同。如果使用单一的 API 为这两类客户端服务,那么这些差异性会使得 API 的维护成本变高。

Backend For Frontend 模式指的是为每一种类型的前端创建其独有的后端 API。这个 API 专门为前端设计,完全满足其需求,通常由该前端的团队来维护。

下图是 Backend For Frontend 模式的示意图,其中移动客户端和桌面客户端使用专门为它们设计的 API,这些 API 使用同样的后台服务作为数据来源。

在微服务架构的应用中,这种模式实际上更加适用,因为微服务已经把系统的功能进行了划分,在实现前端需要的 API 时,只需要把微服务的 API 进行整合即可,同时也对前端屏蔽了后端 API 的细节。

API 组合的实现

API 组合有很多种不同的实现方式,最简单的做法是基于已有的工具进行配置,复杂的实现则需要自己编写代码开发。

本课时介绍的是为管理乘客的 Web 界面创建的 API 组合,完整的实现请参考示例应用源代码中的 happyride-passenger-web-api 模块。

乘客管理 Web 界面在管理乘客时,需要用到乘客的地址信息,地址管理服务负责对地址进行统一管理,提供了地址的搜索和查询 API,地址管理服务对应的聚合的根实体是地址对象。在乘客管理服务中,表示用户地址的 UserAddress 对象只包含了地址对象的标识符,并没有包含其他信息。这就意味着当乘客查看或编辑地址时,对应的 API 需要调用地址管理服务的 API 来获取地址的详细数据。

Web 界面需要的这个 API 负责返回乘客的所有相关信息,包括每个地址的详细信息。而对于地址搜索和查询相关的请求,则直接转发给内部的地址管理服务。

示例应用使用 Spring Cloud Gateway 来实现该 API 组合。

1. Spring Cloud Gateway

Spring Cloud Gateway 是 Spring 框架提供的 API 网关的实现,基于 Spring Boot 2、Spring WebFlux 和 Project Reactor。在使用 Spring Cloud Gateway 之前,需要对反应式编程的概念有基本的了解。

反应式编程是一套完整的编程体系,既有其指导思想,又有相应地框架和库的支持,并且在生产环境中有大量实际的应用。Java 9 中把反应式流规范以 java.util.concurrent.Flow 类的方式添加到了 Java 标准库中,Spring 5 对反应式编程模型提供了支持,尤其是反应式 Web 应用开发使用的 WebFlux,Spring 5 默认的反应式框架是 Reactor。

Reactor 是一个完全基于反应式流规范的库,两个最核心的类是 Flux 和 Mono,用来表示流:

  • Flux 表示包含 0 到无限个元素的流;

  • Mono 则表示最多一个元素的流。

Flux 和 Mono 的强大之处来源于各种不同的操作符,可以对流中的元素进行不同的处理。

Spring Cloud Gateway 中有 3 个基本的概念,分别是路由、断言和过滤器。

  • 路由是网关的基本组成部分,由标识符、目的地 URI、断言的集合和过滤器的集合组成。

  • 断言用来判断是否匹配 HTTP 请求,本质上是一个 Java 中的 Predicate 接口的对象,进行判断时的输入类型是 Spring 的 ServerWebExchange 对象。

  • 过滤器用来对 HTTP 请求和响应进行处理,它们都是 GatewayFilter 接口的对象,多个过滤器串联在一起,组成过滤器链。前一个过滤器的输出作为下一个过滤器的输入,这一点与 Servlet 规范中的过滤器是相似的。

当客户端的请求发送到网关时,网关会通过路由的断言来判断该请求是否与某个路由相匹配。如果找到了对应的路由,请求会由该路由的过滤器链来处理,过滤器既可以在请求发送到目标服务之前进行处理,也可以对目标服务返回的响应进行处理。

Spring Cloud Gateway 提供了两种方式来配置路由,一种方式是通过配置来声明,另一种是通过代码来完成。

Spring Cloud Gateway 提供了大量内置的断言和过滤器的工厂实现。以断言来说,可以通过 HTTP 请求的头、方法、路径、查询参数、Cookie 和主机名等来进行匹配;以过滤器来说,内置的过滤器工厂可以对 HTTP 请求的头、路径、查询参数和内容进行修改,也可以对 HTTP 响应的状态码、头和内容进行修改,还可以添加请求速率限制、自动重试和断路器等功能。

2. 具体实现

对于本课时要实现的 API 组合来说,地址相关的 API 只需要转发给地址管理服务即可,这可以通过 Spring Cloud Gateway 的配置来完成。下表是 API 组合所提供的功能。

API 路径说明
/address/**转发给地址管理服务
/passenger/{passengerId}获取乘客的详细信息,组合来自乘客管理服务和地址管理服务的数据

在下面的配置中,标识符为 address_service 的路由的目的地 URI 由配置项 destination.address 来确定,使用的断言是 Path 类型,也就是根据请求的路径来判断,即以 /address 开头的全部请求。使用的过滤器是 StripPrefix,也就是去掉 URI 的路径中的一些前缀。在使用这个过滤器之后,路径 /address/search 会被替换为 /search,与地址管理服务的 API 路径相匹配。

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: address_service
          uri: ${destination.address}
          predicates:
            - Path=/address/**
          filters:
            - StripPrefix=1

在实现获取乘客详细信息的 API 时,需要对乘客管理服务返回的乘客信息进行修改,添加地址的详细信息。获取地址信息需要访问地址管理服务的 API,这里使用的是 Spring WebFlux 提供的反应式客户端 WebClient 对象。下面代码 AddressServiceProxy 中的 getAddresses 方法用来访问地址管理服务的 API,配置对象 DestinationConfig 中包含了地址管理服务的地址。

当访问 API 出现错误时,getAddresses 方法返回的 Mono 对象中包含的是一个空的列表,这样做可以保证乘客 API 在地址管理服务出现问题时,仍然可以返回有价值的部分数据。这也是错误处理的一种常见策略。

java
@Service
public class AddressServiceProxy {
  @Autowired
  DestinationConfig destinationConfig;
  public Mono<List<AddressVO>> getAddresses(final String addressIds) {
    return WebClient.create(this.destinationConfig.getAddress())
        .get()
        .uri(uriBuilder -> uriBuilder.path("/addresses/{addressIds}")
            .build(ImmutableMap.of("addressIds", addressIds)))
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<List<AddressVO>>() {})
        .onErrorReturn(Collections.emptyList());
  }
}

在实现乘客 API 时,需要用到修改 HTTP 响应内容的过滤器,该过滤器只能通过代码来配置,如下面的代码所示。RouteLocatorBuilder 构建器用来创建包含路由的 RouteLocator 对象,路由的标识符是 enrich_passenger,使用的断言基于请求的路径进行匹配。第一个过滤器 stripPrefix 去掉 /passenger 前缀,第二个过滤器 modifyResponseBody 声明了原始的响应内容的类型是 PassengerVO 对象,而修改之后的内容的类型是 PassengerResponse 对象。

在进行修改时,把 PassengerVO 对象中包含乘客的所有地址的标识符以逗号分隔并连接起来之后,调用 AddressServiceProxy 对象的 getAddresses 方法来批量获取地址的信息。最后把这两部分数据组合在 PassengerResponse 对象中,作为最终的响应。

java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
  @Autowired
  AddressServiceProxy addressServiceProxy;
  @Autowired
  DestinationConfig destinationConfig;
  @Bean
  public RouteLocator routes(final RouteLocatorBuilder builder) {
    return builder.routes()
        .route("enrich_passenger", r -> r.path("/passenger/{passengerId}")
            .filters(f -> f
                .stripPrefix(1)
                .modifyResponseBody(PassengerVO.class, PassengerResponse.class,
                    (exchange, passenger) -> {
                      final String addressIds = passenger.getUserAddresses()
                          .stream()
                          .map(UserAddressVO::getAddressId)
                          .collect(Collectors.joining(","));
                      return this.addressServiceProxy.getAddresses(addressIds)
                          .map(addresses ->
                              PassengerResponse
                                  .fromPassengerAndAddresses(passenger,
                                      addresses));
                    }))
            .uri(this.destinationConfig.getPassenger())).build();
  }
}

下面的 JSON 代码展示了乘客管理服务返回的乘客信息的数据。

json
{
    "email": "test@test.com",
    "id": "55af028b-6bd3-4266-b8db-70d2b0d2dc07",
    "mobilePhoneNumber": "13812345678",
    "name": "test",
    "userAddresses": [
        {
            "addressId": "c258ac5f-c86c-4fd0-b046-a17c047ba6a3",
            "id": "b11db379-f652-4aa9-ac4a-0cb0c0224b30",
            "name": "home"
        },
        {
            "addressId": "ba629ecf-3f92-4953-afe3-766a6586bbb5",
            "id": "63992a22-411d-49b0-893c-c5742d43d970",
            "name": "office"
        }
    ]
}

在经过 API 组合之后,乘客 API 返回的乘客信息的数据如下所示。从中可以看到,userAddresses 属性的值进行了修改,包含了地址的详细信息。

json
{
    "email": "33",
    "id": "54d4dd20-09dd-4105-b9aa-48560c841ca1",
    "mobilePhoneNumber": "33",
    "name": "bob",
    "userAddresses": [
        {
            "addressId": "585d747e-8605-442f-93bb-043aac15ea7e",
            "addressLine": "王府井社区居委会-0",
            "areaId": 16,
            "areas": [],
            "id": "83e5597d-dd8f-4906-ba8d-abf24a7754c2",
            "lat": 39.914211,
            "lng": 116.414808,
            "name": "xyz"
        },
        {
            "addressId": "df7eccb3-06d6-4400-b0e3-975be546c691",
            "addressLine": "王府井社区居委会-1",
            "areaId": 16,
            "areas": [],
            "id": "a604a3d3-6d01-497f-99c4-6a3de311cd9f",
            "lat": 39.914293,
            "lng": 116.414966,
            "name": "def"
        }
    ]
}

总结

为了满足不同客户端的需求,微服务架构的应用中通常需要使用 API 组合来创建客户端独有的 API。通过本课时的学习,你可以了解到 API 网关和 API 组合的基本概念,以及 Backend For Frontend 模式在实际开发中的作用,最后还可以掌握如何使用 Spring Cloud Gateway 来组合 API。