目录

  1. gateway本地文件常规路由配置

  2. 本地文件配置对业务造成的痛点

  3. 动态路由改造

1 gateway本地文件常规路由配置

我们先大致看下gateway中的常规概念

  • Route(路由):路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发。

  • Predicate(谓语、断言):路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:PathQueryMethodHeader等。

  • Filter(过滤器):过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容。

整体架构:

我们本地文件配置路由信息的时候都是application.yml中配置这样一段内容

 spring:    cloud:    gateway:      routes:        - id: authWsdl          uri: lb://demo-auth          predicates:            - Path=/demo/authenticationIf          filters:            - StripPrefix=1            - name: RequestRateLimiter              args:                # 令牌桶每秒填充平均速率                redis-rate-limiter.replenishRate: 100                # 令牌桶的上限                redis-rate-limiter.burstCapacity: 200                # 使用SpEL表达式从Spring容器中获取Bean对象                key-resolver: "#{@pathKeyResolver}"

gateway就可以转发到demo-auth认证微服务上了。至于其他的断言方式配置就不一一展现了,网上一搜一大把。

2 本地文件配置对业务造成的痛点

这中文件配置在实际生产环境中对业务有什么影响么?

我们网关作为业务的最前沿,如果经常更改配置文件重启会造成业务系统可用率大大降低,因为网关一单重启所有业务都不可用。而且对于流量大的公司来说重启网关将是灾难性的。

但是我们实际生产环境中可能后端服务经常变动,频繁的发布版本,或者灰度发布,虽然我们的服务都是注册到注册中心的,但是从业务下线到业务上线,这个时间段网关感知能力不是实时的,再次期间部分请求是失败的。

如果我们后端服务增加或者减少了 同样需要改网关配置文件,然后重启生效。那我们有没有办法吧网关的路由配置搞成动态的,让网关直接去数据库中动态读取呢?这样我们就可以减少网关的重启此处,降低系统失败率。

于是我们翻开spring cloud gateway的源码发现如下几个核心类:

​​​​​​​

org.springframework.cloud.gateway.route.RouteDefinitionWriterorg.springframework.cloud.gateway.route.RouteDefinitionLocatororg.springframework.cloud.gateway.route.RouteDefinitionRepositoryorg.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository

见名之意,一看着就是个mapper层的东西。原来啊gateway默认是走的内存,启动的时候回把配置文件解析到内存中。而RouteDefinitionRepository这个就是mapper的接口,那就好办了,接下来我们自己搞下,有点长耐心看下。

3 动态路由改造

我们的设想是这样

  1. 通过实现RouteDefinitionRepository接口来干扰路由信息的存储、读取等操作。

  2. 抽象出来一个GatewayRouteRepository接口,支持扩展多种存储方式。

  3. 开发一个管理后台对数据源头进行更新操作,后台提供可视化的页面,避免操作人员操作失误(漏写信息,错写单词等等)。

  4. 通过zk实现监听机制,因为我们实际生产环境中网关也是集群部署,我们需要更新完路由配置信息后所有网关实例都可以感知到,然后去数据源端重新拉取最新路由信息。

ok既然思路有了那我们就开始吧,创建MyRouteDefinitionRepository来实现:

org.springframework.cloud.gateway.route.RouteDefinitionRepository

该接口有3个方法:​​​​​​​

//全量获取路由信息Flux<RouteDefinition> getRouteDefinitions() //保存路由信息 Mono<Void> save(Mono<RouteDefinition> route) //删除路由配置信息根据路由idMono<Void> delete(Mono<String> routeId)

