Gateway 动态路由实现

1,687 阅读6分钟

GateWay动态路由

​ 在Gateway的学习过程中都是基于配置文件(application.yml)的方式静态加载路由,而在实际的生产环境中静态的配置往往不能够满足实际的开发需求,所以需要一个存储终端持久化所需要的路由信息,并且支持实时加载更新。

​ 本文采用 监听 Nacos 配置文件读取到JSON将其转为 RouteDefinition 对象,配合 RouteDefinitionWriterApplicationEventPublisher 实例进行路由刷新, 实现动态路由

探讨目的

  1. 深入了解 Gateway 内部路由加载逻辑
  2. 巩固 Nacos-Client 核心底层操作
  3. 基于 Nacos 2.x 实现 Gateway 的动态路由加载

Gateway RouteLocator 源码导读

在 Spring-Cloud-Gateway 为了进行路由的加载操作,在自动装配类中存在一个路由定位器 RouteLocator

@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties, List<GatewayFilterFactory> gatewayFilters, List<RoutePredicateFactory> predicates, RouteDefinitionLocator routeDefinitionLocator, ConfigurationService configurationService) {
    return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, gatewayFilters, properties, configurationService);
}

@Bean
@Primary
@ConditionalOnMissingBean(name = {"cachedCompositeRouteLocator"})
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
    return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}
public interface RouteLocator {
    Flux<Route> getRoutes();
}

通过方法定义可以看出他最终内部保存的路由是 Route 对象实例

RouteLocator 的三个实现类分别为:

  • RouteDefinitionRouteLocator:路由基础定义
  • CachingRouteLocator:缓存路由定位器
  • CompositeRouteLocator:复合路由

image.png

本文主要以RouteDefinitionRouteLocator 源代码,来解释在Gateway中内部路由是如何存储的,以下是截取源代码

package org.springframework.cloud.gateway.route;

public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware {
    public static final String DEFAULT_FILTERS = "defaultFilters";
    protected final Log logger = LogFactory.getLog(this.getClass());
    private final RouteDefinitionLocator routeDefinitionLocator;
    private final ConfigurationService configurationService;
    private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap();
    private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap();
    private final GatewayProperties gatewayProperties;
    
    public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator, List<RoutePredicateFactory> predicates, List<GatewayFilterFactory> gatewayFilterFactories, GatewayProperties gatewayProperties, ConfigurationService configurationService) {
        this.routeDefinitionLocator = routeDefinitionLocator;
        this.configurationService = configurationService;
        this.initFactories(predicates);
        gatewayFilterFactories.forEach((factory) -> {
            GatewayFilterFactory var10000 = (GatewayFilterFactory)this.gatewayFilterFactories.put(factory.name(), factory);
        });
        this.gatewayProperties = gatewayProperties;
    }

    public Flux<Route> getRoutes() {
        Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
        if (!this.gatewayProperties.isFailOnRouteDefinitionError()) {
            routes = routes.onErrorContinue((error, obj) -> {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("RouteDefinition id " + ((RouteDefinition)obj).getId() + " will be ignored. Definition has invalid configs, " + error.getMessage());
                }

            });
        }

        return routes.map((route) -> {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("RouteDefinition matched: " + route.getId());
            }

            return route;
        });
    }

    private Route convertToRoute(RouteDefinition routeDefinition) {
        AsyncPredicate<ServerWebExchange> predicate = this.combinePredicates(routeDefinition);
        List<GatewayFilter> gatewayFilters = this.getFilters(routeDefinition);
        return ((AsyncBuilder)Route.async(routeDefinition).asyncPredicate(predicate).replaceFilters(gatewayFilters)).build();
    }

}

通过以上部分截取源代码可以发现在父接口中定义的 getRoutes() 方法实现中将 RouteDefinition 对象转换为了 Route对象。

提问:RouteDefinition 与 Route 对象哪个才是我们关心的?,有什么差异

RouteDefinition :

package org.springframework.cloud.gateway.route;

@Validated
public class RouteDefinition {
    private String id;
    @NotEmpty
    @Valid
    private List<PredicateDefinition> predicates = new ArrayList();
    @Valid
    private List<FilterDefinition> filters = new ArrayList();
    @NotNull
    private URI uri;
    private Map<String, Object> metadata = new HashMap();
    private int order = 0;

    public RouteDefinition() {
    }

}

Route :

package org.springframework.cloud.gateway.route;

public class Route implements Ordered {
    private final String id;
    private final URI uri;
    private final int order;
    private final AsyncPredicate<ServerWebExchange> predicate;
    private final List<GatewayFilter> gatewayFilters;
    private final Map<String, Object> metadata;

