Nacos & OpenFeign快速回忆
🦙

Nacos & OpenFeign快速回忆

Created
May 3, 2024 10:31 AM
Tags

注册中心

微服务拆分并通过 Http 请求实现跨微服务的远程调用存在一些问题:
notion image
此时,每个item-service的实例其IP或端口不同:
  • cart-service 如何知道每一个实例的地址?
  • URL 地址不能写死
  • 运行过程中某个item-service实例宕机,cart-service依然在调用该怎么办?
  • 如果并发太高,item-service临时多部署了N台实例,cart-service如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念。
 
在微服务远程调用的过程中,包括两个角色:服务提供者、服务消费者,而注册中心就是协调服务提供者、服务消费者的桥梁:
notion image
有了注册中心的参与,调用的流程如下:
  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
  • 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
  • 调用者自己对实例列表负载均衡,挑选一个实例
  • 调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
  • 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
  • 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
  • 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表

Nacos 启动

Docker 上运行:
docker run -d \ --name nacos \ --env-file ./nacos/custom.env \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --restart=always \ nacos/nacos-server:v2.1.0-slim
这里 ./nacos/custom.env 是配置文件,配置了 Nacos 配套的数据库
PREFER_HOST_MODE=hostname MODE=standalone SPRING_DATASOURCE_PLATFORM=mysql MYSQL_SERVICE_HOST=... MYSQL_SERVICE_DB_NAME=nacos MYSQL_SERVICE_PORT=3306 MYSQL_SERVICE_USER=root MYSQL_SERVICE_PASSWORD=123 MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
notion image
参考:
启动完成后,访问 http://虚拟机IP地址:8848/nacos/ 跳转到登录页,账号密码都是nacos

服务注册

将微服务注册到 Nacos 步骤如下:
  • 引入依赖
  • 配置Nacos地址
  • 重启
<!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
spring: application: name: item-service # 服务名称 cloud: nacos: server-addr: 虚拟机IP地址:8848 # nacos地址
测试:
为了测试一个服务多个实例的情况,我们再配置一个item-service的部署实例,并改变端口:
notion image
notion image
notion image
notion image

服务发现

服务发现步骤如下:
  • 引入依赖
  • 配置Nacos地址
  • 发现并调用服务
前两步和服务注册一样,我们看如何发现并调用已注册的服务:
服务发现需要用到 Spring Cloud 提供的 DiscoveryClient,我们可以直接注入使用:
notion image
notion image
之前调用时我们需要写死服务提供者的IP和端口,现在我们通过 DiscoveryClient 发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用。

配置管理

Nacos 还提供了配置管理功能解决以下问题:
  • 网关路由在配置文件中写死了,如果变更必须重启微服务
  • 某些业务配置在配置文件中写死了,每次修改都要重启服务
  • 每个微服务都有很多重复的配置,维护成本高
这些问题都可以通过统一的配置管理器服务解决。而 Nacos 不仅仅具备注册中心功能,也具备配置管理的功能:
notion image
微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新
网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。
操作分为两步:
  • 在Nacos中添加共享配置
  • 微服务拉取配置
cart-service为例,我们看看有哪些配置是重复的,可以抽取的:
notion image
notion image
notion image
在nacos控制台分别添加这些配置
notion image
notion image
spring: datasource: url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: ${hm.db.un:root} password: ${hm.db.pw:123} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto
shared-jdbc.yaml
这里的jdbc的相关参数并没有写死,例如:
  • 数据库ip:通过${hm.db.host:192.168.150.101}配置了默认值为192.168.150.101,同时允许通过${hm.db.host}来覆盖默认值
  • 数据库端口:通过${hm.db.port:3306}配置了默认值为3306,同时允许通过${hm.db.port}来覆盖默认值
  • 数据库database:可以通过${hm.db.database}来设定,无默认值
按照同样的方法配置:
logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}"
shared-log.yaml
knife4j: enable: true openapi: title: ${hm.swagger.title:黑马商城接口文档} description: ${hm.swagger.description:黑马商城接口文档} email: ${hm.swagger.email:zhanghuyi@itcast.cn} concat: ${hm.swagger.concat:虎哥} url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - ${hm.swagger.package}
shared-swagger.yaml
接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。
不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml
也就是说引导阶段,application.yaml文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢?
SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。
notion image
因此,微服务整合Nacos配置管理的步骤如下:
在cart-service模块引入依赖:
<!--nacos配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--读取bootstrap文件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
新建bootstrap.yaml:
notion image
spring: application: name: cart-service # 服务名称 profiles: active: dev cloud: nacos: server-addr: 192.168.150.101 # nacos地址 config: file-extension: yaml # 文件后缀名 shared-configs: # 共享配置 - dataId: shared-jdbc.yaml # 共享mybatis配置 - dataId: shared-log.yaml # 共享日志配置 - dataId: shared-swagger.yaml # 共享日志配置
修改application.yaml,删除不必要的配置,最后重启服务

配置热更新

有很多的业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是10,对应代码如下:
notion image
现在这里购物车是写死的固定值,我们应该将其配置在配置文件中,方便后期修改。 但现在的问题是,即便写在配置文件中,修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启,直接生效呢?这就要用到Nacos的配置热更新能力了。
首先,我们在nacos中添加一个配置文件,将购物车的上限数量添加到配置中:
notion image
注意文件的dataId格式:
[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成:
  • 服务名:我们是购物车服务,所以是cart-service
  • spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置
  • 后缀名:例如yaml
这里我们直接使用cart-service.yaml这个名称,则不管是dev还是local环境都可以共享该配置。
接着,我们在微服务中读取配置,实现配置热更新。在cart-service中新建一个属性读取类:
notion image
package com.hmall.cart.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "hm.cart") public class CartProperties { private Integer maxAmount; }
接着,在业务中使用该属性加载类:
notion image
这样,当我们在 Nacos 修改配置并保存推送,无需重启服务,配置热更新就生效了

