微服务的架构设计之前总结过,微服务的思想是分离,微服务模式下将应用程序拆分为不同的微小服务,通过使用或者组合不同的服务来完成不同的业务功能。那么一旦分离后再组合,就意味着服务之间一定会存在相互调用的过程,在前面微服务的定义中提到过,微服务之间都使用粗糙的通信机制,它一定是轻量级的,而且是可以支持跨语言调用的,包括微服务本身对客户端提供服务也是采用这种机制的。因此,设计并实现适合的通信组件来提供远程调用的能力是微服务架构的核心。下面来了解微服务的远程调用方式。
微服务架构中常用的调用方式有两种:一种是通过异步的消息交换来通信,这里服务调用双方一般使用一些中间件,如RabbitMQ、Kafka等;另一种是通过HTTP的资源接口,通过JSON作为信息载体的格式,即REST API的方式进行远程通信。
1. 异步消息通信
先来看一下异步消息是如何通信的,这里以RabbitMQ的工作原理为例,如图2.6所示。
首先消息由生产者发送到一个统一的交换中心(又称交易所),交换中心根据一定的转发规则(如直接、主题匹配、扇出等方式),将消息转发到对应的队列中,然后通过消息队列最终将消息传递给消费者。消费者可以选择只订阅自己关心的消息。
可以利用消息机制来做很多事情,如可以做一个简单的任务队列,也可以去订阅和发布消息。当然,订阅和发布的方式有很多,最常见的就是图2.7RabbitMQ远程调用工作原理中描述的定向转发和Topic机制,这里对消息机制的原理不做阐述,利用这种发布和订阅的工作方式,我们可以通过消息做到服务的远程调用。
但是,在微服务中这并不是常见的用法,因为本身消息机制的实现会依赖相关的中间件或框架,服务调用双方都需要集成相同的消息服务或技术框架,这本身就会加重微服务架构的通信方式,而且过度松散异步的操作也为代码带来了一定的复杂度。所以,在微服务中最常见的通信还是基于HTTP的REST API。
2. REST API
REST(Representational State Transfer,表述性状态转移)用来描述创建HTTP API的标准方法,REST API的核心概念是资源,对于资源有4种常见的行为:查看、创建、编辑和删除,都可以直接映射到HTTP中已实现的GET、POST、PUT和DELETE方法。REST本身并没有创造新的技术、组件或服务,只是正确地使用Web的现有特征和能力,更好地使用现有Web标准中的一些准则和约束。
如果一个架构符合REST的约束条件和原则,就称它为RESTful架构,当然理论上REST架构风格并不是绑定在HTTP上的,只不过目前HTTP是唯一与REST相关的实现,所以一般情况下REST API都是表示基于HTTP的RESTful接口。
既然是一种标准、一种架构风格,就必然会有它的相关概念和设计原则。表2.1所示为REST API中的一些重要术语。
同时,REST API也有很多设计原则,如URL中永远不能包含动词,那么URL如何表示自己的行为呢?前面提到过4种常见的行为(查看、创建、编辑和删除)映射到HTTP中已实现的方法中,具体如下。
(1)GET(查看):从服务器或资源列表中检索特定资源。
(2)POST(创建):在服务器上创建一个新资源。
(3)PUT(编辑):更新服务器上的资源,提供整个资源。
(4)PATCH(编辑):更新服务器上的资源,仅提供已更改的属性。
(5)DELETE(删除):从服务器中删除资源。
下面两个不是很常用。
(1)HEAD(查看):检索有关资源的元数据,如数据的哈希值或上次更新的时间。
(2)OPTIONS(查看):检索有关允许消费者使用资源的信息。客户端和服务端的交互是无状态的,GET请求通常是可以被缓存的,资源使用复数,URL中可以有表述版本的信息,举例如下。
按照顺序对应的含义如下。
(1)使用2.0版本的接口,创建一个产品。
(2)使用2.0版本的接口,根据ID查看产品信息。
(3)使用1.0版本的接口,根据ID全量更新产品信息。
(4)使用1.0版本的接口,根据ID部分更新产品信息。
(5)使用1.0版本的接口,根据ID删除产品。
RESTful的接口还有很多设计原则,这里不再赘述,感兴趣的读者可以查阅相关资料进行学习,在微服务中使用的HTTP通信协议就是采用的RESTful的接口设计,所以熟练掌握REST API的设计用法在微服务架构中是十分重要的。
一般在项目中采用什么技术来完成HTTP通信?在一些早期的项目中,可以看到Apache HttpComponents的身影,它的功能也十分强大,但是在使用时,需要编写大量的基础代码,往往还需要进行二次封装,而在如今Spring Boot盛行的时代,大家更热衷于现取现用,正所谓约定大于配置,所有的基础工作都按照一定的约定交由框架来完成。下面以Spring为例,主要介绍RestTemplate和WebClient两种方式进行HTTP的通信。
1. RestTemplateRestTemplate是Spring Web中提供的用于在客户端完成同步的HTTP请求的核心类,大大简化了与HTTP服务器的通信,并实现了RESTful的设计原则。RestTemplate默认采用JDK原生的HTTP连接工具实现,当然也可以切换库,如Apache HttpComponents、Netty和OKHttp等第三方的HTTP库。我们以默认的实现为例,首先需要声明一个RestTemplate,这里采用Spring Boot的注解式声明方式,代码如下。
当然,还可以给它初始化一些公共属性,如URL、用户名密码,或者一些连接超时等设置,代码如下。
完成 RestTemplate 的声明之后就可以使用它了 , 之前说过RestTemplate实现了RESTful的设计原则,所以RestTemplate提供了便捷的方法去实现HTTP的GET、POST、PUT、PATCH和DELETE方法。例如,getForEntity()就是以GET的方式发送HTTP请求,而postForEntity()则 是 以 POST 的 方 式 发 送 HTTP 请 求 。 当 然 , 还 有 其 他 实 现 , 如patchForObject()、put()、delete()和optionsForAllow()等方法,这里就不一一介绍了。下面以GET和POST两种最常见的方式来介绍RestTemplate的用法,其他的大同小异。
(1)RestTemplate的GET方法。
RestTemplate提供了getForObject和getForEntity两种方式发送GET的HTTP请求,其中getForObject方法可以直接将响应的Body转换为指定的类型,方法定义如下。
其中,第一个方法比较常用,按照顺序传递URL的参数,通过在URL中定义{}来表示参数的站位,{}中可以写具体含义的单词,也可以写数字,如{0},代码如下。
此外,getForObject还提供了另外两种重载方法,分别提供了通过Map传递参数和没有参数两种功能。没有参数很好理解,不再赘述,下面的代码展示了通过Map传递参数的方式。
不难看出,Map中的key对应着URL中{}里的单词,使用Map的不足之处是当参数太多时,顺序容易弄错,而且方法会写得很长,不易读,也不易维护。getForObject方法能满足我们大部分的需求,但有时可能需要获取除Body之外的信息,如响应头、响应状态码等,这时就需要getForEntity了,getForEntity和getForObject一样,提供了3种实现,方法定义如下。
可以发现,getForEntity的方法参数和getForObject的一样,唯一的区别是getForEntity的返回类型是ResponseEntity。这里不再对每个方法进行详细介绍了,下面还是以第一个方法为例,代码如下。
(2)RestTemplate的POST方法。
与GET的方式一样,POST也提供了postForObject和postForEntity两种方式来完成POST的HTTP请求。方法定义如下。
POST的6个方法定义与GET几乎一样,所以这里没有把方法说明粘贴进来,仔细观察可以发现,POST的方法多了一个request的参数,这个参数会被放进请求的Body中,当没有需要时也可以传入null,举例如下:
关于RestTemplate的其他方法就不再列举了,感兴趣的读者可以查看Spring Web库的源码,或者访问Spring的官网查阅相关教程。
2. WebClient
WebClient相比RestTemplate是一个较新的HTTP访问方式,之前提到过,RestTemplate是一个同步的请求方式,当请求发出后,当前线程会等待,直到有响应后才会继续执行后续代码。其实远程调用是一个可以异步的过程,在等待请求响应时,我们完全可以做其他的事情,所以RestTemplate在一些性能要求比较高的地方使用就显得不是那么合适了。
这时就需要使用可以异步完成请求的WebClient了,当然,我们可以仍然使用RestTemplate,然后通过线程池或CompletableFuture等方式创建新的线程来执行RestTemplate的请求而不阻塞当前线程的执行。不过这样做不是特别优雅,而且每次还需要自行维护关于创建不同线程的代码。
随着JAVAScript的Reactive设计理念越来越流行,不少语言和框架开始相继模仿。Spring也采用了“No blocking”(无阻塞)的方式,推出了Spring WebFlux,关于WebFlux的功能有很多,这里主要来看一下WebFlux中WebClient的用法。
相比RestTemplate,WebClient最大的优势就是可以使用Reactive的方式执行非阻塞的HTTP请求,即异步的请求服务端。WebClient同样实现了RESTful的设计原则,支持GET、POST、PUT、PATCH和DELETE等操作,而且写法更接近于流式,示例代码如下。
这是一个GET方法,代码很好理解,只不过是流式的写法,先定义HTTP的方法,然后定义URI,最后定义返回类型。再来看一个POST的例子,代码如下。
很显然,相比GET方法多一个syncBody 方法,类似于RestTemplate的request参数,这个方法会把该参数当作请求的Body发送到服务端。仔细观察可以发现,与RestTemplate相比有一个最大的不同,就是方法的返回值变成了Mono类型,其实除了Mono类型,WebFlux还提供了Flux类型,代码如下。
首先通过代码可以发现,Mono和Flux的区别在于前者是单个元素,后者是集合,当然这不是最关键的,Mono中也可以是一个集合,如Mono<List<User>>、Mono和Flux之间可以相互转换。关键在于WebClient是异步的请求,在调用它时得到的返回类型不可能是直接的期待返回的类型,如findAllUsers得到的不是List<User>类型,而是Flux<User>类型。
这就好比我们通过CompletableFuture创建了一个线程A,然后在执行时会立刻返回一个CompletableFuture类型的对象,这时主线程就不会阻塞,而是继续执行。当我们需要得到返回值时,可以通过CompletableFuture的join方法,将线程A加入当前线程,这时如果线程A已经执行完成,那么当前线程就会立刻得到线程A的执行结果,如果线程A还没有执行完,那么当前线程就会等待,直到线程A执行完成。这是一个典型的异步执行方法的例子,而WebClient的Mono和Flux就和CompletableFuture具有相同的作用,而且更加强大。
例如,可以提前定义对返回数据的操作,代码如下。
再如,可以打包合并多个Mono或Flux,然后只执行一次join,代码如下。
可以看到,Mono提供了类似join的方法:block。如果你更喜欢使用 CompletableFuture , Mono 和 Flux 也可以轻松地转换成CompletableFuture,代码如下。