本章节将会介绍服务化拆分后,这些独立的服务间是如何进行 “通信” 的。最终的实战项目中,服务通信的实现基于 Open Feign,“通信”协议为 HTTP,所以也会结合实际的编码讲解 Java 语言中 HTTP 调用的不同实现方式。
认识服务通信
为什么需要服务通信?
有因必有果。那么导致服务通信出现的原因是什么?或者说为什么需要服务通信?
以创建外卖订单这个行为来举例,外卖 APP 的订单确认页面有如下信息:用户地址信息、菜品信息、红包信息、商家信息、配送信息、优惠券信息等等。在一切信息确认之后就可以生成这个外卖订单了,在订单功能的实现方法里会根据这些信息来创建一条订单数据。
在未做服务化拆分时,各个功能模块间在同一个工程中可以直接进行方法或功能的调用。比如当前所举例的外卖项目,当这些功能模块在同一个工程里时,直接在 OrderService 中调用每个功能实现类中的对应方法即可,如下图所示。
而微服务架构或者类微服务架构的项目本质上是运行在多台机器上的分布式系统,每个服务都是独立的,一个服务如果想要调用另外一个服务上的功能或方法就需要确保二者之间能够 “通信”。
举例的外卖 APP 项目中,上述所提到的服务如果都被服务化拆分并做成了一个个独立的服务,那么在创建订单时 OrderService 类就无法做到本地调用 UserService、FoodService、DeliveryService、CouponService 等等实现类中的方法了。此时,如果无法打通服务间的调用链路,订单生成功能就无从谈起了。
在分布式架构或者微服务架构中,服务是独立开发和独立部署的,物理层面是独立的。但是在具体的功能实现时,可能也需要各个服务的配合。
服务通信是什么?
当然,服务通信并不是一个非常复杂的概念。
最终的实战项目中,服务通信的实现基于 Open Feign,“通信” 协议为 HTTP。做一个简单的类比,单体应用中可以直接进行本地方法调用,使用 Feign 或者 Open Feign 就是基于 HTTP 的调用,需要发送 HTTP 请求和处理 HTTP 请求的回调。比如在创建订单时,OrderService 实现类需要调用菜品 FoodService 实现类中的方法 getFoodListByIds(),本地方法调用很简单,如下图所示:
拆分后的服务 OrderService 当然也需要调用菜品 FoodService 的方法 getFoodListByIds()。虽然无法直接调用 getFoodListByIds() 方法,但是 FoodService 会把 getFoodListByIds() 方法的结果通过 REST 接口的方式返回给调用端 OrderService,之后 OrderService 需要处理请求回调,最终得到的依然是 getFoodListByIds() 方法的结果,调用过程及注意事项如下图所示:
类比到现实世界中,小张和小李两个人如果在一间房子里,是可以直接对话的,小张问小李:“你今天写了几个 BUG?”,小李说:“我怎么可能写 BUG”。而如果两个人相隔很远,则只能通过通讯工具来实现了,比如打电话、发送 IM 消息、视频通话等等,两个人依然可以进行沟通。
所以服务通信这个概念中的 “通信” 本质上依然是方法调用,只是无法做到本地调用,需要借助其它技术来实现。最终的实战项目中,服务通信的实现基于 HTTP 的,就像是小张和小李通话时选择了打电话这种方式。当然也可以选择其它的技术实现,比如 Dubbo、gRPC、Thrift 等技术。HTTP、Dubbo、gRPC、Thrift 这些属于服务通信中的同步调用,就是严格的遵循“一问一答”,调用端发起一次“通信”,被调用端处理后需要及时回应,可能导致阻塞。而除了同步调用外,还有异步调用,常见的就是通过消息队列来实现,调用端与被调用端通过异步消息来通信和具体的功能实现,这种状态下,及时回应就不是必须的了,也不会导致阻塞。
HTTP 调用之编码实践
对于 HTTP 请求,读者应该都不会陌生。打开浏览器,在地址栏输入一个正确的网址即可获得响应内容,如下图所示。
在实际的服务调用中肯定不会返回一个页面,而是返回接口的响应内容,通常是 JSON 格式的字符串,在浏览器的结果如下图所示。
那么不借助浏览器,在 Java 代码里该如处理呢?接下来通过几份代码示例,讲解一下 Java 代码中如何发起 HTTP 请求和处理 HTTP 响应结果的。
被调用端编码实现
首先创建一个名称为 service-demo 的 Spring Boot 实例项目,端口号设置为 8081。之后分别创建 com.liboshuai.service 包和 com.liboshuai.web 包,分别存放业务层实现类和 REST 层的 Controller 类。
在 service 包中新建 HelloServiceImpl 类,代码如下:
package com.liboshuai.service;
import org.springframework.stereotype.Component;
@Component
public class HelloServiceImpl {
public String getName(){
return "service01";
}
}
定义了 getName() 方法,方法作用是返回一个字符串。
在 web 包中新建 HelloServiceController 类,代码如下:
package com.liboshuai.web;
import com.liboshuai.service.HelloServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloServiceController {
@Autowired
private HelloServiceImpl helloService;
@GetMapping("/hello")
public String hello() {
return "hello from " + helloService.getName();
}
}
该类使用了 @RestController 注解,所以并不会返回视图对象。类中定义了 hello() 方法,映射地址为 / hello,在访问该地址后会返回一串字符串给调用端。
本次代码主要用于功能演示,并没有做复杂的功能逻辑。
之后复制 service-demo 为 service-demo2,修改一下类名和配置文件中的端口号。这样就有了两个被调用端的实例,代码结构如下图所示。
编码完成后分别启动两个实例,启动成功后,可以分别访问两个接口地址:
如果项目中没有报错,且访问结果如下图所示,则编码完成。
被调用端的编码和验证都已经完成。接下来,就来编写调用端的代码。
Java 项目开发中,向其它服务发起 HTTP 调用是常见的功能需求,编码实现时需要使用客户端工具或者第三方提供的 HTTP 开发包。有很多常用的 HTTP 开发包供开发者们选择,比如 Java 自带的 HttpUrlConnection 类、HttpClient 工具包、Spring 提供的 RestTemplate 工具和 WebClient 工具。
笔者将使用 HttpClient、RestTemplate、WebClient 分别演示它们是如何对 HTTP 请求进行处理的。
使用 HttpClient 处理请求
创建一个名称为 request-demo 的 Spring Boot 实例项目,端口号设置为 8083。然后在 pom.xml 文件中添加 httpclient 的依赖配置,代码如下:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
之后创建 com.liboshuai.web 包,用于存放调用端所需的测试类。在 web 包中新建 ConsumerController 类,代码如下:
package com.liboshuai.web;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
public class ConsumerController {
private final String SERVICE_URL = "http://localhost:8081";
@GetMapping("/httpClientTest")
public String httpClientTest() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(SERVICE_URL + "/hello");
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} finally {
if (response != null) {
response.close();
}
httpClient.close();
}
return "请求成功";
}
}
定义了 SERVICE_URL 变量,用于存放请求地址。在 httpClientTest() 方法中,使用 HttpClient 工具对目标服务的接口进行了请求,并打印了接收到的请求结果。
编码完成后,依次启动 service-demo、service-demo2、request-demo 三个实例,启动成功后可以访问如下测试地址:
http://localhost:8083/httpClientTest
如果项目中没有报错,控制台打印出了此次的请求结果,则编码完成。
hello from service01
hello from service02
使用 RestTemplate 处理请求
RestTemplate 是 Spring 提供的一个 HTTP 请求工具,它提供了常见的 REST 请求方案的模版,简化了在 Java 代码中处理 http 请求的编码过程。接下来笔者将使用 RestTemplate 工具来完成 HTTP 请求的处理。
依然在 request-demo 项目中进行编码。首先,创建 config 包,并新建 RestTemplate 的配置类,代码如下:
package com.liboshuai.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.converter.StringHttpMessageConverter;
import java.nio.charset.Charset;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
restTemplate.getMessageConverters().set(1,
new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(10 * 1000);
factory.setConnectTimeout(5 * 1000);
return factory;
}
}
然后在 ConsumerController 类中引入 RestTemplate 对象,并使用它来发起请求和处理请求回调结果。代码如下:
@Resource
private RestTemplate restTemplate;
@GetMapping("/restTemplateTest")
public String restTemplateTest() {
System.out.println(restTemplate.getForObject(SERVICE_URL + "/hello", String.class));
return "请求成功";
}
在 restTemplateTest() 方法中,使用 RestTemplate 工具对目标服务的接口进行了请求,并打印了接收到的请求结果。相较于 HttpClient 工具,编码更加简单,不管是发起请求还是请求回调的处理都做了很多封装,方便开发者们使用。
编码完成后,依次启动 service-demo、service-demo2、request-demo 三个实例,启动成功后可以访问如下测试地址:
http://localhost:8083/restTemplateTest
如果项目中没有报错,控制台打印出了此次的请求结果,则编码完成。
hello from service01
hello from service02
使用 WebClient 处理请求
WebClient 是从 Spring WebFlux 5.0 版本开始提供的一个非阻塞的基于响应式编程的进行 HTTP 请求的客户端工具,它的响应式编程的基于 Reactor 的。与 RestTemplate 工具类似,都是 Spring 官方提供的 HTTP 请求工具,方便开发者们进行网络编程。
只是二者有些许不同, RestTemplate 是阻塞式客户端,WebClient 是非阻塞客户端,而且二者所依赖的 Servlet 环境不同,WebClient 是 Spring WebFlux 开发库的一部分,引入 starter 场景启动器时使用的依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
不用引入 spring-boot-starter-web。使用 RestTemplate 工具则直接引用 spring-boot-starter-web 即可。
复制 request-demo 为 request-demo2,修改 pom.xml 文件中的 web 场景启动器为 spring-boot-starter-webflux,然后新建 ConsumerController2 类,并新增如下代码:
package com.liboshuai.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@RestController
public class ConsumerController2 {
private final String SERVICE_URL = "http://localhost:8081";
private WebClient webClient = WebClient.builder()
.baseUrl(SERVICE_URL)
.build();
@GetMapping("/webClientTest")
public String webClientTest() {
Mono<String> mono = webClient
.get()
.uri("/hello")
.retrieve()
.bodyToMono(String.class);
mono.subscribe(result -> {
System.out.println(result);
});
return "请求成功";
}
}
在 webClientTest() 方法中,使用 WebClient 工具对目标服务的接口进行了请求,并打印了接收到的请求结果。相较于 RestTemplate 工具,编码方式有所区别,可以应用函数式编程与流式 API,支持 Reactive 类型(Mono 和 Flux)。
编码完成后,依次启动 service-demo、service-demo2、request-demo2 三个实例,启动成功后可以访问如下测试地址:
http://localhost:8084/webClientTest
如果项目中没有报错,控制台打印出了此次的请求结果,则编码完成。
hello from service01
hello from service02
最终代码结果如下图所示:
当然,读者也可以自行编码,直接分别创建 4 个 Spring Boot 项目进行编码即可。本章节里是为了功能演示和源码整理,才把所有代码放在了同一个工程里。
总结
在微服务架构中服务通信是一个非常核心的知识点,因为微服务需要使用进程间通信的机制进行交互。本章节由为什么需要服务通信这个问题开始讲起,之后介绍了服务通信究竟是什么,最后结合实际的编码介绍了基于 HTTP 协议的服务通信实践,希望读者能够有所收获。读者如果有任何问题或者想要和笔者讨论的内容,都可以在评论区留下看法,笔者会根据读者的反馈和问题继续整理和完善本章节内容。
原文地址 juejin.cn