目录

前言

1. 配置共享

1.1. 添加共享配置

1.2. 拉取共享配置

1.2.1. 引入依赖

1.2.2. 新建 bootstrap.yaml

1.2.3. 修改 application.yaml

2. 配置热更新

2.1. 添加配置到 Nacos

2.2. 配置热更新

3. 动态路由

3.1. 监听 Nacos 配置变更

3.2. 更新路由

3.3. 实现动态路由

总结


本文介绍了如何利用Nacos实现微服务配置管理和动态路由功能。主要内容包括:

  1. 配置共享:将jdbc、日志、swagger等公共配置抽取到Nacos统一管理,通过bootstrap.yaml加载配置,支持参数化配置和默认值设置。

  2. 配置热更新:使用@ConfigurationProperties注解读取配置,当Nacos中的配置变更时,会自动推送到微服务并立即生效,无需重启服务。

  3. 动态路由:通过监听Nacos配置变更事件,使用RouteDefinitionWriter动态更新网关路由表,实现无需重启网关即可修改路由配置。

这些功能解决了微服务架构中的配置集中管理、配置热更新和路由动态调整等问题,完善了微服务技术栈。通过Nacos的配置管理能力,大大提升了微服务系统的可维护性和灵活性。

前言

到目前为止我们已经解决了微服务相关的几个问题:

  • 微服务远程调用

  • 微服务注册、发现

  • 微服务请求路由、负载均衡

  • 微服务登录用户信息传递

不过,现在依然还有几个问题需要解决:

  • 网关路由在配置文件中写死了,如果变更必须重启微服务

  • 某些业务配置在配置文件中写死了,每次修改都要重启服务

  • 每个微服务都有很多重复的配置,维护成本高

这些问题都可以通过统一的配置管理服务解决。而 Nacos 不仅仅具备注册中心功能,也具备配置管理的功能:

微服务共享的配置可以统一交给 Nacos 保存和管理,在 Nacos 控制台修改配置后,Nacos 会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。

网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。


1. 配置共享

我们可以把微服务共享的配置抽取到 Nacos 中统一管理,这样就不需要每个微服务都重复配置了。分为两步:

  • 在 Nacos 中添加共享配置

  • 微服务拉取配置

1.1. 添加共享配置

以 cart-service 为例,我们看看有哪些配置是重复的,可以抽取的:

首先是 jdbc 相关配置:

spring:
  datasource:
    url: jdbc:mysql://${sp.db.host}:3307/sp-pay?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${sp.db.pw}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto

然后是日志配置:

logging:
  level:
    com.example: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"

然后是 swagger 以及 OpenFeign 的配置:

knife4j:
  enable: true
  openapi:
    title: 商城支付管理接口文档
    description: "商城接口文档"
    email: dev@example.com
    concat: 开发者
    url: https://www.example.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.sp.pay.controller

我们在 nacos 控制台分别添加这些配置。

首先是 jdbc 相关配置,在配置管理 -> 配置列表中点击 + 新建一个配置:

在弹出的表单中填写信息:

其中详细的配置如下:

spring:
  datasource:
    url: jdbc:mysql://${demo.db.host:localhost}:${demo.db.port:3306}/${demo.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${demo.db.un:root}
    password: ${demo.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

注意这里的 jdbc 的相关参数并没有写死,例如:

  • 数据库 ip:通过 ${demo.db.host:localhost} 配置了默认值为 localhost,同时允许通过 ${demo.db.host} 来覆盖默认值

  • 数据库端口:通过 ${demo.db.port:3306} 配置了默认值为 3306,同时允许通过 ${demo.db.port} 来覆盖默认值

  • 数据库 database:可以通过 ${demo.db.database} 来设定,无默认值

然后是统一的日志配置,命名为 shared-log.yaml,配置内容如下:

logging:
  level:
    com.example: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"

然后是统一的 swagger 配置,命名为 shared-swagger.yaml,配置内容如下:

knife4j:
  enable: true
  openapi:
    title: ${demo.swagger.title:接口文档}
    description: ${demo.swagger.description:接口文档}
    email: ${demo.swagger.email:dev@example.com}
    concat: ${demo.swagger.concat:开发者}
    url: https://www.example.com
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - ${demo.swagger.package}

注意,这里的 swagger 相关配置我们没有写死,例如:

  • title:接口文档标题,我们用了 ${demo.swagger.title} 来代替,将来可以有用户手动指定

  • email:联系人邮箱,我们用了 ${demo.swagger.email:dev@example.com},默认值是 dev@example.com,同时允许用户利用 ${demo.swagger.email} 来覆盖。

1.2. 拉取共享配置

接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的 application.yaml 配置合并,完成项目上下文的初始化。

不过,需要注意的是,读取 Nacos 配置是 Spring Cloud 上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化 Spring Boot 上下文,去读取 application.yaml。

也就是说引导阶段,application.yaml 文件尚未读取,根本不知道 nacos 地址,该如何去加载 nacos 中的配置文件呢?

Spring Cloud 在初始化上下文的时候会先读取一个名为 bootstrap.yaml(或者 bootstrap.properties)的文件,如果我们将 nacos 地址配置到 bootstrap.yaml 中,那么在项目引导阶段就可以读取 nacos 中的配置了。

因此,微服务整合 Nacos 配置管理的步骤如下:

1.2.1. 引入依赖

在 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>
1.2.2. 新建 bootstrap.yaml

在 cart-service 中的 resources 目录新建一个 bootstrap.yaml 文件:

内容如下:

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

由于一些配置挪到了 bootstrap.yaml,因此 application.yaml 需要修改为:

server:
  port: 8082
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
demo:
  swagger:
    title: 购物车服务接口文档
    package: com.example.cart.controller
  db:
    database: cart

重启服务,发现所有配置都生效了。


2. 配置热更新

有很多的业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是 10,对应代码如下:

现在这里购物车是写死的固定值,我们应该将其配置在配置文件中,方便后期修改。

但现在的问题是,即便写在配置文件中,修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启,直接生效呢?

这就要用到 Nacos 的配置热更新能力了,分为两步:

  • 在 Nacos 中添加配置

  • 在微服务读取配置

2.1. 添加配置到 Nacos

首先,我们在 nacos 中添加一个配置文件,将购物车的上限数量添加到配置中:

注意文件的 dataId 格式:

[服务名]-[spring.active.profile].[后缀名]

文件名称由三部分组成:

  • 服务名:我们是购物车服务,所以是 cart-service

  • spring.active.profile:就是 spring boot 中的 spring.active.profile,可以省略,则所有 profile 共享该配置

  • 后缀名:例如 yaml

这里我们直接使用 cart-service.yaml 这个名称,则不管是 dev 还是 local 环境都可以共享该配置。

配置内容如下:

demo:
  cart:
    maxAmount: 1 # 购物车商品数量上限

提交配置,在控制台能看到新添加的配置

2.2. 配置热更新

接着,我们在微服务中读取配置,实现配置热更新。

在 cart-service 中新建一个属性读取类:

代码如下:

package com.example.cart.config;
​
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
​
@Data
@Component
@ConfigurationProperties(prefix = "demo.cart")
public class CartProperties {
    private Integer maxAmount;
}

接着,在业务中使用该属性加载类:

测试,向购物车中添加多个商品:

我们在 nacos 控制台,将购物车上限配置为 5:

无需重启,再次测试购物车功能:

加入成功!

无需重启服务,配置热更新就生效了!


3. 动态路由

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

因此,我们必须监听 Nacos 的配置变更,然后手动把最新的路由更新到路由表中。这里有两个难点:

  • 如何监听 Nacos 配置变更?

  • 如何把路由信息更新到路由表?

3.1. 监听 Nacos 配置变更

在 Nacos 官网中给出了手动监听 Nacos 配置变更的 SDK:

Java SDK

如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。

public void addListener(String dataId, String group, Listener listener)

请求参数说明:

参数名参数类型描述
dataIdstring配置 ID,保证全局唯一性,只允许英文字符和 4 种特殊字符("."、":"、"-"、"_")。不超过 256 字节。
groupstring配置分组,一般是默认的 DEFAULT_GROUP。
listenerListener监听器,配置变更进入监听器的回调函数。

示例代码:

String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.创建ConfigService,连接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.读取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {
    @Override
    public void receiveConfigInfo(String configInfo) {
        // 配置变更的通知处理
        System.out.println("recieve1:" + configInfo);
    }
    @Override
    public Executor getExecutor() {
        return null;
    }
});

这里核心的步骤有 2 步:

  • 创建 ConfigService,目的是连接到 Nacos

  • 添加配置监听器,编写配置变更的通知处理逻辑

由于我们采用了 spring-cloud-starter-alibaba-nacos-config 自动装配,因此 ConfigService 已经在 com.alibaba.cloud.nacos.NacosConfigAutoConfiguration 中自动创建好了:

NacosConfigManager 中是负责管理 Nacos 的 ConfigService 的,具体代码如下:

因此,只要我们拿到 NacosConfigManager 就等于拿到了 ConfigService,第一步就实现了。

第二步,编写监听器。虽然官方提供的 SDK 是 ConfigService 中的 addListener,不过项目第一次启动时不仅仅需要添加监听器,也需要读取配置,因此建议使用的 API 是这个:

String getConfigAndSignListener(
    String dataId, // 配置文件id
    String group, // 配置组,走默认
    long timeoutMs, // 读取配置的超时时间
    Listener listener // 监听器
) throws NacosException;

既可以配置监听器,并且会根据 dataId 和 group 读取配置并返回。我们就可以在项目启动时先更新一次路由,后续随着配置变更通知到监听器,完成路由更新。

3.2. 更新路由

更新路由要用到 org.springframework.cloud.gateway.route.RouteDefinitionWriter 这个接口:

