• SpringCloud Gateway 基于nacos实现动态路由


    动态路由背景

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

    • 代码方式
    1. @SpringBootApplication
    2. public class DemogatewayApplication {
    3.     @Bean
    4.     public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    5.         return builder.routes()
    6.             .route("path_route", r -> r.path("/get")
    7.                 .uri("http://httpbin.org"))
    8.             .route("host_route", r -> r.host("*.myhost.org")
    9.                 .uri("http://httpbin.org"))
    10.             .route("rewrite_route", r -> r.host("*.rewrite.org")
    11.                 .filters(f -> f.rewritePath("/foo/(?.*)""/${segment}"))
    12.                 .uri("http://httpbin.org"))
    13.             .route("hystrix_route", r -> r.host("*.hystrix.org")
    14.                 .filters(f -> f.hystrix(c -> c.setName("slowcmd")))
    15.                 .uri("http://httpbin.org"))
    16.             .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
    17.                 .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
    18.                 .uri("http://httpbin.org"))
    19.             .route("limit_route", r -> r
    20.                 .host("*.limited.org").and().path("/anything/**")
    21.                 .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
    22.                 .uri("http://httpbin.org"))
    23.             .build();
    24.     }
    25. }
    • 配置文件方式
    1. spring:
    2.   jmx:
    3.     enabled: false
    4.   cloud:
    5.     gateway:
    6.       default-filters:
    7.       - PrefixPath=/httpbin
    8.       - AddResponseHeader=X-Response-Default-Foo, Default-Bar
    9.       routes:
    10.       # =====================================
    11.       # to run server
    12.       # $ wscat --listen 9000
    13.       # to run client
    14.       # $ wscat --connect ws://localhost:8080/echo
    15.       - id: websocket_test
    16.         uri: ws://localhost:9000
    17.         order9000
    18.         predicates:
    19.         - Path=/echo
    20.       # =====================================
    21.       - id: default_path_to_httpbin
    22.         uri: ${test.uri}
    23.         order10000
    24.         predicates:
    25.         - 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方法。

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

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

    1. package com.july.gateway.service;
    2. import com.fasterxml.jackson.core.JsonProcessingException;
    3. import com.fasterxml.jackson.core.type.TypeReference;
    4. import com.fasterxml.jackson.databind.ObjectMapper;
    5. import lombok.extern.slf4j.Slf4j;
    6. import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
    7. import org.springframework.cloud.gateway.route.RouteDefinition;
    8. import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
    9. import org.springframework.context.ApplicationEventPublisher;
    10. import org.springframework.util.StringUtils;
    11. import reactor.core.publisher.Mono;
    12. import java.util.ArrayList;
    13. import java.util.List;
    14. /**
    15.  * 动态路由更新服务
    16.  *
    17.  * @author wanghongjie
    18.  */
    19. @Slf4j
    20. public class RouteOperator {
    21.     private ObjectMapper objectMapper;
    22.     private RouteDefinitionWriter routeDefinitionWriter;
    23.     private ApplicationEventPublisher applicationEventPublisher;
    24.     private static final List<String> routeList = new ArrayList<>();
    25.     public RouteOperator(ObjectMapper objectMapper, RouteDefinitionWriter routeDefinitionWriter, ApplicationEventPublisher applicationEventPublisher) {
    26.         this.objectMapper = objectMapper;
    27.         this.routeDefinitionWriter = routeDefinitionWriter;
    28.         this.applicationEventPublisher = applicationEventPublisher;
    29.     }
    30.     /**
    31.      * 清理集合中的所有路由,并清空集合
    32.      */
    33.     private void clear() {
    34.         // 全部调用API清理掉
    35.         try {
    36.             routeList.forEach(id -> routeDefinitionWriter.delete(Mono.just(id)).subscribe());
    37.         } catch (Exception e) {
    38.             log.error("clear Route is error !");
    39.         }
    40.         // 清空集合
    41.         routeList.clear();
    42.     }
    43.     /**
    44.      * 新增路由
    45.      *
    46.      * @param routeDefinitions
    47.      */
    48.     private void add(List<RouteDefinition> routeDefinitions) {
    49.         try {
    50.             routeDefinitions.forEach(routeDefinition -> {
    51.                 routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
    52.                 routeList.add(routeDefinition.getId());
    53.             });
    54.         } catch (Exception exception) {
    55.             log.error("add route is error"exception);
    56.         }
    57.     }
    58.     /**
    59.      * 发布进程内通知,更新路由
    60.      */
    61.     private void publish() {
    62.         applicationEventPublisher.publishEvent(new RefreshRoutesEvent(routeDefinitionWriter));
    63.     }
    64.     /**
    65.      * 更新所有路由信息
    66.      *
    67.      * @param configStr
    68.      */
    69.     public void refreshAll(String configStr) {
    70.         log.info("start refreshAll : {}", configStr);
    71.         // 无效字符串不处理
    72.         if (!StringUtils.hasText(configStr)) {
    73.             log.error("invalid string for route config");
    74.             return;
    75.         }
    76.         // 用Jackson反序列化
    77.         List<RouteDefinition> routeDefinitions = null;
    78.         try {
    79.             routeDefinitions = objectMapper.readValue(configStr, new TypeReference<>() {
    80.             });
    81.         } catch (JsonProcessingException e) {
    82.             log.error("get route definition from nacos string error", e);
    83.         }
    84.         // 如果等于null,表示反序列化失败,立即返回
    85.         if (null == routeDefinitions) {
    86.             return;
    87.         }
    88.         // 清理掉当前所有路由
    89.         clear();
    90.         // 添加最新路由
    91.         add(routeDefinitions);
    92.         // 通过应用内消息的方式发布
    93.         publish();
    94.         log.info("finish refreshAll");
    95.     }
    96. }

    RouteConfigListener 路由变化监听器

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

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

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

    1. package com.july.gateway.listener;
    2. import com.alibaba.nacos.api.NacosFactory;
    3. import com.alibaba.nacos.api.PropertyKeyConst;
    4. import com.alibaba.nacos.api.config.ConfigService;
    5. import com.alibaba.nacos.api.config.listener.Listener;
    6. import com.alibaba.nacos.api.exception.NacosException;
    7. import com.july.gateway.service.RouteOperator;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.springframework.beans.factory.annotation.Autowired;
    10. import org.springframework.beans.factory.annotation.Value;
    11. import org.springframework.stereotype.Component;
    12. import javax.annotation.PostConstruct;
    13. import java.util.Properties;
    14. import java.util.concurrent.Executor;
    15. /**
    16.  * nacos监听器
    17.  *
    18.  * @author wanghongjie
    19.  */
    20. @Component
    21. @Slf4j
    22. public class RouteConfigListener {
    23.     private String dataId = "gateway-json-routes";
    24.     @Value("${spring.cloud.nacos.config.server-addr}")
    25.     private String serverAddr;
    26.     @Value("${spring.cloud.nacos.config.namespace}")
    27.     private String namespace;
    28.     @Value("${spring.cloud.nacos.config.group}")
    29.     private String group;
    30.     @Autowired
    31.     RouteOperator routeOperator;
    32.     @PostConstruct
    33.     public void dynamicRouteByNacosListener() throws NacosException {
    34.         log.info("gateway-json-routes dynamicRouteByNacosListener config serverAddr is {} namespace is {} group is {}", serverAddr, namespace, group);
    35.         Properties properties = new Properties();
    36.         properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
    37.         properties.put(PropertyKeyConst.NAMESPACE, namespace);
    38.         ConfigService configService = NacosFactory.createConfigService(properties);
    39.         // 添加监听,nacos上的配置变更后会执行
    40.         configService.addListener(dataId, group, new Listener() {
    41.             @Override
    42.             public void receiveConfigInfo(String configInfo) {
    43.                 // 解析和处理都交给RouteOperator完成
    44.                 routeOperator.refreshAll(configInfo);
    45.             }
    46.             @Override
    47.             public Executor getExecutor() {
    48.                 return null;
    49.             }
    50.         });
    51.         // 获取当前的配置
    52.         String initConfig = configService.getConfig(dataId, group, 5000);
    53.         // 立即更新
    54.         routeOperator.refreshAll(initConfig);
    55.     }
    56. }

    RouteOperatorConfig 配置类

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

    1. package com.july.gateway.config;
    2. import com.fasterxml.jackson.databind.ObjectMapper;
    3. import com.july.gateway.service.RouteOperator;
    4. import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
    5. import org.springframework.context.ApplicationEventPublisher;
    6. import org.springframework.context.annotation.Bean;
    7. import org.springframework.context.annotation.Configuration;
    8. /**
    9.  * 路由配置类
    10.  *
    11.  * @author wanghongjie
    12.  */
    13. @Configuration
    14. public class RouteOperatorConfig {
    15.     @Bean
    16.     public RouteOperator routeOperator(ObjectMapper objectMapper,
    17.                                        RouteDefinitionWriter routeDefinitionWriter,
    18.                                        ApplicationEventPublisher applicationEventPublisher) {
    19.         return new RouteOperator(objectMapper,
    20.                 routeDefinitionWriter,
    21.                 applicationEventPublisher);
    22.     }
    23. }

    测试

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

    在nacos中增加以下配置:

    1. [
    2.   {
    3.     "id""app-server-a",
    4.     "uri""lb://app-server-a",
    5.     "predicates": [
    6.       {
    7.         "name""Path",
    8.         "args": {
    9.           "pattern""/a/**"
    10.         }
    11.       }
    12.     ],
    13.     "filters": [
    14.       {
    15.         "name""StripPrefix",
    16.         "args": {
    17.           "parts""1"
    18.         }
    19.       }
    20.     ]
    21.   }
    22. ]

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

     

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

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

     

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

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

    可以看到访问成功:

     

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

    nacos配置如下:

    1. [
    2.   {
    3.     "id""app-server-a",
    4.     "uri""lb://app-center-a",
    5.     "predicates": [
    6.       {
    7.         "name""Path",
    8.         "args": {
    9.           "pattern""/a/**"
    10.         }
    11.       }
    12.     ],
    13.     "filters": [
    14.       {
    15.         "name""StripPrefix",
    16.         "args": {
    17.           "parts""1"
    18.         }
    19.       }
    20.     ]
    21.   },
    22.   {
    23.     "id""app-server-b",
    24.     "uri""lb://app-center-b",
    25.     "predicates": [
    26.       {
    27.         "name""Path",
    28.         "args": {
    29.           "pattern""/b/**"
    30.         }
    31.       }
    32.     ],
    33.     "filters": [
    34.       {
    35.         "name""StripPrefix",
    36.         "args": {
    37.           "parts""1"
    38.         }
    39.       }
    40.     ]
    41.   }
    42. ]

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

     

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

     

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

     

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

     

    1. [
    2.   {
    3.     "predicate""Paths: [/a/**], match trailing slash: true",
    4.     "route_id""app-server-a",
    5.     "filters": [
    6.       "[[StripPrefix parts = 1], order = 1]"
    7.     ],
    8.     "uri""lb://app-center-a",
    9.     "order"0
    10.   },
    11.   {
    12.     "predicate""Paths: [/c/**], match trailing slash: true",
    13.     "route_id""app-server-b",
    14.     "filters": [
    15.       "[[StripPrefix parts = 1], order = 1]"
    16.     ],
    17.     "uri""lb://app-center-b",
    18.     "order"0
    19.   }
    20. ]

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

    拓展

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

    WX20220906-175454@2x

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

    1. [
    2.     {
    3.         "id""server",
    4.         "uri""lb://jdd-server",
    5.         "predicates":[
    6.             {
    7.                 "name""Path",
    8.                 "args": {
    9.                     "pattern""/server/**"
    10.                 }
    11.             }
    12.         ],
    13.         "filters":[
    14.             {
    15.                 "name":"StripPrefix",
    16.                 "args":{
    17.                     "parts""1"
    18.                 }
    19.             },
    20.             {
    21.                 "name":"RequestRateLimiter",
    22.                 "args":{
    23.                     "redis-rate-limiter.replenishRate":"1000",
    24.                      "redis-rate-limiter.burstCapacity":"1000",
    25.                       "key-resolver":"#{@remoteAddrKeyResolver}"
    26.                 }
    27.             }
    28.         ]
    29.     }
    30. ]

    over!

    关注公众号:杰子学编程 ,回复《动态路由》获取源码。

  • 相关阅读:
    单区域OSPF配置
    Docker部署前后端服务示例
    Qt 窗口的坐标体系
    ASML大举向中国出口光刻机,或在于忧虑中国光刻机技术取得突破
    LeetCode20 有效的括号
    想学会SOLID原则,看这一篇文章就够了!
    Go基础 Map
    空气开关(空开)
    Revit中创建基于线的砌体墙及【快速砌体排砖】
    gin通过文件流提供流式下载文件,golang
  • 原文地址:https://blog.csdn.net/July_whj/article/details/126731696