SpringCloud-服务间调用

服务间调用

Sprint提供了RestTemplate工具,可以实现服务间调用

注入IOC的方式

Spring推荐使用构造函数注入,而不是简单的Autoware注入

1
2
3
4
private RestTemplate restTemplate;
public CartServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

我们可以通过搭配使用lombok来实现注解注入构造函数

@AllArgsConstructor注解可以为所有成员变量生成构造函数,但这种方式并不是我们想要的

因为有一些变量是无需加入构造函数的(也无需使用IOC容器注入)

用final来修饰的变量必须有初值,所以我们可以搭配@RequiredArgsConstructor注释,实现给必要的变量生成构造函数,其最终效果为:为使用final修饰的属性加构造函数

1
2
3
4
5
6
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final RestTemplate restTemplate;
……
}


RestTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class YourService {

private final RestTemplate restTemplate; // 假设 RestTemplate 已经被注入

public YourService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public void processCarts(List<Long> itemIds) { // 假设 itemIds 是一个商品ID列表
// 1. 构建请求URL和参数
// 使用 Hutool 的 CollUtil.join 方法,将商品ID列表 (itemIds) 转换为逗号分隔的字符串
// 例如:[1, 2, 3] -> "1,2,3"
String idsParam = CollUtil.join(itemIds, ",");

// 2. 使用 RestTemplate 发送 GET 请求到远程商品服务
// "http://localhost:8081/items?ids={ids}" 是请求的URL模板
// HttpMethod.GET 指定请求方法为 GET
// null 表示请求体为空 (GET 请求通常没有请求体)
// new ParameterizedTypeReference<List<ItemDTO>>() {} 用于处理泛型响应类型,确保正确反序列化为 List<ItemDTO>
// Map.of("ids", idsParam) 将 idsParam 映射到 URL 模板中的 {ids} 占位符
ResponseEntity<List<ItemDTO>> re = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}", // 请求URL,包含占位符
HttpMethod.GET, // 请求方法
null, // 请求实体 (无请求体)
new ParameterizedTypeReference<List<ItemDTO>>() {}, // 响应类型 (泛型列表)
Map.of("ids", idsParam) // URL参数,使用Hutool处理后的ID字符串
);

// 3. 检查HTTP响应状态码
// re.getStatusCode().is2xxSuccessful() 判断响应状态码是否为 2xx 系列 (表示成功)
// 如果请求失败 (例如 4xx, 5xx 错误),则直接返回,不继续处理
if (!re.getStatusCode().is2xxSuccessful()){
// 可以在这里添加日志记录或抛出自定义异常
return;
}

// 4. 从成功响应中获取数据体
// re.getBody() 获取 HTTP 响应的实际数据内容,即 List<ItemDTO>
List<ItemDTO> items = re.getBody();

// 5. 再次检查获取到的数据列表是否为空
// 使用 Hutool 的 CollUtil.isEmpty 方法,判断列表是否为 null 或不包含任何元素
// 如果列表为空,则直接返回,终止后续处理
if (CollUtil.isEmpty(items)) {
// 可以在这里添加日志记录
return;
}
}
}

注册中心

为了解决HttpTemplate的一系列服务间调用问题,我们使用服务中心来进行最佳实践

很多框架中都提供了注册中心的服务,我们来学习经典的一款——nacos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
services:
nacos:
image: nacos/nacos-server:v2.1.0-slim # 推荐使用特定版本,避免不兼容问题
container_name: nacos-server
env_file:
- ./custom.env # 引用外部环境变量文件
# volumes:
# 持久化Nacos的配置和数据,防止容器删除后数据丢失
# 请根据你的实际需求调整路径
# - ./logs:/home/nacos/logs
# - ./conf:/home/nacos/conf
ports:
- "8848:8848" # Nacos 控制台端口
- "9848:9848" # Nacos GRPC 端口 (客户端连接)
- "9849:9849" # Nacos GRPC 端口 (集群间通信,单机模式也需要)
networks:
- my_app_network # 将Nacos服务加入到你创建的共享网络
restart: no # 容器退出后总是重启

