SpringCloud Gateway 基于nacos实现动态路由

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
应用型负载均衡 ALB,每月750个小时 15LCU
简介: Spring Cloud Gateway作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在Spring Cloud Gateway运行时动态配置网关。

动态路由背景

在使用 Cloud Gateway 的时候,官方文档提供的方案总是基于配置文件配置的方式

  • 代码方式

@SpringBootApplication

publicclassDemogatewayApplication {

   @Bean

   publicRouteLocatorcustomRouteLocator(RouteLocatorBuilderbuilder) {

       returnbuilder.routes()

           .route("path_route", r->r.path("/get")

               .uri("http://httpbin.org"))

           .route("host_route", r->r.host("*.myhost.org")

               .uri("http://httpbin.org"))

           .route("rewrite_route", r->r.host("*.rewrite.org")

               .filters(f->f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))

               .uri("http://httpbin.org"))

           .route("hystrix_route", r->r.host("*.hystrix.org")

               .filters(f->f.hystrix(c->c.setName("slowcmd")))

               .uri("http://httpbin.org"))

           .route("hystrix_fallback_route", r->r.host("*.hystrixfallback.org")

               .filters(f->f.hystrix(c->c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))

               .uri("http://httpbin.org"))

           .route("limit_route", r->r

               .host("*.limited.org").and().path("/anything/**")

               .filters(f->f.requestRateLimiter(c->c.setRateLimiter(redisRateLimiter())))

               .uri("http://httpbin.org"))

           .build();

   }

}

  • 配置文件方式

spring:

 jmx:

   enabled: false

 cloud:

   gateway:

     default-filters:

     - PrefixPath=/httpbin

     - AddResponseHeader=X-Response-Default-Foo, Default-Bar

     routes:

     # =====================================

     # to run server

     # $ wscat --listen 9000

     # to run client

     # $ wscat --connect ws://localhost:8080/echo

     - id: websocket_test

       uri: ws://localhost:9000

       order: 9000

       predicates:

       - Path=/echo

     # =====================================

     - id: default_path_to_httpbin

       uri: ${test.uri}

       order: 10000

       predicates:

       - Path=/**

Spring Cloud Gateway作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在Spring Cloud Gateway运行时动态配置网关。

我们明确了目标需要实现动态路由,那么实现动态路由的方案有很多种,这里拿三种常见的方案来说明下:

  • mysql + api 方案实现动态路由
  • redis + api 实现动态路由
  • nacos 配置中心实现动态路由

前两种方案本质上是一种方案,只是数据存储方式不同,大体实现思路是这样,我们通过接口定义路由的增上改查接口,通过接口来修改路由信息,将修改后的数据存储到mysql或redis中,并刷新路由,达到动态更新的目的。

第三种方案相对前两种相对简单,我们使用nacos的配置中心,将路由配置放在nacos上,写个监听器监听nacos上配置的变化,将变化后的配置更新到GateWay应用的进程内。

我们下面采用第三种方案,因为网关未连接mysql,使用redis还有开发相应的api和对应的web,来配置路由信息,而我们目前没有开发web的需求,所以我们采用第三种方案。

架构设计思路

  • 封装RouteOperator类,用来删除和增加gateway进程内的路由;
  • 创建一个配置类RouteOperatorConfig,可以将RouteOperator作为bean对象注册到Spring环境中;
  • 创建nacos配置监听器,监听nacos上配置变化信息,将变更的信息更新到进程中;

整体架构图如下:

源码

代码目录结构:

app-server-a、app-server-b 为测试服务,gateway-server为网关服务。

这里我们重点看下网关服务的实现;

代码非常简单,主要配置类、监听器、路由更新机制。

RouteOperator 动态路由更新服务

动态路由更新服务主要提供网关进程内删除、添加等操作。

该类主要有路由清除clear、路由添加add、路由发布到进程publish和更新全部refreshAll方法。其中clearaddpublishprivate方法,对外提供的为refreshAll方法。

实现思路:先清空路由->添加全部路由->发布路由更新事件->完成。

具体内容我们看下面代码:

packagecom.july.gateway.service;

importcom.fasterxml.jackson.core.JsonProcessingException;

importcom.fasterxml.jackson.core.type.TypeReference;

importcom.fasterxml.jackson.databind.ObjectMapper;

importlombok.extern.slf4j.Slf4j;

importorg.springframework.cloud.gateway.event.RefreshRoutesEvent;

importorg.springframework.cloud.gateway.route.RouteDefinition;

importorg.springframework.cloud.gateway.route.RouteDefinitionWriter;

importorg.springframework.context.ApplicationEventPublisher;

importorg.springframework.util.StringUtils;

importreactor.core.publisher.Mono;

importjava.util.ArrayList;

importjava.util.List;

/**

* 动态路由更新服务

*

* @author wanghongjie

*/