我们先看内存实现的方式源码​​​​​​​

    private final Map<String, RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap());
    public InMemoryRouteDefinitionRepository() {    }
    public Mono<Void> save(Mono<RouteDefinition> route) {        return route.flatMap((r) -> {            if (StringUtils.isEmpty(r.getId())) {                return Mono.error(new IllegalArgumentException("id may not be empty"));            } else {                this.routes.put(r.getId(), r);                return Mono.empty();            }        });    }
    public Mono<Void> delete(Mono<String> routeId) {        return routeId.flatMap((id) -> {            if (this.routes.containsKey(id)) {                this.routes.remove(id);                return Mono.empty();            } else {                return Mono.defer(() -> {                    return Mono.error(new NotFoundException("RouteDefinition not found: " + routeId));                });            }        });    }
    public Flux<RouteDefinition> getRouteDefinitions() {        return Flux.fromIterable(this.routes.values());    }

那就简单了 源代码粘过来,然后把

private final Map<String,RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap());

改成我们自己的GatewayRouteRepository接口即可,这是我们的GatewayRouteRepository:​​​​​​​

public interface GatewayRouteRepository {
    void saveData(String id,String data) throws Exception ;
    void remove(String id) throws Exception ;
    List<RouteDefinition> getAllList();
}

我们实现了2中存储方式一种是redis 、一种是zk 为什么实现这2中呢,我们希望网关中引入的组件越少越好,这样可以降低网关的问题率,提高可用度,本身我们就通过redis在网关层做了限流操作,通过zk实现lvs自动负载到网实例(网关注册到zk,agent监听zk临时节点信息动态更改lvs配置)又设想通过zk动态通知所有实例刷新路由故此实现了这两种。

其他朋友也可以通过实现GatewayRouteRepository来适配适合你们自己的存储方式。

我们分别看下这2中实现​​​​​​​

