SpringCloud Gateway 基于nacos实现动态路由

简介: 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!

目录
相关文章
|
3月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
2月前
|
缓存 JSON NoSQL
别再手写过滤器!SpringCloud Gateway 内置30 个,少写 80% 重复代码
小富分享Spring Cloud Gateway内置30+过滤器,涵盖请求、响应、路径、安全等场景,无需重复造轮子。通过配置实现Header处理、限流、重试、熔断等功能,提升网关开发效率,避免代码冗余。
372 1
|
4月前
|
API
使用Gateway with Inference Extension路由外部MaaS服务
本文介绍如何通过Gateway with Inference Extension对接百炼服务,实现请求路由时自动添加API Key并重写路径,包含操作步骤及验证方法。
|
5月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
373 0
|
6月前
|
缓存 监控 Java
说一说 SpringCloud Gateway 堆外内存溢出排查
我是小假 期待与你的下一次相遇 ~
877 5
|
6月前
|
Java API Nacos
|
12月前
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
1070 69
利用Spring Cloud Gateway Predicate优化微服务路由策略
|
JSON Java Nacos
SpringCloud 应用 Nacos 配置中心注解
在 Spring Cloud 应用中可以非常低成本地集成 Nacos 实现配置动态刷新,在应用程序代码中通过 Spring 官方的注解 @Value 和 @ConfigurationProperties,引用 Spring enviroment 上下文中的属性值,这种用法的最大优点是无代码层面侵入性,但也存在诸多限制,为了解决问题,提升应用接入 Nacos 配置中心的易用性,Spring Cloud Alibaba 发布一套全新的 Nacos 配置中心的注解。
1214 141
|
9月前
|
存储 人工智能 Kubernetes
ACK Gateway with AI Extension:面向Kubernetes大模型推理的智能路由实践
本文介绍了如何利用阿里云容器服务ACK推出的ACK Gateway with AI Extension组件,在Kubernetes环境中为大语言模型(LLM)推理服务提供智能路由和负载均衡能力。文章以部署和优化QwQ-32B模型为例,详细展示了从环境准备到性能测试的完整实践过程。
|
10月前
|
Cloud Native Java Nacos
springcloud/springboot集成NACOS 做注册和配置中心以及nacos源码分析
通过本文,我们详细介绍了如何在 Spring Cloud 和 Spring Boot 中集成 Nacos 进行服务注册和配置管理,并对 Nacos 的源码进行了初步分析。Nacos 作为一个强大的服务注册和配置管理平台,为微服务架构提供
4177 14

热门文章

最新文章