SpringCloud Gateway 基于 Nacos 实现动态路由

文章目录

  • 前言
    • [SpringCloud Gateway 基于 Nacos 实现动态路由](#SpringCloud Gateway 基于 Nacos 实现动态路由)
      • [1. 作用](#1. 作用)
      • [2. 使用场景](#2. 使用场景)
      • [3. 测试效果](#3. 测试效果)
      • [4. 关键代码如下](#4. 关键代码如下)

前言

  如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^ _ ^。

  而且听说点赞的人每天的运气都不会太差,实在白嫖的话,那欢迎常来啊!!!


SpringCloud Gateway 基于 Nacos 实现动态路由

1. 作用

将网关路由配置外部化托管在 Nacos 配置中心,网关监听 Nacos 配置变更,自动重新加载路由并刷新 Gateway 内置路由缓存,无需重启网关即可实现路由动态生效。

2. 使用场景

  1. 网关集群部署,需要统一管控路由 多台 Gateway 实例,要求修改路由所有节点同步生效。原生内存仓库只单实例生效,不满足集群需求。
  2. 频繁变更转发规则,不能重启网关 业务迭代、灰度发布、上下游服务扩缩容,需要随时新增 / 修改 / 下线路由,网关重启成本高,会造成请求中断。
  3. 路由配置统一平台管理,运维直接操作 Nacos 控制台维护路由 不需要开发独立的路由管理后台。运维 / 开发直接在 Nacos 控制台编辑路由 JSON,实现路由上下线。
  4. 项目 POC 验证、中小规模业务网关 想快速落地动态路由,不想实现 RouteDefinitionRepository 的 save/delete 读写逻辑,降低开发成本。
  5. 静态路由 + 动态路由共存 yml 保留基础固定路由(如网关自身健康检查),业务动态路由由 Nacos 提供,CompositeRouteDefinitionLocator合并两套数据源。

3. 测试效果

下面是测试的配置。

启动网关:

修改该配置的一个值。

把上述图片红框处,从8089改到8088,然后看一下服务日志的效果。

可以看到服务器日志打印信息,已经监听到nacos配置表更,刷新了配置,测试通过。

4. 关键代码如下

springboot版本: 2.6.15

springCloud版本:2021.0.9

Spring Cloud Alibaba版本:2021.0.6.0

yml nacos集成配置。

bash 复制代码
spring:
  application:
    name: yzy-gateway-out
  cloud:
    nacos:
      config:
        file-extension: json
        server-addr: 127.0.0.1:8848
        username: nacos
        password: nacos
        namespace: public
        group: DEFAULT_GROUP
        data-id: out-gateway-routes
        refresh-enabled: true
        shared-configs:
          - data-id: out-service
            group: DEFAULT_GROUP
            file-extension: json
            refresh-enabled: true
      discovery:
        server-addr: 127.0.0.1:8848
        username: nacos
        password: nacos
        namespace: public
        group: DEFAULT_GROUP
    retry: # 重试配置
      max-attempts: 3
      delay: 1000 #重试间隔ms
    timeout: 3000 # 超时配置
    gateway:
      discovery:
        locator:
          enabled: true
    loadbalancer:
      cache:
        enabled: true
        ttl: 5s

启动类:

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
* yzy-gateway 网关服务启动类
* Spring Cloud Gateway + Nacos(注册中心/配置中心)
* 路由 /api/b/** → yzy-b-demo
*
* @author yzy
* @version 1.0
*/
@EnableDiscoveryClient
@SpringBootApplication
public class YzyGatewayOutApplication {


    public static void main(String[] args) {
        SpringApplication.run(YzyGatewayOutApplication.class, args);
    }
}

网关配置:

java 复制代码
import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author yangzhenyu
* @version 1.0
* @date 2026/8/15 10:32
*/
@Configuration
public class GatewayConfig {

    @Bean
    public RouteDefinitionRepository routeDefinitionRepository(){
        //  内存版路由仓库,路由全部存在 JVM 内存
        return new InMemoryRouteDefinitionRepository();
    }
}

Nacos动态路由服务:

java 复制代码
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.fastjson2.JSON;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;


/**
* Nacos动态路由服务
* <p>
* 监听Nacos配置中心路由配置变更,实时更新网关路由。
* 使用Spring Cloud Alibaba注入的NacosConfigManager获取ConfigService,
* 确保namespace认证等配置与Spring容器一致。
*
* @author yzy
* @version 1.1
*/
@Slf4j
@Component
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {


    @Value("${spring.cloud.nacos.config.data-id}")
    private String dataId;
    @Value("${spring.cloud.nacos.config.group}")
    private String group;


    private final NacosConfigManager nacosConfigManager;
    private final RouteDefinitionWriter routeDefinitionWriter;
    private final RouteDefinitionRepository routeDefinitionRepository;
    private ApplicationEventPublisher publisher;


    public NacosDynamicRouteService(NacosConfigManager nacosConfigManager,
                                    RouteDefinitionWriter routeDefinitionWriter,
                                    RouteDefinitionRepository routeDefinitionRepository) {
        this.nacosConfigManager = nacosConfigManager;
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeDefinitionRepository = routeDefinitionRepository;
    }


    @EventListener(ApplicationReadyEvent.class)
    public void init() {
        log.info("=== 初始化Nacos动态路由服务 ===");
        log.info("dataId={}, group={}", dataId, group);
        try {
            ConfigService configService = nacosConfigManager.getConfigService();
            log.info("ConfigService状态: {}", configService);

            // 首次拉取配置
            String configInfo = configService.getConfig(dataId, group, 5000);
            log.info("首次拉取路由配置: {}", configInfo);
            if (configInfo != null && !configInfo.trim().isEmpty()) {
                updateRoutes(configInfo);
                publisher.publishEvent(new RefreshRoutesEvent(this));
                log.info("首次路由加载完成,已发布RefreshRoutesEvent");
            } else {
                log.warn("Nacos中未找到路由配置,dataId={}, group={}", dataId, group);
            }

            // 注册配置变更监听器
            configService.addListener(dataId, group, new Listener() {
                @Override
                public Executor getExecutor() {
                    return Runnable::run;
                }

                @Override
                public void receiveConfigInfo(String configInfo) {
                    log.info("=== 收到Nacos配置变更通知 ===");
                    log.info("新配置内容: {}", configInfo);
                    if (configInfo != null && !configInfo.trim().isEmpty()) {
                        updateRoutes(configInfo);
                        publisher.publishEvent(new RefreshRoutesEvent(this));
                        log.info("路由刷新完成,已发布RefreshRoutesEvent");
                    }
                }
            });
            log.info("Nacos配置监听器注册成功,等待配置变更...");
        } catch (NacosException e) {
            log.error("Nacos配置监听初始化失败", e);
        }
    }

    /**
     * 反射清空InMemoryRouteDefinitionRepository内部全部路由
     */
    @SuppressWarnings("unchecked")
    private void clearAllRoutes() {
        if (!(routeDefinitionRepository instanceof InMemoryRouteDefinitionRepository)) {
            log.warn("当前不是内存仓库,跳过清空");
            return;
        }
        try {
            InMemoryRouteDefinitionRepository memoryRepo = (InMemoryRouteDefinitionRepository) routeDefinitionRepository;
            Field field = InMemoryRouteDefinitionRepository.class.getDeclaredField("routes");
            field.setAccessible(true);
            Map<String, RouteDefinition> routesMap = (Map<String, RouteDefinition>) field.get(memoryRepo);
            int before = routesMap.size();
            routesMap.clear();
            log.info("反射清空内存路由完成,清除{}条旧路由", before);
        } catch (Exception e) {
            log.error("反射清空路由失败", e);
        }
    }

    /**
     * 解析JSON路由配置并更新内存路由仓库
     *
     * @param configInfo Nacos返回的JSON格式路由配置
     */
    private void updateRoutes(String configInfo) {
        try {
            List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
            if (routeDefinitions == null) {
                log.warn("路由解析结果为空");
                return;
            }
            log.info("解析到{}条路由定义", routeDefinitions.size());
            for (RouteDefinition rd : routeDefinitions) {
                log.info("  路由: id={}, uri={}, predicates={}",
                        rd.getId(), rd.getUri(), rd.getPredicates());
            }

            // 反射清空全部旧路由
            clearAllRoutes();

            // 写入新路由
            for (RouteDefinition definition : routeDefinitions) {
                routeDefinitionWriter.save(Mono.just(definition))
                        .subscribe(
                                unused -> log.info("  保存路由成功: id={}", definition.getId()),
                                err -> log.error("  保存路由id={}失败", definition.getId(), err)
                        );
            }
            log.info("路由更新完成,共{}条", routeDefinitions.size());
        } catch (Exception e) {
            log.error("解析/更新路由失败", e);
        }
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }
}
相关推荐
FYKJ_20101 小时前
django个性化新闻推荐35173-计算机课程设计、毕业设计
java·spring boot·后端·python·架构·django·课程设计
计算机毕设定制辅导-无忧学长1 小时前
《校园食堂在线订餐小程序的设计与实现》
java·vue.js·spring boot·微信小程序·uniapp·校园食堂在线订餐小程序
夜之眷属1 小时前
服务启动即崩:InetAddress.getLocalHost() 抛 UnknownHostException 排查记
java·运维·后端·docker
IT研究室2 小时前
最新计算机毕业设计选题推荐-基于spring boot的食品安全信息管理系统-网站-文档指导-Java-springboot
java·spring boot·课程设计
Sayai2 小时前
MCP Server 开发实战:Spring AI 把后端日志查询暴露给 AI Agent(7 条设计原则与踩坑实录)
java·人工智能·ai agent·spring ai·mcp
大牧师2 小时前
Mybatis 框架教程
java·数据库·mybatis·数据持久化·orm
SunnyDays10112 小时前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java·开发语言·合并pdf
源码宝3 小时前
云HIS系统源码,HIS全套源码,支持监管系统对接与二次开发
java·源码·电子病历·云his系统·程序代码·全套源码