暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

如何实现Spring Gateway路由的动态加载和刷新?

领创集团Advance Group 2022-03-30
2348

导语:Spring Cloud是一套优秀的微服务解决方案,堪称微服务架构集大成者。然而官方提供的API网关中,没有实现动态更新路由这个对大型复杂系统非常重要的功能。在此我们提供了一个完整的技术实现方式,希望能为有类似需求的同仁提供参考。


前言

在微服务化过程中,客户端可能需要调用多个服务的接口才能完成一个业务需求。网关作为后端微服务的统一入口,利用路由转发可以方便客户端快速接入各个微服务。
Spring Gateway (以下简称gateway)路由配置默认情况下是写在配置文件中的,有一个新的服务接入时,需要修改配置文件,然后重启网关才能生效。然而目前更多的需求是通过提供可视化页面,在前端页面增删改来使网关路由动态生效。
结合实际业务需求,接下来从原理和实践两个方面介绍如何上述需求。

原理

我们分两部分来说,先说动态加载,再说动态刷新。
2.1 动态加载
要想实现动态加载,我们要先弄清楚gateway是如何加载现有的配置信息的。
目前配置路由主要有两种方式,一种是用yml配置文件,一种是写代码里。
    # yml配置文件形式
    spring:
    cloud:
    gateway:
    routes:
    - id: v1
    uri: http://xxx/v1
    predicates:
    - Path=/xxx
    复制

      // 代码形式
      @Bean
      public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
      return builder.routes()
      .route("v1", r -> r.path("/xxx")
      .uri("http://xxx/v1"))
      .build();
      }
      复制
      无论是yml还是代码,这些配置最终都是被封装到RouteDefinition对象中的。所有路由信息在系统启动时就被加载装配好了,并存到了内存里。
      参考代码如下:
        //GatewayAutoConfiguration
        @Bean
        @ConditionalOnMissingBean
        public PropertiesRouteDefinitionLocator
        propertiesRouteDefinitionLocator(
        GatewayProperties properties) {
        return new PropertiesRouteDefinitionLocator(properties);
        }
        @Bean
        @ConditionalOnMissingBean(RouteDefinitionRepository.class)
        public InMemoryRouteDefinitionRepository
        inMemoryRouteDefinitionRepository() {
        return new InMemoryRouteDefinitionRepository();
        }
        //PropertiesRouteDefinitionLocator
        public class PropertiesRouteDefinitionLocator implements
        RouteDefinitionLocator {
        private final GatewayProperties properties;
        ...
        @Override
        public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(this.properties.getRoutes());
        }
        }
        复制
        重点类为 RouteDefinitionLocator,此接口有多个实现类,分别对应不同方式配置的路由方式。其中 RouteDefinitionRepository 是从存储器中(例如:mysql、redis 等)读取 RouteDefinition。可以结合 RouteDefinitionRepository 来实现动态路由加载。其他几种获取路由方式不是本文重点,暂不详说。

        这样,我们就可以在网关启动时从一个动态数据源(数据库等,而非代码和配置文件)加载配置数据,并通过 RouteDefinitionRepository 的实现类 InMemoryRouteDefinitionRepository 加载为路由信息了。

        2.2 动态刷新
        通过2.1我们找到了gateway从数据库加载路由的方式,但是还停留在gateway启动时加载数据的方式。接下来就看下如何实现在用户操作数据的时候就能完成gateway路由的刷新。
        其实gateway是提供了刷新的事件的,那就是RefreshRoutesEvent
        参考代码如下:
          public class RefreshRoutesEvent extends ApplicationEvent {  
          **
          * Create a new ApplicationEvent.
            * @param source the object on which the event initially occurred
          (never {@code null})  
          */  
          public RefreshRoutesEvent(Object source) {    
          super(source);  
          }
          }
          复制
          我们再来深挖下触发了RefreshRoutesEvent事件后,gateway是如何处理的。
            @Override
            public void onApplicationEvent(ApplicationEvent event) {
            ...
            else if (event instanceof RefreshRoutesEvent && routeLocator != null)
            {  
             forces initialization  
            routeLocator.ifAvailable(locator ->
            locator.getRoutes().subscribe());  
            }
            }
            复制
            所以,我们的思路是,在用户操作路由数据,在数据保存到数据库之后,代码向gateway发送刷新事件就可以了。


            实践

            通过原理的分析,了解了网关从存储器中加载和动态刷新的机制。我们来整理下思路。
            • 想加载路由数据到gateway内存中,需要调用InMemoryRouteDefinitionRepository 的save()方法。

            • 想触发gateway的动态刷新,需要调用RefreshRoutesEvent事件。

            接下来从这两点出发,一起开启实践之旅。
            3.1 动态加载
            动态加载按照顺序可以分为三步,加载路由数据、组装路由、保存到gateway内存中。
            3.1.1 加载数据库数
            可以通过 feign 调用的方式读取数据,此步骤比较简单,不做详细的描述。
              List<GatewayRoute> routeList =
              gatewayServiceClient.getApiRouteList().getData();
              复制
              3.1.2 组装路由、保存到gateway内存
              刚刚也分析到了RouteDefinition 是路由数据的核心。它大体包括三个部分,基本信息、断言(PredicateDefinition)、过滤器(FilterDefinition)。
              接下来根据查询到的路由数据,进行gateway路由数据封装和保存。
                //路由封装
                routeList.forEach(gatewayRoute -> {


                RouteDefinition结构
                RouteDefinition definition = new RouteDefinition();


                设置基本信息
                definition.setId(gatewayRoute.getRouteName());
                    definition.setUri(UriComponentsBuilder.fromUriString("lb://" +gatewayRoute.getServiceId()).build().toUri());    
                  
                  //设置断言信息  
                  List<PredicateDefinition> predicates = Lists.newArrayList();  
                  PredicateDefinition predicatePath = new PredicateDefinition();  
                  Map<String, String> predicatePathParams = new HashMap<>(8);    
                  predicatePath.setName("Path");  
                  predicatePathParams.put("name", gatewayRoute.getRouteName());  
                  predicatePathParams.put("pattern", gatewayRoute.getPath());  
                  predicatePathParams.put("pathPattern", gatewayRoute.getPath());  
                  predicatePath.setArgs(predicatePathParams);  
                  predicates.add(predicatePath);  
                  definition.setPredicates(predicates);    
                  
                  //设置过滤器信息  
                  List<FilterDefinition> filters = filters(gatewayRoute);  
                  if (!CollectionUtils.isEmpty(filters)) {    
                   definition.setFilters(filters); 
                  }   
                  
                  //重点,保存到gateway内存,其实是放在了Map<String, RouteDefinition> routes里  this.repository.save(Mono.just(definition)).subscribe();
                });
                复制
                3.2 动态刷新
                动态刷新分为两步。第一步是提供公共方法,允许业务侧更新数据库之后调用刷新网关事件。第二步在gateway端接收到刷新网关事件后,调用RefreshRoutesEvent事件实现刷新。
                3.2.1 
                公共gateway刷新方法
                参考代码如下
                  public class OpenRestTemplate extends RestTemplate {
                     /** 
                     * 刷新网关
                  */
                  public void refreshGateway() {
                  publisher.publishEvent(
                  new RefreshRemoteApplicationEvent(this,busProperties.getId(),null)
                  );
                  }
                  }
                  复制

                  3.2.2  调用RefreshRoutesEvent事件实现刷新
                  gateway接收到业务侧路由刷新事件后,调用 RefreshRoutesEvent 即可完成刷新动作。
                  参考代码如下
                    /**
                    * 接收业务侧刷新事件
                    *
                    * @param event
                    */
                    @Override
                    public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
                    refresh();
                    }
                    /**
                    * 刷新路由
                    *
                    * @return
                    */
                    public Mono<Void> refresh() {
                    //3.1加载路由的逻辑
                    this.loadRoutes();
                    //触发默认路由刷新事件,刷新缓存路由
                    this.publisher.publishEvent(new RefreshRoutesEvent(this));
                    return Mono.empty();
                    }
                    复制


                    结论

                    结合实际业务需求,从原理和实践两个方面介绍了实现网关路由的动态加载和刷新方式。至此动态加载和刷新的功能就实现了,通过此功能,无论开发、测试、生产环境,都可以通过可视化页面快速的实现路由的动态修改,在配置速度和准确率上都可以得到很大提升。

                    感谢阅读「技术创想」第48期文章

                    领创集团正在春季招聘中

                    期待你的加入

                    点击文末

                    阅读原文

                    获取更多

                    招聘信息


                    关于领创集团

                    (Advance Intelligence Group)

                    领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。

                    文章转载自领创集团Advance Group,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论