public class RedisGateRouteRespository implements GatewayRouteRepository {
    private Logger log= LoggerFactory.getLogger(RedisGateRouteRespository.class);
    @Autowired    private StringRedisTemplateTest redisTemplate;
    @Override    public void saveData(String id, String data) throws Exception {        redisTemplate.hset(GateWayContext.ROUTEPATH,id,data);    }
    @Override    public void remove(String id) throws Exception {        redisTemplate.hdel(GateWayContext.ROUTEPATH,id);    }    @Override    public List<RouteDefinition> getAllList() {        Set<String> keys = redisTemplate.hkeys(GateWayContext.ROUTEPATH);        List<RouteDefinition> routeDefinitions=new ArrayList<>();        keys.forEach(key->{            GatewayRouteDefinition gatewayRouteDefinition =                    JSON.parseObject(                            (String) redisTemplate.hget(GateWayContext.ROUTEPATH, key),                            GatewayRouteDefinition.class);            log.info("缓存信息:{}",gatewayRouteDefinition.toString());            routeDefinitions.add(RouteDefintionAssemble.assembleRouteDefinition(gatewayRouteDefinition));        });        return routeDefinitions;    }
}
​​​​​​​
public class ZookeeperRepository implements GatewayRouteRepository {
    @Autowired    private CuratorFramework zk;
    @Override    public void saveData(String id,String data) throws Exception {        if(null == zk.checkExists().forPath(GateWayContext.ROUTEPATH + "/" + id)) {            zk.create().creatingParentsIfNeeded().                    withMode(CreateMode.PERSISTENT)                    .forPath(GateWayContext.ROUTEPATH + "/" + id, data.getBytes());        }else{            zk.setData().forPath(GateWayContext.ROUTEPATH + "/" + id, data.getBytes());        }    }
    @Override    public void remove(String id) throws Exception {        zk.delete().forPath(id);    }
    @Override    public List<RouteDefinition> getAllList() {        List<RouteDefinition> routeDefinitions=new ArrayList<>();        try {            List<String> strings = zk.getChildren().forPath(GateWayContext.ROUTEPATH);            strings.forEach(path->{                try {                    String s = new String(zk.getData().forPath(GateWayContext.ROUTEPATH + "/" + path));                    GatewayRouteDefinition gatewayRouteDefinition = JSON.parseObject(s, GatewayRouteDefinition.class);                    routeDefinitions.add(RouteDefintionAssemble.assembleRouteDefinition(gatewayRouteDefinition));                } catch (Exception e) {                    e.printStackTrace();                }            });        } catch (Exception e) {            e.printStackTrace();        }        return routeDefinitions;    }

这2中实现代码很简单,就不做过多阐述了。我们来看下如何同时网关刷新路由信息。还是的看下源码是怎么做的,再次打开源码发现了一个event包如图

哈哈这里有个RefreshRoutesEvent,一看这就是个通知刷新路由的event,那源码是否是通过spring事件机制刷新的路由的呢?我们来debug下看下调用链。

果然是呀,我们向网关增加一个路由然后发布refreshRoutesEvent事件,果然调用到我们自己的gatRouteDefinitions方法中取重新过去了全量路由信息

那我们来看下我们如何用zk实现监听​

public class ZookeeperEventListener {    @Autowired    private NacosDiscoveryProperties nacosDiscoveryProperties;
    private Logger  log= LoggerFactory.getLogger(ZookeeperEventListener.class);
    @Autowired    private CuratorFramework zk;
    public ZookeeperEventListener(){        log.info("开始实例化 zk类");    }
    @Autowired    private MyGateWayApplicationEvent applicationEvent;
    @PostConstruct    public void init() throws Exception {        register();        listenerRoute();    }
   private void listenerRoute() throws Exception {        PathChildrenCache pathChildrenCache = new PathChildrenCache(zk, GateWayContext.ROUTEPATH,true);
        pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {            @Override            public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception  {                log.info("监听到zk发生变化,对应类型为:{}",event.getType());                //如果发现节点变更,清楚当前路由,从新加载                PathChildrenCacheEvent.Type type = event.getType();                if(type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){                    applicationEvent.publish();                }            }        });       pathChildrenCache.start();
    }
    private void register() throws Exception {        log.info("开始向zk注册");        String ip = nacosDiscoveryProperties.getIp();        zk.create().creatingParentsIfNeeded()                .withMode(CreateMode.EPHEMERAL)                .forPath(GateWayContext.GATEWAYREGISTERPATH+ip+":31081");    }
}
public class MyGateWayApplicationEvent implements ApplicationEventPublisherAware {
    private Logger log= LoggerFactory.getLogger(MyGateWayApplicationEvent.class);
    private ApplicationEventPublisher applicationEventPublisher;
    @Override    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {        this.applicationEventPublisher=applicationEventPublisher;    }
    public void publish(){        log.info("监听到zk的route节点发生变更,开始发布刷新事件");        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));    }}

只是这里是全量刷新,其实我们可以自己编写事件Event来根据路由id更新固定的某个路由信息,发布的时候吧路由id携带过去即可(需要自定义事件RefreshRoutesEventById,自己实现监听刷新逻辑,我们这里就不实现了有兴趣的朋友可以自己尝试下)。

我们管理后台那边只需把数据在源端更新后再更新zk的固定节点即可,这样所有网关实例均可马上感知到,不过实际代码debug中发现spring cloud gateway会定时去全量刷新。所以这部分看个人需求,如果想要立刻生效的可以采用我这种实现方式。

我这里为了避免后台项目需要依赖spring cloud gateway 包所以存储的model采用自定义的,这样的话gateway哪里就需要转换下。​​​​​​​

public class RouteDefintionAssemble {
    //把传递进来的参数转换成路由对象    public static  RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {        RouteDefinition definition = new RouteDefinition();        definition.setId(gwdefinition.getId());        definition.setOrder(gwdefinition.getOrder());
        //设置断言        List<PredicateDefinition> pdList=new ArrayList<>();        List<GatewayPredicateDefinition> gatewayPredicateDefinitionList=gwdefinition.getPredicates();        for (GatewayPredicateDefinition gpDefinition: gatewayPredicateDefinitionList) {            PredicateDefinition predicate = new PredicateDefinition();            predicate.setArgs(gpDefinition.getArgs());            predicate.setName(gpDefinition.getName());            pdList.add(predicate);        }        definition.setPredicates(pdList);
        //设置过滤器        List<FilterDefinition> filters = new ArrayList();        List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();        for(GatewayFilterDefinition filterDefinition : gatewayFilters){            FilterDefinition filter = new FilterDefinition();            filter.setName(filterDefinition.getName());            filter.setArgs(filterDefinition.getArgs());            filters.add(filter);        }        definition.setFilters(filters);        URI uri = null;        if(gwdefinition.getUri().startsWith("http")){            uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();        }else{            // uri为 lb://consumer-service 时使用下面的方法            uri = URI.create(gwdefinition.getUri());        }        definition.setUri(uri);        return definition;    }}

这是我自己实现的3个model ​​​​​​​

public class GatewayFilterDefinition {
    //过滤器名称    private String name;
    // 路由规则    private Map<String,String> args=new LinkedHashMap<>();
    public String getName() {        return name;    }
    public void setName(String name) {        this.name = name;    }
    public Map<String, String> getArgs() {        return args;    }
    public void setArgs(Map<String, String> args) {        this.args = args;    }}
​​​​​​
public class GatewayRouteDefinition {
    //路由id    private String id;
    //路由断言集合配置    private List<GatewayPredicateDefinition> predicates=new ArrayList<>();
    //路由过滤器集合配置    private List<GatewayFilterDefinition> filters=new ArrayList<>();
    //转发目标uri    private String uri;
    //执行顺序    private int order=0;
    public String getId() {        return id;    }
    public void setId(String id) {        this.id = id;    }
    public List<GatewayPredicateDefinition> getPredicates() {        return predicates;    }
    public void setPredicates(List<GatewayPredicateDefinition> predicates) {        this.predicates = predicates;    }
    public List<GatewayFilterDefinition> getFilters() {        return filters;    }
    public void setFilters(List<GatewayFilterDefinition> filters) {        this.filters = filters;    }
    public String getUri() {        return uri;    }
    public void setUri(String uri) {        this.uri = uri;    }
    public int getOrder() {        return order;    }
    public void setOrder(int order) {        this.order = order;    }    @Override    public String toString() {        return JSON.toJSONStringWithDateFormat(this,"yyyy-MM-dd HH:mm:ss");    }}

​​​​​​​​​​​​​​

public class GatewayPredicateDefinition {
    //断言对应的name    private String  name;
    //配置 断言规则    private Map<String,String> args=new LinkedHashMap<>();
    public String getName() {        return name;    }
    public void setName(String name) {        this.name = name;    }
    public Map<String, String> getArgs() {        return args;    }
    public void setArgs(Map<String, String> args) {        this.args = args;    }}

总结

通过以上改造我们的网关就可以不重启的情况下,动态更改路由信息了,还可以基于此功能实现灰度发布,动态增加、减少转发功能,后端服务上线下也可以做到最业务0影响,上述代码已经上线1月目前运行稳定,如果有需要改进的地方望朋友们踊跃指出。

spring这个家族 真的很伟大,所有产品都那么优秀,高度抽象,提供各种各样的接口来供我们扩展,非常值得我们去学习。

spring cloud gateway 之 动态路由改造相关推荐

