02拆分项目并使用nacos注册、OpenFeign实现项目间访问
微服务是一种软件架构风格,它以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用。本文将围绕单体架构的不足、微服务的拆分思想、Nacos 注册中心以及 OpenFeign 远程调用展开,最终把黑马商城从单体项目拆分为五个独立的微服务。
一、认识微服务
1.1 单体架构
单体架构将业务的所有功能集中在一个项目中开发,打成一个包部署。以黑马商城为例,商品管理、用户管理、购物车管理、交易管理等功能都集中在同一个 Project 中。

单体架构的优缺点比较明显:
- 优点:架构简单、部署成本低
- 缺点:团队协作成本高、系统发布效率低、系统可用性差
总结来说,单体架构适合开发功能相对简单、规模较小的项目。
1.2 微服务架构
微服务架构是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目,每个微服务具备粒度小、团队自治、服务自治的特点。

拆分后形成的是一个微服务集群,集群内还需要服务治理、远程调用、请求路由、配置管理、服务保护等能力共同支撑,最终对外提供完整的业务能力。

1.3 SpringCloud
SpringCloud 是目前国内使用最广泛的微服务框架,官网地址 https://spring.io/projects/spring-cloud。它集成了各种微服务功能组件,并基于 SpringBoot 实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
- 服务注册发现:Eureka、Nacos、Consul
- 服务远程调用:OpenFeign、Dubbo
- 服务链路监控:Zipkin、Sleuth
- 统一配置管理:SpringCloudConfig、Nacos
- 统一网关路由:SpringCloudGateway、Zuul
- 流控、降级、保护:Hystrix、Sentinel

SpringCloud 基于 SpringBoot 实现了微服务组件的自动装配,但对 SpringBoot 的版本也有要求,常见版本对应关系如下:
| SpringCloud 版本 | SpringBoot 版本 |
|---|---|
| 2022.0.x aka Kilburn | 3.0.x |
| 2021.0.x aka Jubilee | 2.6.x, 2.7.x (Starting with 2021.0.3) |
| 2020.0.x aka Ilford | 2.4.x, 2.5.x (Starting with 2020.0.3) |
| Hoxton | 2.2.x, 2.3.x (Starting with SR5) |
| Greenwich | 2.1.x |
| Finchley | 2.0.x |

二、微服务拆分
2.1 服务拆分原则
什么时候拆分?需要区分项目类型:
- 创业型项目:先采用单体架构,快速开发、快速试错。随着规模扩大,再逐渐拆分。
- 确定的大型项目:资金充足、目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。
怎么拆分?从目标上来说要做到:
- 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
- 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖。
从拆分方式来说,一般包含两种:
- 纵向拆分:按照业务模块来拆分。
- 横向拆分:抽取公共服务,提高复用性。
2.2 拆分黑马商城
黑马商城包含用户、商品、购物车、订单、支付五大模块,工程结构通常有两种方式:独立 Project 或 Maven 聚合。本项目中采用 Maven 聚合的方式,在父工程 hmall 下分别建立各业务 module。
按照需求,将 hm-service 中与商品管理相关的功能拆分到 item-service,与购物车相关的功能拆分到 cart-service,再依次建立 user-service、trade-service、pay-service。
2.3 远程调用
服务拆分后立刻会遇到一个问题:购物车服务在填充购物项的商品信息时,需要查询商品服务的数据,但两者已经不在同一个进程内,无法直接调用本地方法。

Spring 提供了 RestTemplate 工具,可以方便地发送 HTTP 请求。使用步骤如下:
- 注入
RestTemplate到 Spring 容器:
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}- 调用
exchange方法发起远程调用:
public <T> ResponseEntity<T> exchange(
String url, // 请求路径
HttpMethod method, // 请求方式
@Nullable HttpEntity<?> requestEntity, // 请求实体,可以为空
Class<T> responseType, // 返回值类型
Map<String, ?> uriVariables // 请求参数
)例如,查询商品信息:
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?id={id}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {},
Map.of("id", "1")
);但这种方式存在明显缺陷:URL 中写死了 localhost:8081,当 item-service 部署多实例时无法选择,这就引出了服务治理的需求。
三、服务治理
3.1 注册中心原理
当 item-service 部署了多个实例时,购物车服务作为调用方无法知道该访问哪一个。注册中心正是用来解决这一问题:服务提供者启动时把自身信息注册到注册中心,消费者从注册中心订阅和拉取服务列表,再通过负载均衡挑选一个实例发起调用。
服务治理中有三个角色:
- 服务提供者:暴露服务接口,供其它服务调用。
- 服务消费者:调用其它服务提供的接口。
- 注册中心:记录并监控微服务各实例状态,推送服务变更信息。
其工作机制如下:
- 服务提供者启动时注册自己信息到注册中心。
- 消费者从注册中心订阅和拉取服务信息。
- 服务提供者通过心跳机制向注册中心报告自己的健康状态;当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者。
- 消费者通过负载均衡算法,从多个实例中选择一个发起远程调用。
3.2 Nacos 注册中心
Nacos 是目前国内企业中占比最多的注册中心组件,由阿里巴巴出品,目前已加入 SpringCloudAlibaba 体系。

