续上篇: SpringBoot:将单体项目拆分成微服务项目-CSDN博客
参考:黑马程序员之微服务
💥 该系列属于【SpringBoot基础】专栏,如您需查看其他SpringBoot相关文章,请您点击左边的连接
目录
[1. 基本概念](#1. 基本概念)
[2. Nacos注册中心](#2. Nacos注册中心)
[3. 服务注册【服务提供者在注册中心注册】](#3. 服务注册【服务提供者在注册中心注册】)
[4. 服务发现【服务消费者从注册中心订阅服务】](#4. 服务发现【服务消费者从注册中心订阅服务】)
[1. OpenFeign简介](#1. OpenFeign简介)
[2. 用法](#2. 用法)
[3. OpenFeign基于连接池优化](#3. OpenFeign基于连接池优化)
[1. 思路分析](#1. 思路分析)
[2. 抽取Feign客户端](#2. 抽取Feign客户端)
[3. 扫描包](#3. 扫描包)
[1. 定义日志级别](#1. 定义日志级别)
[2. 配置](#2. 配置)
前置项目结构
里面有三个微服务,端口号如下:
一、服务注册和发现
在微服务远程调用的过程中,包括两个角色:
-
服务提供者:提供接口供其它微服务访问,比如
item-service
-
服务消费者:调用其它微服务提供的接口,比如
cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。
1. 基本概念
(1)注册中心、服务提供者、服务消费者三者间关系
(2)调用服务的流程
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
(3)服务提供者的实例宕机或启动新实例发生的情况
-
服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
-
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
2. Nacos注册中心
目前开源的注册中心框架有很多,国内比较常见的有Nacos。Nacos为Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用。
下面基于Docker来部署Nacos的注册中心,首先要准备MySQL数据库表,用来存储Nacos的数据,然后将SQL文件导入到你Docker中的MySQL容器中,导入后如下:
然后编写nacos环境,保存到nacos/custom.env
中:
bash
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.88.128
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=wangjx17
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
将nacos
目录上传至虚拟机的/root
目录。进入root目录,然后执行下面的docker命令:
bash
docker run -d \
--name nacos \
--env-file /root/nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
启动完成后,访问下面地址:
http://192.168.88.128:8848/nacos/,将192.168.88.128
替换为自己的虚拟机IP地址。 首次访问会跳转到登录页,账号密码都是nacos
3. 服务注册【服务提供者在注册中心注册】
(1)添加依赖
在item-service
的pom.xml
中添加依赖:
XML
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
(2)配置Nacos地址
在item-service
的application.yml
中添加nacos地址配置:
bash
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
(3)启动服务实例
添加依赖并配置完Nacos地址后,可以启动ItemApplication的项目
访问nacos控制台,可以发现服务注册成功:
点击详情查看:
再添加一个实例:
确定,然后启动该8083的springboot项目 ,然后访问nacos控制台:
4. 服务发现【服务消费者从注册中心订阅服务】
(1)添加依赖
在cart-service
的pom.xml
中添加依赖:
XML
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
(2)配置Nacos地址
在cart-service
的application.yml
中添加nacos地址配置:
bash
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
(3)发现并调用服务
服务调用者cart-service
就可以去订阅item-service
服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:随机,轮询,IP的hash,最近最少访问等。这里可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口。但现在不需要了,我们通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:
Swagger测试:
当停止了8081的服务,注册中心可以感受到该服务的宕机,从实例列表剔除;
当8081又重新启动服务,注册中心又监测了该端口的启动,又增加到了实例列表。
二、OpenFeign
1. OpenFeign简介
OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送。
利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了,要写一堆复杂的代码,可读性变差。
以上是原始代码,代码很长!
其实远程调用的关键点就在于四个:
-
请求方式
-
请求路径
-
请求参数
-
返回值类型
所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。
2. 用法
(1)引入依赖【服务消费者一方】
在cart-service
服务的pom.xml中引入OpenFeign
的依赖和loadBalancer
依赖:
XML
<!--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>
(2)启用OpenFeign
在cart-service
的CartApplication
启动类上添加注解@EnableFeignClients,启动OpenFeign功能:
(3)编写OpenFeign客户端
在cart-service
中,定义一个新的接口,编写Feign客户端:
XML
@FeignClient("item-service") //从注册中心获取微服务的实例
public interface ItemClient {
//该接口不用手动实现,已经封装好了实现的方法
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
-
@FeignClient("item-service")
:声明服务名称 -
@GetMapping
:声明请求方式 -
@GetMapping("/items")
:声明请求路径 -
@RequestParam("ids") Collection<Long> ids
:声明请求参数 -
List<ItemDTO>
:返回值类型
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items
发送一个GET
请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>
。
我们只需要直接调用这个方法,即可实现远程调用了。
(4)使用FeignClient
在cart-service
的com.hmall.cart.service.impl.CartServiceImpl
中改造代码,直接调用ItemClient
的方法:
仅需一行List<ItemDTO> items = itemClient.queryItemByIds(itemIds);即可查询,当然要先把itemClient从容器中注入。
java
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
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());
}
}
3. OpenFeign基于连接池优化
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
-
HttpURLConnection:默认实现 ,不支持连接池
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
(1)引入依赖
在cart-service
的pom.xml
中引入依赖:
XML
<!--OK http 的依赖,支持连接池发起HTTP请求 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
(2)配置中开启连接池
在cart-service
的application.yml
配置文件中开启Feign的连接池功能:
bash
feign:
okhttp:
enabled: true # 开启OKHttp功能
重启服务,连接池就生效了。调试后,可以看见使用了OKHTTP方式发送请求:
三、工程项目结构最佳实践
将来我们要把与下单有关的业务抽取为一个独立微服务:trade-service
,而它也需要远程调用item-service
中的根据id批量查询商品功能。这个需求与cart-service
中是一样的。
因此,我们就需要在trade-service
中再次定义ItemClient
接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?
1. 思路分析
避免重复编码的办法就是抽取。不过这里有两种抽取思路:
-
思路1:抽取到微服务之外的公共module
-
思路2:每个微服务自己抽取一个module
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
由于item-service已经创建好,无法继续拆分,因此这里我们采用方案1.
2. 抽取Feign客户端
在hmall
下定义一个新的module,命名为hm-api
hm-api的依赖如下:
然后把ItemDTO和ItemClient都拷贝过来,最终结构如下:
现在,任何微服务要调用item-service
中的接口,只需要引入hm-api
模块依赖即可,无需自己编写Feign客户端了。
接下来,我们在cart-service
的pom.xml
中引入hm-api
模块:
XML
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
3. 扫描包
cart-service的启动类定义在com.hmall.cart
包下,扫描不到ItemClient,
在cart-service的启动类上添加声明即可,两种方式:
添加注解后,再次测试:
四、OpenFeign的日志输出
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
1. 定义日志级别
在hm-api模块下新建一个配置类,定义Feign的日志级别:
2. 配置
接下来,要让日志级别生效,还需要配置这个类。可以在启动类的@EnableFeignClients
中配置,针对所有FeignClient
生效。
java
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)