  1. 基于Nacos配置中心实现Spring Cloud Gateway的动态路由管理

    前面我们了解过了Sentinel 网关流量控制之Spring Cloud Gateway实战,今天带给大家是基于Nacos配置中心实现Spring Cloud Gateway的动态路由管理. 1.为什 ...

  2. 基于Nacos实现Spring Cloud Gateway实现动态路由

    简介 该文档主要介绍以Nacos为配置中心,实现Spring Cloud GateWay 实现动态路由的功能.Spring Cloud Gateway启动时候,就将路由配置和规则加载到内存里,无法做到 ...

  3. spring cloud gateway 之动态路由

    前面分别对 Spring Cloud Zuul 与 Spring Cloud Gateway 进行了简单的说明,它门是API网关,API网关负责服务请求路由.组合及协议转换,客户端的所有请求都首先经过 ...

  4. 使用Nacos实现Spring Cloud Zuul的动态路由

    一.前言 Nacos是阿里巴巴开源的致力于服务发现与管理.动态配置管理,以及动态DNS服务的中间件,目前已发布至0.5.0版本,除了与Spring Cloud更加紧密结合以外,还丧心病狂地支持JDK1 ...

  5. angularjs 让当前路由重新加载_Spring Cloud Gateway的动态路由怎样做?集成Nacos实现很简单...