3.2.1 服务注册
服务注册的步骤如下:
- 引入 nacos discovery 依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>- 在
application.yaml中配置 Nacos 地址:
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址启动项目后,可在 Nacos 控制台看到 item-service 的服务实例信息。
3.2.2 服务发现
消费者需要连接 Nacos 以拉取和订阅服务,因此服务发现的前两步与服务注册一致(引入依赖、配置地址),最后再加上服务调用即可。SpringCloud 已经帮我们自动装配了 DiscoveryClient,可直接注入使用:
private final DiscoveryClient discoveryClient;
private void handleCartItems(List<CartVO> vos) {
// 1.根据服务名称,拉取服务的实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
// 2.负载均衡,挑选一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
// 3.获取实例的IP和端口
URI uri = instance.getUri();
// ... 后续借助 RestTemplate 发起调用
}四、OpenFeign
4.1 快速入门
虽然借助 RestTemplate + DiscoveryClient 已经能完成远程调用,但代码相对繁琐。OpenFeign 是一个声明式的 HTTP 客户端,是 SpringCloud 在 Eureka 公司开源的 Feign 基础上改造而来。其作用就是基于 SpringMVC 的常见注解,帮我们优雅地实现 HTTP 请求的发送。

一次完整的远程调用需要四个要素:服务名称、请求方式和路径、返回值类型、请求参数。OpenFeign 已经被 SpringCloud 自动装配,实现起来非常简单:
- 引入依赖,包括 OpenFeign 和负载均衡组件 SpringCloud LoadBalancer:
<!--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 功能:
@EnableFeignClients
@SpringBootApplication
public class CartApplication { // ... 略 }- 编写 FeignClient:
@FeignClient(value = "item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}- 使用 FeignClient 实现远程调用,调用方式与本地方法一致:
List<ItemDTO> items = itemClient.queryItemByIds(List.of(1, 2, 3));OpenFeign 会自动从注册中心拉取 item-service 的实例列表,并通过 SpringCloud LoadBalancer 进行负载均衡,最终以 HTTP 方式发起请求。
4.2 连接池
OpenFeign 对 HTTP 请求做了优雅的伪装,但其底层发起 HTTP 请求依赖于其它框架。这些框架可以自行选择,主要包括三种:
- HttpURLConnection:默认实现,不支持连接池
- Apache HttpClient:支持连接池
- OKHttp:支持连接池
OpenFeign 整合 OKHttp 的步骤如下:
- 引入依赖:
<!--ok-http-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>- 在
application.yaml中开启连接池功能:
feign:
okhttp:
enabled: true # 开启OKHttp连接池支持4.3 最佳实践
随着服务数量增多,每个服务都各自维护一份 FeignClient 会导致重复代码和数据传输对象。最佳实践是由服务提供者编写独立 module,将 FeignClient 及 DTO 抽取到一个公共 API 模块(如 hm-api),消费方只需引入该模块即可调用。

当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围时,可以通过两种方式解决:
- 方式一:指定 FeignClient 所在包:
@EnableFeignClients(basePackages = "com.hmall.api.clients")- 方式二:指定 FeignClient 字节码:
@EnableFeignClients(clients = {UserClient.class})4.4 日志
OpenFeign 只会在 FeignClient 所在包的日志级别为 DEBUG 时才会输出日志,且日志级别有 4 级:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法、URL 以及响应状态码和执行时间。
- HEADERS:在 BASIC 的基础上,额外记录请求和响应的头信息。
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
由于 Feign 默认的日志级别就是 NONE,所以默认看不到请求日志。要自定义日志级别,需要声明一个类型为 Logger.Level 的 Bean:
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.FULL;
}
}如果只想配置某个 FeignClient 的日志,可以在 @FeignClient 注解中声明:
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)如果想要全局配置,让所有 FeignClient 都按照这个日志配置,则在 @EnableFeignClients 注解中声明:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)总结
回顾本文,主要完成了以下几件事:
- 什么时候拆分微服务?初创型公司或项目尽量采用单体项目,快速试错,随着项目发展到达一定规模再做拆分。
- 如何拆分微服务?目标是高内聚、低耦合;方式是纵向拆分和横向拆分。
- 拆分后碰到的第一个问题是什么,如何解决?拆分后某些数据在不同服务,无法直接调用本地方法查询,可利用
RestTemplate发送 HTTP 请求实现远程调用。 - 如何优雅地实现服务间调用?使用 Nacos 作为注册中心管理服务实例,使用 OpenFeign 声明式客户端简化 HTTP 请求的发送。
- 如何进一步优化 OpenFeign?通过 OKHttp 连接池提升性能,通过抽取公共 API 模块复用 FeignClient,通过
Logger.Level配置日志级别方便调试。