先说明两个概念:路由配置和路由规则,路由配置是指配置某请求路径路由到指定的目的地址;路由规则是指匹配到路由配置之后,再进行自定义的规则判断,规则判断可以更改路由目的地址

zuul默认的路由都是在properties里配置的,如果需要动态路由,需要自己实现,由上面的源码分析可以看出,实现动态路由需要实现可刷新的路由定位器接口(RefreshableRouteLocator),并可以继承默认的实现(SimpleRouteLocator)再进行扩展

实现动态路由主要关注两个方法

  • protected Map<String, ZuulRoute> locateRoutes():此方法是加载路由配置的,父类中是获取properties中的路由配置,可以通过扩展此方法,达到动态获取配置的目的
  • public Route getMatchingRoute(String path):此方法是根据访问路径,获取匹配的路由配置,父类中已经匹配到路由,可以通过路由id查找自定义配置的路由规则,以达到根据自定义规则动态分流的效果

为了实现针对不同存储方式的动态路由,定义抽象类实现基本的功能,代码如下

package com.itopener.zuul.route.spring.boot.common;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.itopener.zuul.route.spring.boot.common.rule.IZuulRouteRule;
import com.itopener.zuul.route.spring.boot.common.rule.IZuulRouteRuleMatcher;
 
public abstract class ZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
    public final static Logger logger = LoggerFactory.getLogger(ZuulRouteLocator.class);
     
    private ZuulProperties properties;
     
    @Autowired
    private IZuulRouteRuleMatcher zuulRouteRuleMatcher;
     
    public ZuulRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.properties = properties;
        logger.info("servletPath:{}", servletPath);
    }
     
    /**
     * @description 刷新路由配置
     * @author fuwei.deng
     * @date 2017年7月3日 下午6:04:42
     * @version 1.0.0
     * @return
     */
    @Override
    public void refresh() {
        doRefresh();
    }
     
    @Override
    protected Map<String, ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
        // 从application.properties中加载路由信息
        // routesMap.putAll(super.locateRoutes());
        // 加载路由配置
        routesMap.putAll(loadLocateRoute());
        // 优化一下配置
        LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
        for (Map.Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            // Prepend with slash if not already present.
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.properties.getPrefix())) {
                path = this.properties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, entry.getValue());
        }
        return values;
    }
     
    /**
     * @description 加载路由配置,由子类去实现
     * @author fuwei.deng
     * @date 2017年7月3日 下午6:04:42
     * @version 1.0.0
     * @return
     */
    public abstract Map<String, ZuulRoute> loadLocateRoute();
     
    /**
     * @description 获取路由规则,由子类去实现
     * @author fuwei.deng
     * @date 2017年7月3日 下午6:04:42
     * @version 1.0.0
     * @return
     */
    public abstract List<IZuulRouteRule> getRules(Route route);
     
    /**
     * @description 通过配置的规则改变路由目的地址
     * @author fuwei.deng
     * @date 2017年7月3日 下午6:04:42
     * @version 1.0.0
     * @return
     */
    @Override
    public Route getMatchingRoute(String path) {
        Route route = super.getMatchingRoute(path);
        // 增加自定义路由规则判断
        List<IZuulRouteRule> rules = getRules(route);
        return zuulRouteRuleMatcher.matchingRule(route, rules);
    }
     
    /**
     * @description 路由定位器的优先级
     * @author fuwei.deng
     * @date 2017年7月3日 下午6:04:42
     * @version 1.0.0
     * @return
     */
    @Override
    public int getOrder() {
        return -1;
    }
     
    /**
     * @description 存储路由的entity转换为zuul需要的ZuulRoute
     * @author fuwei.deng
     * @date 2017年7月3日 下午6:19:40
     * @version 1.0.0
     * @param locateRouteList
     * @return
     */
    public Map<String, ZuulRoute> handle(List<ZuulRouteEntity> locateRouteList){
        if(CollectionUtils.isEmpty(locateRouteList)){
            return null;
        }
        Map<String, ZuulRoute> routes = new LinkedHashMap<>();
        for (ZuulRouteEntity locateRoute : locateRouteList) {
            if (StringUtils.isEmpty(locateRoute.getPath())
                    || !locateRoute.isEnable()
                    || (StringUtils.isEmpty(locateRoute.getUrl()) && StringUtils.isEmpty(locateRoute.getServiceId()))) {
                continue;
            }
            ZuulRoute zuulRoute = new ZuulRoute();
            try {
                zuulRoute.setCustomSensitiveHeaders(locateRoute.isCustomSensitiveHeaders());
                zuulRoute.setSensitiveHeaders(locateRoute.getSensitiveHeadersSet());
                zuulRoute.setId(locateRoute.getId());
//              zuulRoute.setLocation("");
                zuulRoute.setPath(locateRoute.getPath());
                zuulRoute.setRetryable(locateRoute.isRetryable());
                zuulRoute.setServiceId(locateRoute.getServiceId());
                zuulRoute.setStripPrefix(locateRoute.isStripPrefix());
                zuulRoute.setUrl(locateRoute.getUrl());
                logger.info("add zuul route: " + JSON.toJSONString(zuulRoute));
            } catch (Exception e) {
                logger.error("=============load zuul route info with error==============", e);
            }
            routes.put(zuulRoute.getPath(), zuulRoute);
        }
        return routes;
    }
}

然后定义子类实现路由配置和路由规则的获取,如存储在Zookeeper

