为什么需要远程调用?
在微服务架构中,每个服务都是独立部署和运行的,它们之间需要相互协作以完成复杂的业务逻辑。因此,远程调用成为微服务之间通信的主要方式。通过远程调用,一个服务可以请求另一个服务执行某些操作或获取所需数据,从而实现服务的解耦和复用。
例如:在商城项目拆分中购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service
服务,导致我们无法查询。最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。
查询购物车列表的流程图:
远程调用的方式
微服务远程调用的方式多种多样,主要包括以下几种:
- HTTP/REST:
- 使用HTTP协议进行同步调用,通常使用JSON或XML作为数据交换格式。
- 优点:简单、通用性强、可跨语言、跨平台。
- 缺点:消息封装较为臃肿,对于大量数据或频繁调用的场景可能不是最优选择。
- RPC(远程过程调用):
- 允许像调用本地方法一样调用远程服务的方法。
- 优点:通信速度快、效率高、可自定义数据格式。
- 缺点:限制于开发语言环境,跨语言和跨平台能力较弱。
- 消息队列:
- 通过消息队列进行消息传递,实现服务之间的解耦。
- 优点:高可用性、高可扩展性、低耦合性。
- 缺点:实时性较弱,可能不适用于所有场景。
- 事件总线:
- 发布/订阅模型,通过事件进行通信。
- 优点:解耦程度高、灵活性好。
- 缺点:实现复杂度较高,需要维护事件的定义和订阅关系。
远程调用的实现框架
在微服务架构中,有多种框架支持远程调用,以下是一些常见的实现框架:
- RestTemplate:
- Spring 提供的用于访问 Rest 服务的客户端。
- 提供了多种便捷访问远程 HTTP 服务的方法。
- 示例代码:通过配置类注入 RestTemplate Bean,并在 Controller 中使用它发送 HTTP 请求。
- Feign:
- Netflix 开发的声明式、模板化的 Http 客户端。
- 通过处理注解相关信息生成 Request,并对调用返回的数据进行解码。
- OpenFeign 是 Spring Cloud 在 Feign 的基础上增加了对 SpringMVC 注解的支持。
- 示例代码:定义 Feign 客户端接口,使用 @FeignClient 注解指定要调用的服务,并在接口方法上使用 SpringMVC 注解定义请求路径和参数。
- gRPC:
- Google 开源的高性能、跨语言的 RPC 框架。
- 使用 Protocol Buffers 作为接口描述语言。
- 适用于对性能要求较高的场景。
- Dubbo:
- 阿里巴巴开源的 Java RPC 框架。
- 支持多种序列化方式,适用于 Java 微服务架构。
我们以RestTemplate为例,RestTemplate其中提供了大量的方法,方便我们发送Http请求,例如:
可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。
我们在服务中定义一个配置类:
先将RestTemplate注册为一个Bean:(首先需要去声明request method)
package com.hmall.cart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RemoteCallConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
接下来,我们修改service
中的服务实现类中方法,发送http请求到service
:
可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:
-
① 请求方式(master枚举:get、post等)
-
② 请求路径(请求的URL路径里面包含的首先就是请求的IP,然后是请求端口,最后才是真正的资源路径)
-
③ 请求参数(查询默认情况下服务端返回都是JASON格式的,但是Rest template可以帮你转成你想要的java类型。比如查到的是个user,那就可以写成user.class。它就会返回一个user,会把你查到的JASON的字符串反序列化)
-
④ 返回值类型(请求中如果有参数,比如说有路径占位符参数,可以使用map指定这个参数
map里面要指定一个key和value,key要跟占位符一致,value就是你的具体的参数)
在实现类中使用Rest template
最常规的方法是autowear注入
但是这种方式spring是不推荐的,它推荐采用构造函数注入,写一个构造函数spring也能帮你做自动注入。
但是假如成员变量很多,那构造函数的列表就会非常长。所以使用lombok可以帮助我们去自动生成构造函数。
什么是lombok库?
Lombok是一个Java库,它可以帮助开发者自动生成getter和setter方法、构造函数、equals、hashCode和toString方法等,从而减少样板代码,提高开发效率。
-
@AllArgsConstructor:这个注解会为类生成一个包含所有参数的构造函数。如果不希望某个变量成为构造函数的一部分,那么不应该使用这个注解。
-
@RequiredArgsConstructor:这个注解会为需要特殊处理的字段(如final字段和被
@NonNull
注解的字段)生成一个构造函数。这样,只有这些“必需”的字段才会被包含在构造函数中。 -
final字段:如果将某个字段声明为final,那么它必须在声明时或在构造函数中进行初始化。使用
@RequiredArgsConstructor
注解时,Lombok会自动为这些final字段生成一个构造函数,以确保它们被正确初始化。 -
手动初始化:如果已经在字段声明时对其进行了初始化,那么即使使用了
@AllArgsConstructor
或@RequiredArgsConstructor
注解,该字段也不会被包含在自动生成的构造函数中,因为它已经被初始化了。
实现类的方法的完整代码如下:
private void handleCartItems(List<CartVO> vos) {
// TODO 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
// 2.1.利用RestTemplate发起http请求,得到http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
// 2.2.解析响应
if(!response.getStatusCode().is2xxSuccessful()){
// 查询失败,直接结束
return;
}
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
return;
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}