Nacos配置管理与动态路由

Nacos配置管理

nacos 可以对多个服务的配置文件进行统一的公共管理,主要有两种方式:1)公共管理 2)热更新。

1.公共管理

当我们有很多个微服务的时候,一些微服务中有很多相同的配置,每次都要去配置这些重复的配置,比较麻烦,我们可以利用nacos将这些公共的配置抽离出来。首先我们要明确要提取的公共的配置的内容,然后到 nacos 注册中心去新建配置,将我们需要添加的配置导入进去,并设置一个 ID,这个 ID 一般以 shared-具体抽取的公共配置的内容.yaml 命名。然后通过 ${} 占位符动态地读取 Spring Boot 中的配置。

如下图:

在这里插入图片描述

这是第一步,完成之后,引入 nacos 的配置依赖(如 spring-cloud-starter-alibaba-nacos-config

        <!--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>

然后去 resource 资源文件中,然后创建 bootstrap.yaml 文件,在其中定义服务的名称、nacos 的地址、命名空间(namespace)、组(group)以及我们刚才设置的公共配置的 ID(通过 shared-configsextension-configs 配置项)等相关信息。

bootstrap.yaml

spring:
  application:
    name: cart-service #服务名称
  profiles:
    active: dev
  #配置nacos
  cloud:
    nacos:
      server-addr: 192.168.153.136:8848
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置

最后需要去我们的 application 配置文件中设置我们定义的第一步的占位符中的内容,然后在 dev 或者 local 文件中设置具体的值。(不要忘记删除我们刚才配置的公共部分的内容)

application.yaml

server:
  port: 8082
feign:
  okhttp:
    enabled: true
hm:
  swagger:
    title: 购物车服务接口文档
    package: com.hmall.cart.controller
  db:
    database: hm-cart
    host: ${hm.db.host}

2.热更新

有一些需求我们不想在中断程序运行、重新编译、打包、发布的情况下进行修改,这时候就需要用到热更新。具体如何实现热更新呢?如下:

我们先在 nacos 中创建配置,然后 ID 设置成 服务名-spring-profile-dev.yaml(后缀一般是 .yaml.properties),其中 spring-profile-dev 可以不设置,直接使用 服务名.yaml,这样的话对所有的 local 或者 dev 环境都有效(前提是 spring.profiles.active 未指定或匹配)。

在这里插入图片描述

然后我们配置需要热更新的数据,在 nacos 中配置好之后,可以创建一个 properties 类,使用 @Component 注解以及 @ConfigurationProperties(prefix = "") 读取我们刚才在 nacos 中设置的热更新的数据。为了支持配置变更后的自动刷新,还需加上 @RefreshScope 注解(如果是使用 @Value 方式读取,则必须加;若使用 @ConfigurationProperties,在较新版本中通常已支持自动刷新)。这样在程序运行的时候,我们就可以去修改 nacos 配置,达到不中断程序而动态修改配置的效果。

在这里插入图片描述

3.程序如何运行

首先服务运行的时候,会通过 bootstrap.yaml 优先加载 nacos 中的配置(包括公共配置和应用专属配置),然后 Spring Cloud 会将这些配置注入到 Spring Boot 的 Environment 中,形成应用上下文的一部分。接着 Spring Boot 会读取本地 application.yaml 的数据,并将其合并到应用上下文中(nacos 配置优先级通常高于本地配置,具体取决于配置方式)。最终合并后的配置生效,程序正常运行。

在这里插入图片描述

扩展:动态路由

网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用上面的热更新来实现动态路由

什么是动态路由?

我们的微服务项目启动的时候,会自动读取bootstrap.yaml的文件,然后读取nacos中的配置然后 Spring Cloud 会将这些配置注入到 Spring Boot 的 Environment 中,形成应用上下文的一部分。接着 Spring Boot 会读取本地 application.yaml 的数据,并将其合并到应用上下文中(nacos 配置优先级通常高于本地配置,具体取决于配置方式)。最终合并后的配置生效,程序正常运行。

如果我们在项目部署上线后,需要对一个服务进行下线或者开发了一个新的服务需要上线,这个时候我们就需要先去修改网关服务对应的配置文件的内容,重写设置网关路由,然后再运行。既重启网关,因为网关是所有客户端发送请求的中心,所有这样会导致我们整个系统业务无法正常运行。所以,我们可以采用动态路由的方式,在不重启网关的情况下,对路由进行动态更新

因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中.

这里主要涉及两个点:

1.监听nacos中路由配置的变更

https://nacos.io/zh-cn/docs/sdk.html 在官方SDK中,我们可以看到详细的用法,如何去监听路由配置变更。 如下为官方SDK中 监听nacos路由配置变更示例代码.

String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
//获取nacos的配置对象 连接到nacos
ConfigService configService = NacosFactory.createConfigService(properties);
//获取dataID,group 的配置相关的信息(一个服务的配置信息)
String content = configService.getConfig(dataId, group, 5000);
//输出
System.out.println(content);
//给当前服务的配置文件 添加监听器 监听文件变更清空
configService.addListener(dataId, group, new Listener() {
    //当文件发生变更时 执行的逻辑操作
	@Override
	public void receiveConfigInfo(String configInfo) {
		System.out.println("recieve1:" + configInfo);
	}
    //返回一个线程池对象 可以指定一个线程池去异步处理这个变更 这里返回null 标识采用nacos默认线程池或者单线程
	@Override
	public Executor getExecutor() {
		return null;
	}
});

// 测试让主线程不退出,因为订阅配置是守护线程,主线程退出守护线程就会退出。 正式代码中无需下面代码
while (true) {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

可以看到在官方SDK中,我们监听nacos中配置变更的操作主要有三步。

  • 连接上nacos: NacosFactory.createConfigService(properties);
  • 获取我们需要监听的配置信息 :getConfig方法
  • 设置监听器:configService.addListener(dataId, group, new Listenner{…})

因为我们的项目是基于spring-cloud-starter,所以项目启动的时候,我们的服务就会自动连接上nacos.那具体如何连接上nacos的呢,这里需要去看下下面的源码。

NacosConfigAutoConfiguration自动配置类中,里面有大量bean对象,在项目启动的时候,会加载到IOC容器中。

观察如下代码,我们发现注入了一个NacosConfigManager对象,这个对象传递了一个NacosConfigProperties类型的参数,只通过它的名字,我觉得大家应该知道它是什么内容。这个对象里面存放了nacos配置的相关信息,也就是官方SDK示例中的构造Properties这一步骤。

在这里插入图片描述
在这里插入图片描述

进入NacosConfigManager对象中,我们可以发现该对象在构造方法中调用了createConfigService方法.

    public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
        this.nacosConfigProperties = nacosConfigProperties;
        createConfigService(nacosConfigProperties);
    }

下面我们就去看下createConfigService方法中的内容,该静态方法,会帮助我们创建一个ConfigService并且返回 也就是帮助我们连接上了Nacos.并且返回一个ConfigService类型的数据.

在这里插入图片描述

在实际使用的时候,我们就可以注入NacosConfigManager该对象,然后通过其getConfigService方法获取到该配置对象。

在ConfigService接口中,存在一些列方法供我们使用,添加监听、移除监听等等。

在这里插入图片描述

所以我们只需要注入NacosManagerConfig对象然后调用其getConfigService方法即可实现添加监听这一操作。

我们现在可以监听到配置文件的变更,并且通过new Listener()执行监听后的操作,那我们该如何将最新变更的配置信息推送到系统的路由表中呢?

2.将最新的路由更新到我们系统的路由表中

在spring-cloud-gateway中给我们提供了一个RouteDefinitionWriter 接口,其中有两个方法savedelete帮助我们将变更的配置信息,保存到内存中的路由表内(一个Map)中,或者删除.
在这里插入图片描述

//save方法 将变更的配置信息 刷新到内存中的路由表中(Map)    
public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap((r) -> {
            if (ObjectUtils.isEmpty(r.getId())) {
                return Mono.error(new IllegalArgumentException("id may not be empty"));
            } else {
                this.routes.put(r.getId(), r);
                return Mono.empty();
            }
        });
    }