OpenFeign

为了使远程调用像本地方法调用一样简单。而这就要用到 OpenFeign 组件了。
远程调用的关键点就在于四个:
  • 请求方式
  • 请求路径
  • 请求参数
  • 返回值类型
OpenFeign 利用 SpringMVC 的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写。
导入依赖:
<!--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>
接下来在 cart-serviceCartApplication 启动类上添加注解,启动OpenFeign功能:
notion image
然后定义一个新的接口,编写Feign客户端,只需要声明接口,无需实现方法。:
import com.hmall.cart.domain.dto.ItemDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @FeignClient("item-service") public interface ItemClient { @GetMapping("/items") List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); }
有了上述信息,OpenFeign 就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带 ids 为请求参数,并自动将返回值处理为List<ItemDTO>。只需要直接调用这个方法即可实现远程调用。
notion image
服务拉取、负载均衡、发送http请求都被 OpenFeign 完成,不再需要 RestTemplate。
OpenFeign 的底层实现是怎么样的呢?
我们仅仅是定义了一个接口,却能当做对象进行调用,说明这个接口一定是被代理对象所代理
注意代理对象类型是 Proxy101
注意代理对象类型是 Proxy101
内部的具体实现是一个叫 h (handler)的 FeignInvocationHandler 代理对象,调用这个对象的 invoke 方法
notion image
接下来和我们用 DiscoveryClient 进行服务发现拉取和选择一样的流程,OpenFeign 自动完成并发送请求。
OpenFeign 底层发起 http 请求需要依赖于其它的框架。其底层支持的 http 客户端实现包括:
  • HttpURLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的 HttpURLConnection。
以 OKHttp 为例:
<!--OK http 的依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
feign: okhttp: enabled: true # 开启OKHttp功能
然后重启服务,连接池生效。

最佳实践

如果多个微服务需要彼此调用,每个微服务各自写对应的 OpenFeign Client 未免太过冗余,避免重复编码的办法就是抽取。不过这里有两种抽取思路:
notion image
  • 思路1:抽取到微服务之外的公共 module,更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
  • 思路2:每个微服务自己抽取出一个 api module,相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
采用思路1,在总体项目下定义一个新的module,命名为 hm-api,由于是可以共用的 module,因此可以将其他微服务远程调用所需的依赖统一在这个 module 里,引用这个 module 时即带有这些依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>hmall</artifactId> <groupId>com.heima</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>hm-api</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <!--open feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- load balancer--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!-- swagger 注解依赖 --> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.6</version> <scope>compile</scope> </dependency> </dependencies> </project>
notion image
记得把 DTO 和 Client都拷贝过来,如果未来有微服务需要对外暴露远程调用接口,就在这个 module 内更新,缺点是微服务在使用时可能会引入不需要的接口。
 
使用时只需在微服务里引入这个 module 的依赖👇
<!--feign模块--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-api</artifactId> <version>1.0.0</version> </dependency>
完成这些修改并启动后会发现报错:原先的 Client 无法被自动注入
notion image
这里因为 ItemClient 现在定义到了 com.hmall.api.client 包下,而 cart-service 的启动类定义在 com.hmall.cart 包下,扫描不到 ItemClient
解决办法:
  • 方式1:声明扫描包:
notion image
  • 方式2:声明要用的多个FeignClient:
notion image
如果我们想看到 OpenFeign 的调用日志,需要做一些配置
OpenFeign 只会在 Feign Client 所在包的日志级别为 DEBUG 时,才会输出日志。而且其日志级别有4级:
  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
默认的日志级别是NONE,默认我们看不到请求日志。
因此在 hm-api 模块下新建一个配置类,定义 Feign 的日志级别:
notion image
package com.hmall.api.config; import feign.Logger; import org.springframework.context.annotation.Bean; public class DefaultFeignConfig { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.FULL; } }
接下来配置日志级别在全局还是范围内生效:
  • 局部生效:在某个FeignClient中配置,只对当前FeignClient生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
  • 全局生效:在@EnableFeignClients中配置,针对所有FeignClient生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

OpenFeign 传递用户

前端发起的请求都会经过网关再到微服务,配合过滤器和拦截器功能,微服务可以轻松获取登录用户信息。但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:
notion image
下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!
由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头
微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor
public interface RequestInterceptor { /** * Called for every request. * Add data using methods on the supplied {@link RequestTemplate}. */ void apply(RequestTemplate template); }
我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。
由于FeignClient全部都是在hm-api模块,因此我们在hm-api模块的com.hmall.api.config.DefaultFeignConfig中编写这个拦截器:
notion image
@Bean public RequestInterceptor userInfoRequestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { // 获取登录用户 Long userId = UserContext.getUser(); if(userId == null) { // 如果为空则直接跳过 return; } // 如果不为空则放入请求头中,传递给下游微服务 template.header("user-info", userId.toString()); } }; }
com.hmall.api.config.DefaultFeignConfig中添加一个Bean
现在微服务之间通过 OpenFeign 调用时也会传递登录用户信息了。