    一.说明 网关的核心概念就是路由配置和路由规则,而作为所有请求流量的入口,在实际生产环境中为了保证高可靠和高可用,是尽量要避免重启的,所以实现动态路由是非常有必要的:本文主要介绍 Spring Clo ...

  6. 有什么办法动态更改yml的值吗_基于Redis实现Spring Cloud Gateway的动态管理

    转载本文需注明出处:微信公众号EAWorld,违者必究. 引言: Spring Cloud Gateway是当前使用非常广泛的一种API网关.它本身能力并不能完全满足企业对网关的期望,人们希望它可以提 ...

  7. Spring Cloud Gateway –配置简单路由

    可以将Spring Cloud Gateway视为Spring Cloud Netflix Zuul项目的后续产品,并有助于在微服务环境中实现Gateway模式 . 它建立在 Spring Boot ...

  8. Spring Cloud GateWay系列(三):路由规则动态刷新

    Spring Cloud Gateway旨在提供一种简单而有效的方式来路由API,并为它们提供横切关注点,例如:安全性.监控/指标和弹性.Route(路由)是网关的基本单元,由唯一标识符ID.目标地址 ...

  9. Nacos整合Gateway实现动态路由

    往期回顾 Nacos的安装与配置 Spring Cloud集成Nacos作为注册中心 LoadBalacer集成Nacos实现负载均衡 常见的负载均衡策略分析 Spring Cloud集成Dubbo实 ...

最新文章

  1. 14 个 Spring MVC 顶级技巧,随时用随时爽,一直用一直爽
  2. C++中初始化列表的使用
  3. 阿里云自定义监控tomcat进程数
  4. spring aop实例讲解_小实例理解Spring中的AOP----面向切面编程
  5. 改变numpy的大小_Numpy入门详细教程
  6. dstat系统监控工具配置与使用
  7. php部分---单文件上传的封装类
  8. [Java] 1006. Sign In and Sign Out (25)-PAT甲级
  9. TensorFlow总结(2020版)
  10. hibernate操作数据库
  11. oracle11 odac,oracle 11g odac 下载
  12. 平流干旱模型matlab,北半球干旱区降水转化和再循环特征及其在全球变暖背景下的变化...
  13. --save-dev 与 --save的区别
  14. Importing the numpy c-extensions failed 解决方案
  15. 显微镜下的大明内容_《显微镜下的大明》读后感1000字
  16. OSPF实验———Secondary Address
  17. 连接GitHub提示远程主机关闭连接
  18. jar构建docker镜像_将jar包打包为docker镜像
  19. opcache 缓存_缓存帽子戏法:Zend Opcache,Etags和查询缓存
  20. 关于深度学习移动化的思考

热门文章

  1. 手机也能用的在线pdf转换成word方法
  2. 常见的交换设备(中继器、集线器hub、网桥bridge、交换机switch)|计算机网络
  3. zoj3738 Buy the Pets(人、猫、狗 DP)
  4. 2023年工业自动化,机器人与控制工程国际会议(IARCE 2023)
  5. flutter GridView和Wrap
  6. 【广告算法工程师入门 16】机制设计-最优拍卖机制设计
  7. 主流 AI工具 发展历程全貌图
  8. 【计算机基础】静态链接与动态链接
  9. JavaScript continue
  10. 文件比较-合并工具粗略比较[zz]