//将某个路由信息 在路由表中进行删除    
public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap((id) -> {
            if (this.routes.containsKey(id)) {
                this.routes.remove(id);
                return Mono.empty();
            } else {
                return Mono.defer(() -> {
                    return Mono.error(new NotFoundException("RouteDefinition not found: " + routeId));
                });
            }
        });
    }

所以,我们可以在代码中,注入RouteDefinitionWriter该Bean对象,然后通过调用上面两个方法,将变更的路由信息,推送到内存中的路由表中,实现动态路由。

如下是一个代码实例

package com.heima.gateway.routes;

import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * 动态路由加载器
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class DynamicRouteLoader {

    private final RouteDefinitionWriter writer;

    private final NacosConfigManager nacosConfigManager;
    //配置 id
    private final String dataId = "gateway-routes.json";
    //群组
    private final String group = "DEFAULT_GROUP";
    //超时等待时间
    private final long timeoutMS = 3000;
    //路由表 dataId
    private final Set<String> routeIds = new HashSet<String>();

    //Bean 初始化之后执行
    @PostConstruct
    public void initRouteConfig() throws NacosException {
        //首次拉取配置并注册监听器
        String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, timeoutMS, new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String configInfo) {
                //监听到路由表变更 更新路由表
                updateRouteConfig(configInfo);
            }
        });
        //首次拉取路由表配置 更新路由表
        updateRouteConfig(configInfo);
    }

    public void updateRouteConfig(String configInfo){
        log.info("检测到网关路由发生变更:{}",configInfo);
        //1.删除路由表旧数据
        for (String routeId : routeIds) {
            //write拿到mono后并不会立即去执行,需要订阅,即订阅这个容器的消息,有了消息后再去处理,既响应式编程。
            writer.delete(Mono.just(routeId)).subscribe();
        }
        routeIds.clear();
        //2.获取路由表信息 对象 这里使用了糊涂包下的工具 JSON转List
        List<RouteDefinition> routeList = JSONUtil.toList(configInfo, RouteDefinition.class);
        for (RouteDefinition routeDefinition : routeList) {
            //保存原来路由id 方便删除
            String id = routeDefinition.getId();
            routeIds.add(id);
            //推送变更到nacos
    writer.save(Mono.just(routeDefinition)).subscribe();
        }

    }
}