package com.itopener.zuul.route.zk.spring.boot.autoconfigure;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.itopener.zuul.route.spring.boot.common.ZuulRouteEntity;
import com.itopener.zuul.route.spring.boot.common.ZuulRouteLocator;
import com.itopener.zuul.route.spring.boot.common.ZuulRouteRuleEntity;
import com.itopener.zuul.route.spring.boot.common.rule.IZuulRouteRule;
/**
 * @author fuwei.deng
 * @date 2017年6月30日 上午11:11:19
 * @version 1.0.0
 */
public class ZuulRouteZookeeperLocator extends ZuulRouteLocator {
    public final static Logger logger = LoggerFactory.getLogger(ZuulRouteZookeeperLocator.class);
    @Autowired
    private CuratorFrameworkClient curatorFrameworkClient;
 
    private List<ZuulRouteEntity> locateRouteList;
    public ZuulRouteZookeeperLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
    }
    @Override
    public Map<String, ZuulRoute> loadLocateRoute() {
        locateRouteList = new ArrayList<ZuulRouteEntity>();
        try {
            locateRouteList = new ArrayList<ZuulRouteEntity>();
            //获取所有路由配置的id
            List<String> keys = curatorFrameworkClient.getChildrenKeys("/");
            //遍历获取所有路由配置
            for(String item : keys){
                String value = curatorFrameworkClient.get("/" + item);
                if(!StringUtils.isEmpty(value)){
                    ZuulRouteEntity route = JSON.parseObject(value, ZuulRouteEntity.class);
                    //只需要启用的路由配置
                    if(!route.isEnable()){
                        continue;
                    }
                    route.setRuleList(new ArrayList<IZuulRouteRule>());
                    //获取路由配置对应的所有路由规则的ID
                    List<String> ruleKeys = curatorFrameworkClient.getChildrenKeys("/" + item);
                    //遍历获取所有的路由规则
                    for(String ruleKey : ruleKeys){
                        String ruleStr = curatorFrameworkClient.get("/" + item + "/" + ruleKey);
                        if(!StringUtils.isEmpty(ruleStr)){
                            ZuulRouteRuleEntity rule = JSON.parseObject(ruleStr, ZuulRouteRuleEntity.class);
                            //只保留可用的路由规则
                            if(!rule.isEnable()){
                                continue;
                            }
                            route.getRuleList().add(rule);
                        }
                    }
                    locateRouteList.add(route);
                }
            }
        } catch (Exception e) {
            logger.error("load zuul route from zk exception", e);
        }
        logger.info("load zuul route from zk : " + JSON.toJSONString(locateRouteList));
        return handle(locateRouteList);
    }
    @Override
    public List<IZuulRouteRule> getRules(Route route) {
        if(CollectionUtils.isEmpty(locateRouteList)){
            return null;
        }
        for(ZuulRouteEntity item : locateRouteList){
            if(item.getId().equals(route.getId())){
                return item.getRuleList();
            }
        }
        return null;
    }
}

以上为封装zuul动态路由的主要代码,完整代码见附件,附件代码包含使用db、zk、redis存储路由配置和路由规则,同时也包含管理页面、示例代码

使用方法和达到的效果

在配置路由的应用里引入对应的starter(引入一个即可)

<!-- zookeeper -->
<dependency>
    <groupId>com.itopener</groupId>
    <artifactId>zuul-route-zk-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>
<!-- db-->
<dependency>
    <groupId>com.itopener</groupId>
    <artifactId>zuul-route-db-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>
<!-- redis-->
<dependency>
    <groupId>com.itopener</groupId>
    <artifactId>zuul-route-redis-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>

配置对应的数据源

  • zk:spring.zuul.route.zk.serverLists
  • db:正常配置数据源DataSource
  • redis:正常配置redis(RedisTemplate)

启动zuul-route-admin,进入管理页面,配置路由和路由规则(可以只配置路由,如果对应的路由规则为空则不进行规则判断),路由规则是js语法,内置obj对象,可以直接通过obj取request里的参数,比如

spring gateway动态路由 spring cloud zuul 动态路由_java

路由配置页面如下,配置的字段与properties配置的一致

spring gateway动态路由 spring cloud zuul 动态路由_json_02

路由和规则可以禁用、启用、删除等,也可以切换数据源查看

spring gateway动态路由 spring cloud zuul 动态路由_数据库_03

spring gateway动态路由 spring cloud zuul 动态路由_java_04

路由配置和规则配置的刷新

  • 路由配置和规则配置后,本机可以直接调用刷新方法,但是考虑到路由网关一般也会多节点部署,所以没有直接调用刷新方法
  • zk可以监听数据变化,如果是使用zk存储,修改数据之后,各节点会自动刷新
  • redis和db没有监听的方法,所以需要配置自动刷新的时间,spring.zuul.route.refreshCron,默认值是:0/30 * * * * ?(每30秒刷新一次)

达到的效果

  • 如需根据时间配置分流(如之前的66活动根据上下午时间分流到不同的应用),可以配置对应的规则,规则内容为:new Date().getHours()>12?'true':'false',规则期望结果:true,规则匹配后即可路由到对应的路由目的地址(蓝色文字为admin管理页面对应的字段)
  • 如需根据参数name分流,可以配置对应的规则:obj.name == 'honey'?'1':'2',然后配置对应的期望结果和路由目的地址
  • 如果所有的规则都没有匹配,会返回404.所以使用时尽量让至少一个规则匹配,避免给用户带来不好的体验

目前已知的问题

  • 通过配置规则目前只能达到分流到指定服务,但不能细化到指定服务的某些节点
  • 由于spring容易监听了HeartbeatEvent事件会触发刷新加载路由配置,并且多次触发就会多次刷新,所以实际刷新的时间可能与自己配置的刷新时间不一致,并且会导致多次重复刷新