@Slf4j

publicclassRouteOperator {

   privateObjectMapperobjectMapper;

   privateRouteDefinitionWriterrouteDefinitionWriter;

   privateApplicationEventPublisherapplicationEventPublisher;

   privatestaticfinalList<String>routeList=newArrayList<>();

   publicRouteOperator(ObjectMapperobjectMapper, RouteDefinitionWriterrouteDefinitionWriter, ApplicationEventPublisherapplicationEventPublisher) {

       this.objectMapper=objectMapper;

       this.routeDefinitionWriter=routeDefinitionWriter;

       this.applicationEventPublisher=applicationEventPublisher;

   }

   /**

    * 清理集合中的所有路由,并清空集合

    */

   privatevoidclear() {

       // 全部调用API清理掉

       try {

           routeList.forEach(id->routeDefinitionWriter.delete(Mono.just(id)).subscribe());

       } catch (Exceptione) {

           log.error("clear Route is error !");

       }

       // 清空集合

       routeList.clear();

   }

   /**

    * 新增路由

    *

    * @param routeDefinitions

    */

   privatevoidadd(List<RouteDefinition>routeDefinitions) {

       try {

           routeDefinitions.forEach(routeDefinition-> {

               routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();

               routeList.add(routeDefinition.getId());

           });

       } catch (Exceptionexception) {

           log.error("add route is error", exception);

       }

   }

   /**

    * 发布进程内通知,更新路由

    */

   privatevoidpublish() {

       applicationEventPublisher.publishEvent(newRefreshRoutesEvent(routeDefinitionWriter));

   }

   /**

    * 更新所有路由信息

    *

    * @param configStr

    */

   publicvoidrefreshAll(StringconfigStr) {

       log.info("start refreshAll : {}", configStr);

       // 无效字符串不处理

       if (!StringUtils.hasText(configStr)) {

           log.error("invalid string for route config");

           return;

       }

       // 用Jackson反序列化

       List<RouteDefinition>routeDefinitions=null;

       try {

           routeDefinitions=objectMapper.readValue(configStr, newTypeReference<>() {

           });

       } catch (JsonProcessingExceptione) {

           log.error("get route definition from nacos string error", e);

       }

       // 如果等于null,表示反序列化失败,立即返回

       if (null==routeDefinitions) {

           return;

       }

       // 清理掉当前所有路由

       clear();

       // 添加最新路由

       add(routeDefinitions);

       // 通过应用内消息的方式发布

       publish();

       log.info("finish refreshAll");

   }

}

RouteConfigListener 路由变化监听器

监听器的主要作用监听nacos路由配置信息,获取配置信息后刷新进程内路由信息。

该配置类通过@PostConstruct注解,启动时加载dynamicRouteByNacosListener方法,通过nacos的host、namespace、group等信息,读取nacos配置信息。addListener接口获取到配置信息后,将配置信息交给routeOperator.refreshAll处理。

这里指定了数据ID为:gateway-json-routes;

packagecom.july.gateway.listener;

importcom.alibaba.nacos.api.NacosFactory;

importcom.alibaba.nacos.api.PropertyKeyConst;

importcom.alibaba.nacos.api.config.ConfigService;

importcom.alibaba.nacos.api.config.listener.Listener;

importcom.alibaba.nacos.api.exception.NacosException;

importcom.july.gateway.service.RouteOperator;

importlombok.extern.slf4j.Slf4j;

