先说明两个概念:路由配置和路由规则,路由配置是指配置某请求路径路由到指定的目的地址;路由规则是指匹配到路由配置之后,再进行自定义的规则判断,规则判断可以更改路由目的地址
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.所以使用时尽量让至少一个规则匹配,避免给用户带来不好的体验
目前已知的问题
- 通过配置规则目前只能达到分流到指定服务,但不能细化到指定服务的某些节点
- 由于spring容易监听了HeartbeatEvent事件会触发刷新加载路由配置,并且多次触发就会多次刷新,所以实际刷新的时间可能与自己配置的刷新时间不一致,并且会导致多次重复刷新