Mono解释:

“Spring Cloud Gateway 基于响应式编程模型,RouteDefinitionWriterdeletesave 方法都接收 Mono 类型参数,这是为了符合 Reactor 的异步非阻塞规范
Mono.just(id) 是将一个普通字符串包装成响应式流,而 .subscribe() 是触发这个流执行的必要操作。
如果不调用 subscribe(),整个操作只是被声明而不会真正执行,会导致动态路由更新失效。”

3.小结

动态路由实现原理(核心两步)

第一步:监听 Nacos 路由配置变更

  • 利用 NacosConfigManager 获取 ConfigService

  • 调用

    getConfigAndSignListener(dataId, group, timeout, listener)
    
    • 首次拉取配置;
    • 注册监听器,配置变更时触发 receiveConfigInfo() 回调。

第二步:手动更新内存路由表

  • 注入 RouteDefinitionWriter(Spring Cloud Gateway 提供的标准接口)。
  • 在监听回调中:
    1. 删除旧路由:遍历已记录的 routeIds,调用 writer.delete(Mono.just(id)).subscribe()
    2. 加载新路由:将 Nacos 中的 JSON 配置解析为 List<RouteDefinition>,逐个调用 writer.save().subscribe()
  • 关键细节
    • 必须调用 .subscribe() 触发响应式操作(否则无效果)。
    • 使用 Set<String> routeIds 记录当前路由 ID,便于清理。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值