importorg.springframework.beans.factory.annotation.Autowired;

importorg.springframework.beans.factory.annotation.Value;

importorg.springframework.stereotype.Component;

importjavax.annotation.PostConstruct;

importjava.util.Properties;

importjava.util.concurrent.Executor;

/**

* nacos监听器

*

* @author wanghongjie

*/

@Component

@Slf4j

publicclassRouteConfigListener {

   privateStringdataId="gateway-json-routes";

   @Value("${spring.cloud.nacos.config.server-addr}")

   privateStringserverAddr;

   @Value("${spring.cloud.nacos.config.namespace}")

   privateStringnamespace;

   @Value("${spring.cloud.nacos.config.group}")

   privateStringgroup;

   @Autowired

   RouteOperatorrouteOperator;

   @PostConstruct

   publicvoiddynamicRouteByNacosListener() throwsNacosException {

       log.info("gateway-json-routes dynamicRouteByNacosListener config serverAddr is {} namespace is {} group is {}", serverAddr, namespace, group);

       Propertiesproperties=newProperties();

       properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);

       properties.put(PropertyKeyConst.NAMESPACE, namespace);

       ConfigServiceconfigService=NacosFactory.createConfigService(properties);

       // 添加监听,nacos上的配置变更后会执行

       configService.addListener(dataId, group, newListener() {

           @Override

           publicvoidreceiveConfigInfo(StringconfigInfo) {

               // 解析和处理都交给RouteOperator完成

               routeOperator.refreshAll(configInfo);

           }

           @Override

           publicExecutorgetExecutor() {

               returnnull;

           }

       });

       // 获取当前的配置

       StringinitConfig=configService.getConfig(dataId, group, 5000);

       // 立即更新

       routeOperator.refreshAll(initConfig);

   }

}

RouteOperatorConfig 配置类

配置类非常简单,熟悉SpringBoot的都能理解;

packagecom.july.gateway.config;

importcom.fasterxml.jackson.databind.ObjectMapper;

importcom.july.gateway.service.RouteOperator;

importorg.springframework.cloud.gateway.route.RouteDefinitionWriter;

importorg.springframework.context.ApplicationEventPublisher;

importorg.springframework.context.annotation.Bean;

importorg.springframework.context.annotation.Configuration;

/**

* 路由配置类

*

* @author wanghongjie

*/

@Configuration

publicclassRouteOperatorConfig {

   @Bean

   publicRouteOperatorrouteOperator(ObjectMapperobjectMapper,

                                      RouteDefinitionWriterrouteDefinitionWriter,

                                      ApplicationEventPublisherapplicationEventPublisher) {

       returnnewRouteOperator(objectMapper,

               routeDefinitionWriter,

               applicationEventPublisher);

   }

}

测试

启动nacos,这里使用本机测试;

在nacos中增加以下配置:

[

 {

   "id": "app-server-a",

   "uri": "lb://app-server-a",

   "predicates": [

     {

       "name": "Path",

       "args": {

         "pattern": "/a/**"

       }

     }

   ],

   "filters": [

     {

       "name": "StripPrefix",

       "args": {

         "parts": "1"

       }

     }

   ]

 }

]

这里我们先将app-server-a添加到网关中。

我们启动app-server-aapp-server-bgateway-server;

我们启动网关可以看到正常拉去到配置信息:

我们测试下服务A能否正常访问,这里网关的端口是8080;

我们访问:127.0.0.1:8080/a/server-a

可以看到访问成功:

我们不停止服务,新增路由访问服务B:

nacos配置如下:

[

 {

   "id": "app-server-a",

   "uri": "lb://app-center-a",

   "predicates": [

     {

       "name": "Path",

       "args": {

         "pattern": "/a/**"

       }

     }

   ],

   "filters": [

     {

       "name": "StripPrefix",

       "args": {

         "parts": "1"

       }

     }

   ]

 },

 {

   "id": "app-server-b",

   "uri": "lb://app-center-b",

   "predicates": [

     {

       "name": "Path",

       "args": {

         "pattern": "/b/**"

       }

     }

   ],

   "filters": [

     {

       "name": "StripPrefix",

       "args": {

         "parts": "1"

       }

     }

   ]

 }

]