    private Route(String id, URI uri, int order, AsyncPredicate<ServerWebExchange> predicate, List<GatewayFilter> gatewayFilters, Map<String, Object> metadata) {
        this.id = id;
        this.uri = uri;
        this.order = order;
        this.predicate = predicate;
        this.gatewayFilters = gatewayFilters;
        this.metadata = metadata;
    }
    
}

application.yml

spring:
  application:
    name: microGateway
  cloud:
    gateway: # 网关
      routes:
        - id: netty-yzhenb
          uri: https://www.baicu.com
          predicates:
            - Path=/yzhenb
        - id: provider-dept
          uri: lb://provider-dept # 匹配的路径
          predicates:
            - Path=/yzb #匹配全部的子路径
          filters:
            - AddRequestHeader=yzhenb1228,你好-gateway
            - Log=hello,yzhenb

不仔细观察 Route 以及 RouteDefinition 结构很难发现他们之间的差异,结合application.yml就显而易见,RouteDefinition 对象是直接与application.yml挂钩的属性类,而在Gateway的内部路由通过Route存储

代码控制路由

在Gateway中存在一个实例 RouteDefinitionWriter 结合事件发布器可以完成路由的保存和删除操作,以下是他的接口定义

public interface RouteDefinitionWriter {
    Mono<Void> save(Mono<RouteDefinition> route);

    Mono<Void> delete(Mono<String> routeId);
}

下面是动态路由的核心代码

RouteService:

package com.yzb.gateway.service;

/**
 * @author ZhenBang-Yi
 * @ClassName RouteService
 * @date 2022/7/20 22:13
 */
@Slf4j
@Service
public class RouteService implements ApplicationEventPublisherAware {

    @Autowired
    private RouteLocator routeLocator;

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.eventPublisher = applicationEventPublisher;
    }

    /**
     * 新增路由
     * @author: ZhenBang-Yi
     * @date 2022/8/2 22:10
    **/
    public boolean insertRoute(RouteDefinition routeDefinition) {
        log.info("开始执行新增路由,ID为:{}", routeDefinition.getId());
        try {
            this.routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
        } catch (Exception e) {
            log.error("路由新增失败,ID为:{}", routeDefinition.getId());
            return false;
        }
        return true;
    }

    /**
     * 删除路由
     * @author: ZhenBang-Yi
     * @date 2022/8/2 22:09
    **/
    public void deleteRoute(String id) {
        try {
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
            log.info("成功删除路由,路由ID为:{}",id);
        }catch (Exception e){
            log.error("删除路由失败,未找到路由,id为:{}",id);
        }
    }

    /**
     * 路由修改
     * @author: ZhenBang-Yi
     * @date 2022/8/2 22:09
    **/
    public boolean updateRoute(RouteDefinition routeDefinition) {
        try {
            this.deleteRoute(routeDefinition.getId());
            this.routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
        } catch (Exception e) {
            e.printStackTrace();
            log.error("更新路由失败,ID为:{}", routeDefinition.getId());
            return false;
        }
        log.info("更新路由成功,ID为:{}",routeDefinition.getId());
        return true;
    }

    /**
     * 用户获取当前网关中所有的路由ID
     * @author: ZhenBang-Yi
     * @date 2022/8/2 22:09
    **/
    public ArrayList<List<String>> getRoutes(){
        Flux<Route> routes = this.routeLocator.getRoutes();
        Mono<List<String>> idList = routes.map((e) -> e.getId()).collectList();
        ArrayList<List<String>> strings = new ArrayList<>();
        idList.subscribe((e)->{
            log.info("读取到ID有:{}",e);
            strings.add(e);
        });
        return strings;
    }
}

具体的路由业务代码编写完后,为了方便对业务代码测试所以编写一个Action方便Postman进行请求发送。

package com.yzb.gateway.action;

import com.yzb.gateway.service.RouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.web.bind.annotation.*;

/**
 * @author ZhenBang-Yi
 * @ClassName DynicRouteAction
 * @date 2022/7/24 12:47
 */
@RequestMapping("routes/*")
@RestController
public class DynicRouteAction {
    @Autowired
    private RouteService routeService;

    @PostMapping("/add")
    public void add(@RequestBody RouteDefinition definition) {
        this.routeService.insertRoute(definition);
    }

    @PostMapping("/delete/{id}")
    public void delete(@PathVariable("id") String id) {
        this.routeService.deleteRoute(id);
    }

    @PostMapping("/update")
    public Object update(@RequestBody RouteDefinition definition) {
        return this.routeService.updateRoute(definition);
    }

