先说明两个概念:路由配置和路由规则,路由配置是指配置某请求路径路由到指定的目的地址;路由规则是指匹配到路由配置之后,再进行自定义的规则判断,规则判断可以更改路由目的地址
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里的参数,比如
路由配置页面如下,配置的字段与properties配置的一致
路由和规则可以禁用、启用、删除等,也可以切换数据源查看
路由配置和规则配置的刷新
-
路由配置和规则配置后,本机可以直接调用刷新方法,但是考虑到路由网关一般也会多节点部署,所以没有直接调用刷新方法
-
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.所以使用时尽量让至少一个规则匹配,避免给用户带来不好的体验
目前已知的问题