springcloud zuul 网关 持久化 动态加载路由的思路分析

在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   实现 即可实现  动态刷新逻辑

第四个 SimpleRouteLocator    可以发现 第二个 继承了此定位器  说明 这个是一个基础的实现   基于所有的配置

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读取 放入 SimpleRouteLocator     再注入到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 路由已刷新");
    }

}

猜你喜欢

转载自my.oschina.net/u/2948566/blog/1592370