networks:
my_app_network:
external: true # 告知Compose这个网络已经存在,不要尝试创建它

1
2
3
4
5
6
7
8
9
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=mysql
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=Zhuwenxue2002
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

注:这里使用docker网络管理的最佳实现,即自定义网络之后,让相关联服务都加入此网络,可直接通过服务名称代替主机名访问服务


服务注册

引入nacos discovery依赖

1
2
3
4
5
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置nacos地址

1
2
3
4
5
6
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址

配置好服务后,nacos的web管理界面http://localhost:8848/nacos/index.html的服务列表中就可以发现服务

请注意,这里的服务名就是yaml的配置项spring-application-name,当配置多台服务时,我们只要保证服务名相同,即达成同一服务的多个实例配置


服务依赖

同样的,我们先来引入依赖(与服务注册相同的依赖)

1
2
3
4
5
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置nacos地址(也相同)

1
2
3
4
5
6
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址

DiscoveryClient接口是SpringCloud定义的标准接口,通过DI注入来获取到Nacos针对此接口的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过discoveryClient拉取实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if (CollUtils.isEmpty(instances)) {
return;
}
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));

ResponseEntity<List<ItemDTO>> re = restTemplate.exchange(instance.getUri() + "/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtils.join(itemIds, ",")));

if (!re.getStatusCode().is2xxSuccessful()){
return;
}
List<ItemDTO> items = re.getBody();

if (CollUtils.isEmpty(items)) {
return;
}

这样,我们通过动态Nacos获取到服务的URI,即可直接访问服务

  • 在服务的时候,可以实现负载均衡的访问
  • 在某一个服务挂掉的时候,仍然可以正常通过其他服务访问

OpenFeign

注入依赖

1
2
3
4
5
6
7
8
9
10
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

在启动类上加上@EnableFeignClients注解,开启OpenFeign

1
2
3
4
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
@EnableFeignClients
public class CartApplication {

编写FeignClient,将服务间的API写成类Controller的形式

1
2
3
4
5
@FeignClient("item-service")
public interface ItemClient {
@GetMapping
List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids );
}

OKHttp

OpenFiegn对Http请求做了优雅的封装,其底层默认使用HttpURLConection的发送方式(JDK自带)

性能很低且不支持连接池,但OpenFiegn对于底层的连接方式做了可插拔式设计

我们可以通过简单配置为其更换其他底层连接方式(支持连接池):

  • Apache HttpClient
  • OKHttp

首先引入okhttp的依赖

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>

开启feign-okhttp的支持

1
2
3
feign:
okhttp:
enabled: true # 开启OKHttp连接池支持

至此,OpenFiegn的底层已经是使用OkHttp进行发送请求,且支持连接池


最佳实践

方案一:重构服务

将一个微服务重构,每一个微服务再拆成三个模块:

  • api:存放api供外部调用
  • dto:api所需传输实体
  • biz:属于微服务本身的业务逻辑

如果有需求,就将api和dto两个模块作为依赖引入,就可以直接实现服务间调用


方案二:抽取API

将所有的API以及DTO抽取出到一个模块中,所有项目都依赖此模块

将API独立模块之后,Feign会找不到API包模块,需要指定FeignClient位置才行

方式一:指定所在包

@EnableFeignClients(basePackages = “com.hmall.api.clients”)

方式二:指定字节码文件

@EnableFeignClients(clients = {UserClient.class})


日志管理

默认情况下OpenFeign不提供任何的日志记录,一般情况下也不需要配置日志,只有调试时需要

创建一个配置类,但不要加配置类注解

1
2
3
4
5
6
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

为单个client配置日志

1
2
3
4
5
@FeignClient(value = "item-service",configuration = DefaultFeignConfig.class)
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids );
}

为该服务整体配置日志

1
2
3
4
5
6
7
8
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}


SpringCloud-服务间调用
http://blog.170827.xyz/2025/06/16/SpringCloud-服务间调用/
作者
XIAOBAI
发布于
2025年6月16日
许可协议