package org.springframework.cloud.gateway.route;
​
import reactor.core.publisher.Mono;
​
/**
 * @author Spencer Gibb
 */
public interface RouteDefinitionWriter {
    /**
     * 添加路由
     */
    Mono<Void> save(Mono<RouteDefinition> route);
​
    /**
     * 删除路由
     */
    Mono<Void> delete(Mono<String> routeId);
}

我们可以通过注入这个接口,然后调用 save 方法添加路由,调用 delete 方法删除路由。

3.3. 实现动态路由

接下来,我们在网关中实现动态路由功能。

首先,在 gateway 的 pom.xml 中添加依赖:

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

然后在网关 gateway 的 resources 目录创建 bootstrap.yaml 文件,内容如下:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost
      config:
        file-extension: yaml
        shared-configs:
          - dataId: shared-log.yaml # 共享日志配置

接着,修改 gateway 的 resources 目录下的 application.yml,把之前的路由移除,最终内容如下:

server:
  port: 8080 # 端口
demo:
  jwt:
    location: classpath:demo.jks # 秘钥地址
    alias: demo # 秘钥别名
    password: demo123 # 秘钥文件密码
    tokenTTL: 30m # 登录有效期
  auth:
    excludePaths: # 无需登录校验的路径
      - /search/**
      - /users/login
      - /items/**

然后,在 gateway 中定义配置监听器:

其代码如下:

package com.example.gateway.route;
​
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 com.example.common.utils.CollUtils;
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;
​
@Slf4j
@Component
@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";
    // 保存更新过的路由id
    private final Set<String> routeIds = new HashSet<>();
​
    @PostConstruct
    public void initRouteConfigListener() throws NacosException {
        // 1.注册监听器并首次拉取配置
        String configInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, 5000, new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }
​
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        updateConfigInfo(configInfo);
                    }
                });
        // 2.首次启动时,更新一次配置
        updateConfigInfo(configInfo);
    }
​
    private void updateConfigInfo(String configInfo) {
        log.debug("监听到路由配置变更,{}", configInfo);
        // 1.反序列化
        List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
        // 2.更新前先清空旧路由
        // 2.1.清除旧路由
        for (String routeId : routeIds) {
            writer.delete(Mono.just(routeId)).subscribe();
        }
        routeIds.clear();
        // 2.2.判断是否有新的路由要更新
        if (CollUtils.isEmpty(routeDefinitions)) {
            // 无新路由配置,直接结束
            return;
        }
        // 3.更新路由
        routeDefinitions.forEach(routeDefinition -> {
            // 3.1.更新路由
            writer.save(Mono.just(routeDefinition)).subscribe();
            // 3.2.记录路由id,方便将来删除
            routeIds.add(routeDefinition.getId());
        });
    }
}

重启网关,任意访问一个接口,比如 http://localhost:8080/search/list?pageNo=1&pageSize=1

发现是 404,无法访问。

接下来,我们直接在 Nacos 控制台添加路由,路由文件名为 gateway-routes.json,类型为 json:

配置内容如下:

[
    {
        "id": "item",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
        }],
        "filters": [],
        "uri": "lb://item-service"
    },
    {
        "id": "cart",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/carts/**"}
        }],
        "filters": [],
        "uri": "lb://cart-service"
    },
    {
        "id": "user",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
        }],
        "filters": [],
        "uri": "lb://user-service"
    },
    {
        "id": "trade",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/orders/**"}
        }],
        "filters": [],
        "uri": "lb://trade-service"
    },
    {
        "id": "pay",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/pay-orders/**"}
        }],
        "filters": [],
        "uri": "lb://pay-service"
    }
]

无需重启网关,稍等几秒钟后,再次访问刚才的地址:

网关路由成功了!


总结

配置共享

  • 将微服务共享的配置抽取到 Nacos 中统一管理

  • 通过 bootstrap.yaml 配置 Nacos 地址和共享配置

  • 支持配置参数化,允许在本地配置中覆盖默认值

配置热更新

  • 使用 @ConfigurationProperties 注解读取配置

  • Nacos 配置变更后会自动推送到微服务

  • 无需重启服务即可生效

动态路由

  • 监听 Nacos 配置变更事件

  • 使用 RouteDefinitionWriter 更新路由表

  • 无需重启网关即可修改路由配置

Spring Cloud 微服务完整技术栈

  1. 服务拆分:将单体应用拆分为多个独立微服务

  2. 服务注册与发现:使用 Nacos 实现服务治理

  3. 远程调用:使用 OpenFeign 实现跨服务调用

  4. 网关路由:使用 Spring Cloud Gateway 实现请求路由

  5. 网关鉴权:实现统一的登录校验和用户信息传递

  6. 配置管理:使用 Nacos 实现统一配置管理和热更新

通过以上技术,我们构建了一个完整的微服务架构,解决了微服务开发中的各种问题。

更多推荐