在springcloud 最新的版本已经有了自己的gateway组件
目前世面上都是基于netflix 出品 zuul 的 gateway 一般我们在生产上 都希望能将路由动态化 持久化 做动态管理
基本设想思路 通过后台页面来管理路由 然后 刷新配置 本文将探索一下如何进行 zuul 路由的数据库持久化 动态化 建议从github上下载spring-cloud-netflix源码
根据一些教程很容易知道 配置路由映射 通过
zuul.routes.<key>.path=/foo/**
zuul.routes.<key>.service-id= 服务实例名称
zuul.routes.<key>.url=http://xxxoo
来设置 由此我们可以找到一个 ZuulProperties 的zuul配置类 ,从中我们发现有个属性
/**
* Map of route names to properties.
*/
private Map<String, ZuulRoute> routes = new LinkedHashMap<>();
从名字上看 知道是路由集合 , 而且有对应的set方法 ,经过对配置元数据json的解读 确认 这就 是 装载路由的容器 我们可以在 ZuulProperties 初始化的时候 将路由装载到容器中 那么 ZuulRoute 又是个什么玩意儿呢:
public static class ZuulRoute {
/**
* 路由的唯一编号 同时也默认为 装载路由的容器的Key 用来标识映射的唯一性 重要.
*/
private String id;
/**
* 路由的规则 /foo/**.
*/
private String path;
/**
* 服务实例ID(如果有的话)来映射到此路由 你可以指定一个服务或者url 但是不能两者同时对于一个key来配置
*
*/
private String serviceId;
/**
* 就是上面提到的url
*
*/
private String url;
/**
* 路由前缀是否在转发开始前被删除 默认是删除
* 举个例子 你实例的实际调用是http://localhost:8002/user/info
* 如果你路由设置该实例对应的path 为 /api/v1/** 那么 通过路由调用
* http://ip:port/api/v1/user/info
* 当为true 转发到 http://localhost:8002/user/info
* 当为false 转发到 http://localhost:8002//api/v1user/info
*/
private boolean stripPrefix = true;
/**
* 是否支持重试如果支持的话 通常需要服务实例id 跟ribbon
*
*/
private Boolean retryable;
/**
* 不传递到下游请求的敏感标头列表。默认为“安全”的头集,通常包含用户凭证。如果下游服务是与代理相同的系统的一
* 部分,那么将它们从列表中删除就可以了,因此它们共享身份验证数据。如果在自己的域之外使用物理URL,那么通常来
* 说泄露用户凭证是一个坏主意
*/
private Set<String> sensitiveHeaders = new LinkedHashSet<>();
/**
* 上述列表sensitiveHeaders 是否生效 默认不生效
*/
private boolean customSensitiveHeaders = false;
上面这些就是我们需要进行 持久化 的东西
你可以用你知道的持久化方式 来实现 当然 这些可以加入缓存来减少IO提高性能 这里只说一个思路具体自己可以实现
当持久化完成后 我们如何让网关来刷新这些配置呢 每一次的curd 能迅速生效呢
这个就要 路由加载的机制和原理 路由是由路由定位器来 从配置中 获取路由表 进行匹配的
package org.springframework.cloud.netflix.zuul.filters;
import java.util.Collection;
import java.util.List;
/**
* @author Dave Syer
*/
public interface RouteLocator {
/**
* Ignored route paths (or patterns), if any.
*/
Collection<String> getIgnoredPaths();
/**
* 获取路由表.
*/
List<Route> getRoutes();
/**
* 将路径映射到具有完整元数据的实际路由.
*/
Route getMatchingRoute(String path);
}
实现有这么几个:
第一个 复合定位器 CompositeRouteLocator
public class CompositeRouteLocator implements RefreshableRouteLocator {
private final Collection<? extends RouteLocator> routeLocators;
private ArrayList<RouteLocator> rl;
public CompositeRouteLocator(Collection<? extends RouteLocator> routeLocators) {
Assert.notNull(routeLocators, "'routeLocators' must not be null");
rl = new ArrayList<>(routeLocators);
AnnotationAwareOrderComparator.sort(rl);
this.routeLocators = rl;
}
@Override
public Collection<String> getIgnoredPaths() {
List<String> ignoredPaths = new ArrayList<>();
for (RouteLocator locator : routeLocators) {
ignoredPaths.addAll(locator.getIgnoredPaths());
}
return ignoredPaths;
}
@Override
public List<Route> getRoutes() {
List<Route> route = new ArrayList<>();
for (RouteLocator locator : routeLocators) {
route.addAll(locator.getRoutes());
}
return route;
}
@Override
public Route getMatchingRoute(String path) {
for (RouteLocator locator : routeLocators) {
Route route = locator.getMatchingRoute(path);
if (route != null) {
return route;
}
}
return null;
}
@Override
public void refresh() {
for (RouteLocator locator : routeLocators) {
if (locator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) locator).refresh();
}
}
}
}
这是一个既可以刷新 又可以定位的定位器 作用 可以将一个到多个定位器转换成 可刷新的定位器
看构造 传入路由定位器集合 然后 进行了排序 赋值 同时实现了 路由定位器的方法 跟刷新方法
我们刷新 可以根据将定位器 放入这个容器进行转换
第二个 DiscoveryClientRouteLocator 是组合 静态 以及配置好的路由 跟一个服务发现实例 而且有优先权
第三个 **RefreshableRouteLocator **实现 即可实现 动态刷新逻辑
第四个 **Simple****RouteLocator ** 可以发现 第二个 继承了此定位器 说明 这个是一个基础的实现 基于所有的配置
public class SimpleRouteLocator implements RouteLocator, Ordered {
private static final Log log = LogFactory.getLog(SimpleRouteLocator.class);
private static final int DEFAULT_ORDER = 0;
private ZuulProperties properties;
private PathMatcher pathMatcher = new AntPathMatcher();
private String dispatcherServletPath = "/";
private String zuulServletPath;
private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
private int order = DEFAULT_ORDER;
public SimpleRouteLocator(String servletPath, ZuulProperties properties) {
this.properties = properties;
if (StringUtils.hasText(servletPath)) {
this.dispatcherServletPath = servletPath;
}
this.zuulServletPath = properties.getServletPath();
}
@Override
public List<Route> getRoutes() {
List<Route> values = new ArrayList<>();
for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
ZuulRoute route = entry.getValue();
String path = route.getPath();
values.add(getRoute(route, path));
}
return values;
}
@Override
public Collection<String> getIgnoredPaths() {
return this.properties.getIgnoredPatterns();
}
@Override
public Route getMatchingRoute(final String path) {
return getSimpleMatchingRoute(path);
}
protected Map<String, ZuulRoute> getRoutesMap() {
if (this.routes.get() == null) {
this.routes.set(locateRoutes());
}
return this.routes.get();
}
protected Route getSimpleMatchingRoute(final String path) {
if (log.isDebugEnabled()) {
log.debug("Finding route for path: " + path);
}
// This is called for the initialization done in getRoutesMap()
getRoutesMap();
if (log.isDebugEnabled()) {
log.debug("servletPath=" + this.dispatcherServletPath);
log.debug("zuulServletPath=" + this.zuulServletPath);
log.debug("RequestUtils.isDispatcherServletRequest()="
+ RequestUtils.isDispatcherServletRequest());
log.debug("RequestUtils.isZuulServletRequest()="
+ RequestUtils.isZuulServletRequest());
}
String adjustedPath = adjustPath(path);
ZuulRoute route = getZuulRoute(adjustedPath);
return getRoute(route, adjustedPath);
}
protected ZuulRoute getZuulRoute(String adjustedPath) {
if (!matchesIgnoredPatterns(adjustedPath)) {
for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
String pattern = entry.getKey();
log.debug("Matching pattern:" + pattern);
if (this.pathMatcher.match(pattern, adjustedPath)) {
return entry.getValue();
}
}
}
return null;
}
protected Route getRoute(ZuulRoute route, String path) {
if (route == null) {
return null;
}
if (log.isDebugEnabled()) {
log.debug("route matched=" + route);
}
String targetPath = path;
String prefix = this.properties.getPrefix();
if (path.startsWith(prefix) && this.properties.isStripPrefix()) {
targetPath = path.substring(prefix.length());
}
if (route.isStripPrefix()) {
int index = route.getPath().indexOf("*") - 1;
if (index > 0) {
String routePrefix = route.getPath().substring(0, index);
targetPath = targetPath.replaceFirst(routePrefix, "");
prefix = prefix + routePrefix;
}
}
Boolean retryable = this.properties.getRetryable();
if (route.getRetryable() != null) {
retryable = route.getRetryable();
}
return new Route(route.getId(), targetPath, route.getLocation(), prefix,
retryable,
route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null,
route.isStripPrefix());
}
/**
* Calculate all the routes and set up a cache for the values. Subclasses can call
* this method if they need to implement {@link RefreshableRouteLocator}.
*/
protected void doRefresh() {
this.routes.set(locateRoutes());
}
/**
* Compute a map of path pattern to route. The default is just a static map from the
* {@link ZuulProperties}, but subclasses can add dynamic calculations.
*/
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}
protected boolean matchesIgnoredPatterns(String path) {
for (String pattern : this.properties.getIgnoredPatterns()) {
log.debug("Matching ignored pattern:" + pattern);
if (this.pathMatcher.match(pattern, path)) {
log.debug("Path " + path + " matches ignored pattern " + pattern);
return true;
}
}
return false;
}
private String adjustPath(final String path) {
String adjustedPath = path;
if (RequestUtils.isDispatcherServletRequest()
&& StringUtils.hasText(this.dispatcherServletPath)) {
if (!this.dispatcherServletPath.equals("/")) {
adjustedPath = path.substring(this.dispatcherServletPath.length());
log.debug("Stripped dispatcherServletPath");
}
}
else if (RequestUtils.isZuulServletRequest()) {
if (StringUtils.hasText(this.zuulServletPath)
&& !this.zuulServletPath.equals("/")) {
adjustedPath = path.substring(this.zuulServletPath.length());
log.debug("Stripped zuulServletPath");
}
}
else {
// do nothing
}
log.debug("adjustedPath=" + adjustedPath);
return adjustedPath;
}
@Override
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
}
我们可以从 第一跟第四个下功夫
将配置从DB读取 放入 Simple****RouteLocator ** 再注入到CompositeRouteLocator**
刷新的核心类 :
package org.springframework.cloud.netflix.zuul;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEvent;
/**
* @author Dave Syer
*/
@SuppressWarnings("serial")
public class RoutesRefreshedEvent extends ApplicationEvent {
private RouteLocator locator;
public RoutesRefreshedEvent(RouteLocator locator) {
super(locator);
this.locator = locator;
}
public RouteLocator getLocator() {
return this.locator;
}
}
基于 事件 我们只要写一个监听器 来监听 就OK了 具体 自行实现
这是我自己实现的 目前多实例情况下还不清楚是否会广播 如果不能广播 可参考config 刷新的思路来解决
@Configuration
public class ZuulConfig {
@Resource
private IRouterService routerService;
@Resource
private ServerProperties serverProperties;
/**
* 将数据库的网关配置数据 写入配置
*
* @return the zuul properties
*/
@Bean
public ZuulProperties zuulProperties() {
ZuulProperties zuulProperties = new ZuulProperties();
zuulProperties.setRoutes(routerService.initRoutersFromDB());
return zuulProperties;
}
/**
* 将配置写入可刷新的路由定位器.
*
* @param zuulProperties the zuul properties
* @return the composite route locator
*/
@Bean
@ConditionalOnBean(ZuulProperties.class)
public CompositeRouteLocator compositeRouteLocator(@Qualifier("zuulProperties") ZuulProperties zuulProperties) {
List<RouteLocator> routeLocators = new ArrayList<>();
RouteLocator simpleRouteLocator = new SimpleRouteLocator(serverProperties.getServletPrefix(), zuulProperties);
routeLocators.add(simpleRouteLocator);
return new CompositeRouteLocator(routeLocators);
}
}
路由刷新器:
@Service
public class ZuulRefresherImpl implements ZuulRefresher {
private static final Logger log = LoggerFactory.getLogger(ZuulRefresher.class);
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Resource
private IRouterService iRouterService;
@Resource
private ServerProperties serverProperties;
@Resource
private ZuulProperties zuulProperties;
@Resource
private CompositeRouteLocator compositeRouteLocator;
@Override
public void refreshRoutes() {
zuulProperties.setRoutes(iRouterService.initRoutersFromDB());
List<RouteLocator> routeLocators = new ArrayList<>();
RouteLocator simpleRouteLocator = new SimpleRouteLocator(serverProperties.getServletPrefix(), zuulProperties);
routeLocators.add(simpleRouteLocator);
compositeRouteLocator = new CompositeRouteLocator(routeLocators);
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(compositeRouteLocator);
applicationEventPublisher.publishEvent(routesRefreshedEvent);
log.info("zuul 路由已刷新");
}
}