目录

  1. gateway本地文件常规路由配置
  2. 本地文件配置对业务造成的痛点
  3. 动态路由改造

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

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

  • Route(路由):路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发。
  • Predicate(谓语、断言):路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:PathQueryMethodHeader等。
  • Filter(过滤器):过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容。
     

整体架构:

gateway 修改 Attributes queryparams body formdata gateway动态修改路由_java

我们本地文件配置路由信息的时候都是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.RouteDefinitionWriter
org.springframework.cloud.gateway.route.RouteDefinitionLocator
org.springframework.cloud.gateway.route.RouteDefinitionRepository
org.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) 
//删除路由配置信息根据路由id
Mono<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包如图

gateway 修改 Attributes queryparams body formdata gateway动态修改路由_redis_02

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

 

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

 

gateway 修改 Attributes queryparams body formdata gateway动态修改路由_网关_03

 

那我们来看下我们如何用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这个家族 真的很伟大,所有产品都那么优秀,高度抽象,提供各种各样的接口来供我们扩展,非常值得我们去学习。