    @GetMapping("/getRoutes")
    public Object getRoutes() {
        return this.routeService.getRoutes();
    }
}

接口测试:( PS: 通过Actuator观察所有的路由情况 )

新增路由:

image.png

image.png

删除路由:

image.png

image.png

修改路由:

image.png

image.png

结合Nacos实现动态路由

通过监听Nacos的配置文件,可以实现动态路由的加载,需要注意在Nacos的领域模型中,不同组的数据是不共享的哦~!

编写一个实体类,从配置文件中读取nacos配置信息,方便后续监听操作。

package com.yzb.gateway.entity;

import com.alibaba.nacos.api.PropertyKeyConst;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Properties;

/**
 * @author ZhenBang-Yi
 * @ClassName DynicRouteProperties
 * @date 2022/7/31 20:05
 */
@Data
@Component
@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery")
public class DynicRouteProperties {
    private String dataId;
    private String namespace;
    private String clusterName;
    private String group;
    private String serverAddr;
    private String service;
    private String userName;
    private String password;
    private String dateId;
    private long timeOut;

    public Properties getProperties() {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, getServerAddr());
        properties.put(PropertyKeyConst.NAMESPACE, getNamespace());
        properties.put(PropertyKeyConst.USERNAME, getUserName());
        properties.put(PropertyKeyConst.PASSWORD, getPassword());
        properties.put(PropertyKeyConst.CLUSTER_NAME, getPassword());
        return properties;
    }

}

通过 CommandLineRunner 在程序启动后,直接读取路由的配置文件,将其转换为 RouteDefinition

package com.yzb.gateway.listening;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yzb.gateway.entity.DynicRouteProperties;
import com.yzb.gateway.service.RouteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

/**
 * @author ZhenBang-Yi
 * @ClassName DynicNacosRouteConfig
 * @date 2022/7/31 20:04
 */
@Slf4j
@Component
@Configuration
public class DynicNacosRouteConfig implements CommandLineRunner {
    @Autowired
    private DynicRouteProperties dynicRouteProperties;
    @Autowired
    private RouteService routeService;
    @Autowired
    private ObjectMapper objectMapper;

    @Bean
    public void listeningNacosConfig() throws NacosException {
        Properties properties = this.dynicRouteProperties.getProperties();
        ConfigService configService = NacosFactory.createConfigService(properties);
        // 获取数据
        String config = configService.getConfig(this.dynicRouteProperties.getDataId(), this.dynicRouteProperties.getGroup(), this.dynicRouteProperties.getTimeOut());
        log.info("获取到Gateway的配置数据:{}", config);
        // 初始化加载路由
        this.routesUpdation(config);
        
        // 监听nacos配置文件的变化
        configService.addListener(this.dynicRouteProperties.getDataId(), this.dynicRouteProperties.getGroup(), new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String content) {
                log.info("读取到更新的路由数据,{}", content);
                DynicNacosRouteConfig.this.routesUpdation(content);
            }
        });
    }

    @Override
    public void run(String... args) throws Exception {
        this.listeningNacosConfig();
    }

    /**
     * 根据gateway配置的额数据,更新理由
     *
     * @param content 接受到的gateway配置数据
     * @author: ZhenBang-Yi
     * @date 2022/7/31 20:27
     **/
    public boolean routesUpdation(String content) {
        try {
            RouteDefinition[] routeDefinitions = this.objectMapper.readValue(content, RouteDefinition[].class);

            // 获取存在当前项目中所有的路由ID
            ArrayList<List<String>> routes = this.routeService.getRoutes();
            List<String> collect = routes.stream().flatMap(e -> e.stream()).collect(Collectors.toList());
            List<String> noneMatchIdList = collect.stream().filter((e) -> Arrays.stream(routeDefinitions).noneMatch((t) -> Objects.equals(t.getId(), e))).collect(Collectors.toList());
            for (String id : noneMatchIdList) {
                try {
                    /**
                    * 以配置文件为主,将多余的路由先删除
                    */
                    this.routeService.deleteRoute(id);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            for (RouteDefinition routeDefinition : routeDefinitions) {
                try {
                    this.routeService.updateRoute(routeDefinition);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } catch (JsonProcessingException e) {
            log.error("Nacos配置文件格式有误,不能将配置的路由结构转为 RouteDefinition.{}", e.getMessage());
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

image.png

启动程序,观察启动日志:

image.png

接下来访问 /provider/dept/list

image.png

更新路由数据,观察日志

image.png

image.png

image.png

此时 list 访问不了 能访问 get

image.png

image.png