深入理解gateway动态路由(源码分析未结束)

2,142 阅读5分钟

背景

公司有个项目,有这样一个业务需求;需要根据某些业务点的具体的业务量,来新增微服务,而且只有这些特定的业务,会走这新增的微服务。gateway是一直在运行的,不可能增加一个新的服务,就更改配置重启服务,所以用到的动态路由方式。

大体的设计思路是这样的:gateway层添加新增、编辑、删除动态路由接口;另外一个门户服务,在新增或更新微服务后,就调用gateway层提供的接口,完成动态路由更新的功能。

问题及想法:

  1. 随之而来的是持久化问题,在调用gateway新增路由接口时,就会持久化路由到redis,但是动态路由是存在于内存中,一旦重启gateway服务,增加的动态路由就会消失;所以在gateway服务上添加了一个启动就会读取redis并生成动态路由的配置类;这样就可以保证就算是重启服务,路由也不会丢失。
  2. 另外一个问题是,当路由的过滤器进行修改,之前持久化redis中的却不会更新,所以决定在门户做一个重载接口,当修改过滤器或者路由配置时,可以触发重载接口,重载接口的作用是将持久化到redis的路由重新加载且放到gateway的内存中。

理解

什么动态路由

路由

gateway最主要的作用是,提供统一的入口,路由,鉴权,限流,熔断;这里的路由就是请求的转发,根据设定好的某些条件,比如断言,进行转发。

动态

动态的目的是让程序更加可以在运行的过程中兼容更多的业务场景。

项目的配置

简述

涉及到两个服务,一个是门户服务(作用是提供给运营人员管理入口--包括:管理路由、绑定路由),一个是网关服务(gateway组件,为门户服务提供:查询路由信息、添加路由、删除路由、编辑路由接口)

门户服务

提供出的接口

这几个都是比较简单的接口,其中绑定接口逻辑上稍微有些复杂,可以着重说一下。

解决问题过程

这个接口最主要的目的是生成新的路由,已知的有这么几个点需要注意:

如何配置过滤器

因为过滤器是一样的,只是路由的地址不同;需要获得过滤器的相关配置,并且放入到动态路由的对象中;有两个思路,一个是在代码中写死过滤器配置,更新过滤器的同时修改这块的代码(考虑到后期不便于项目维护,没有采用);第二种是先拿到配置文件中的过滤器配置,然后在放到生成的路由配置中(需要网关微服务提供一个查询当前路由配置的接口),采用此方式。

如何调用网关提供的接口

因为注册中心和网关微服务都是集群,不能保证集群的任何一台都是可靠的,所以需要从注册中心拿到网关微服务地址,代码如下:

    private static Pattern PATTERN_URL = Pattern.compile("<homePageUrl>(.+?)</homePageUrl>");
    private static final String REPLACE_VALUE = "eureka/";
    /**
     * 从eureka地址中解析出所有的access地址
     *
     * @param urlSuffix eureka存放服务信息的地址
     * @return
     */
    private List<String> getAccessUrlByEureka(String urlSuffix) {
        log.info(LogFormat.formatMsg("AbilityGroupService.getAccessUrlByEureka", "", "req=" + JSON.toJSONString(urlSuffix)));
        List<String> list = new ArrayList<>();
        String[] eurekaSplit = eurekaArray.split(",");
        Map<String, String> header = new HashMap<>();
        for (String s : eurekaSplit) {
            String eurekaUrl = s.replace(REPLACE_VALUE, "");
            try {
                HttpUtil.HTTPResult httpResult = HttpUtil.get(eurekaUrl + urlSuffix, null, header);
                if (200 != httpResult.getCode()) {
                    continue;
                }
                String data = httpResult.getData();
                if (Strings.isEmpty(data)) {
                    continue;
                }

                //解析出所有的网关微服务地址
                Matcher matcher = PATTERN_URL.matcher(data);
                while (matcher.find()) {
                    String homepage = matcher.group(1).trim();
                    list.add(homepage);
                }
                break;

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        log.info(LogFormat.formatMsg("AbilityGroupService.getAccessUrlByEureka", "", "resp=" + JSON.toJSONString(list)));
        return list;
    }

拿出所有的网关微服务的地址之后,轮询调用这些服务(可以做一个重试机制),保证路由都可以下发到。

网关服务

提供的接口

都是对路由配置进行操作的接口,包括查询所有路由,添加路由,更新路由,删除路由接口。

重要的几个方法

可以看到这个控制器注入了两个实例,一个是RouteDefinitionLocator,这个具体的实现放到后面说,可以看到这里的作用是拿到服务运行时所有的路由的列表;另一个是DynamicRouteServiceImpl,这个类是我参考网上的博客写的,可以看一下,具体的实现。

DynamicRouteServiceImpl

    @Resource
    private RouteDefinitionWriter routeDefinitionWriter;
    @Resource
    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        publisher = applicationEventPublisher;
    }

    /**
     * 动态增加路由
     *
     * @param gatewayRouteDefinition
     */
    public void add(GatewayRouteDefinition gatewayRouteDefinition) {
        RouteDefinition routeDefinition = assembleRouteDefinition(gatewayRouteDefinition);
        log.info(LogFormat.formatMsg("DynamicRouteServiceImpl.add", "", "routeDefinition=" + JSON.toJSONString(routeDefinition)));
        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        publisher.publishEvent(new RefreshRoutesEvent(this));
    }

assembleRouteDefinition

    //把传递进来的参数转换成路由对象
    public 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;
    }

小结

到这里,其实整个开发过程就结束了,功能都已经实现,就剩下测试和优化了;但是作为一个爱钻研的技术人,我们应该有打破砂锅问到底的精神;我们来分析一下调用的这个类上都有什么东西。

源码分析

可以看到DynamicRouteServiceImpl这个类并没有多少东西,主要是调用的RouteDefinitionWriter提供的两个方法savedelete,我们就可以跟着到这个类的包下,先看看类之间的关系。

UML关系图

包名:org.springframework.cloud.gateway.route