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

Java框架

浏览数:31

2020-6-1

AD:资源代下载服务

在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 路由已刷新");
    }

}

       

作者:码农小胖哥