我们在浏览器中访问:127.0.0.1:8080/b/server-b

我们把/b/改成c在测试下;

可以看到到使用c可以访问成功啦,在使用b访问,会出现404;

我们使用127.0.0.1:8080/actuator/gateway/routes查看下当前路由。

[

 {

   "predicate": "Paths: [/a/**], match trailing slash: true",

   "route_id": "app-server-a",

   "filters": [

     "[[StripPrefix parts = 1], order = 1]"

   ],

   "uri": "lb://app-center-a",

   "order": 0

 },

 {

   "predicate": "Paths: [/c/**], match trailing slash: true",

   "route_id": "app-server-b",

   "filters": [

     "[[StripPrefix parts = 1], order = 1]"

   ],

   "uri": "lb://app-center-b",

   "order": 0

 }

]

至此,网关动态路由研发测试完成。

拓展

有些公司会在网关中增加限流,使用RequestRateLimiter组件,正常配置信息如下:

那么动态路由中json应该这样配置:

[

   {

       "id": "server",

       "uri": "lb://jdd-server",

       "predicates":[

           {

               "name": "Path",

               "args": {

                   "pattern": "/server/**"

               }

           }

       ],

       "filters":[

           {

               "name":"StripPrefix",

               "args":{

                   "parts": "1"

               }

           },

           {

               "name":"RequestRateLimiter",

               "args":{

                   "redis-rate-limiter.replenishRate":"1000",

                    "redis-rate-limiter.burstCapacity":"1000",

                     "key-resolver":"#{@remoteAddrKeyResolver}"

               }

           }

       ]

   }

]

over!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
3月前
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
223 69
利用Spring Cloud Gateway Predicate优化微服务路由策略
|
25天前
|
前端开发 Java Nacos
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。
85 0
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
|
3月前
|
JavaScript Java Kotlin
深入 Spring Cloud Gateway 过滤器
Spring Cloud Gateway 是新一代微服务网关框架,支持多种过滤器实现。本文详解了 `GlobalFilter`、`GatewayFilter` 和 `AbstractGatewayFilterFactory` 三种过滤器的实现方式及其应用场景,帮助开发者高效利用这些工具进行网关开发。
513 1
|
4月前
|
负载均衡 Java API
项目中用的网关Gateway及SpringCloud
Spring Cloud Gateway 是一个功能强大、灵活易用的API网关解决方案。通过配置路由、过滤器、熔断器和限流等功能,可以有效地管理和保护微服务。本文详细介绍了Spring Cloud Gateway的基本概念、配置方法和实际应用,希望能帮助开发者更好地理解和使用这一工具。通过合理使用Spring Cloud Gateway,可以显著提升微服务架构的健壮性和可维护性。
131 0
|
6月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
299 5
|
6月前
|
Java 开发者 Spring
Spring Cloud Gateway 中,过滤器的分类有哪些?
Spring Cloud Gateway 中,过滤器的分类有哪些?
171 3
|
5月前
|
负载均衡 Java API
【Spring Cloud生态】Spring Cloud Gateway基本配置
【Spring Cloud生态】Spring Cloud Gateway基本配置
105 0
|
24天前
|
人工智能 SpringCloudAlibaba 自然语言处理
SpringCloud Alibaba AI整合DeepSeek落地AI项目实战
在现代软件开发领域,微服务架构因其灵活性、可扩展性和模块化特性而受到广泛欢迎。微服务架构通过将大型应用程序拆分为多个小型、独立的服务,每个服务运行在其独立的进程中,服务与服务间通过轻量级通信机制(通常是HTTP API)进行通信。这种架构模式有助于提升系统的可维护性、可扩展性和开发效率。
227 1
|
1天前
|
负载均衡 Dubbo Java
Spring Cloud Alibaba与Spring Cloud区别和联系?
Spring Cloud Alibaba与Spring Cloud区别和联系?
|
6月前
|
SpringCloudAlibaba API 开发者
新版-SpringCloud+SpringCloud Alibaba
新版-SpringCloud+SpringCloud Alibaba