logo头像

From zero to HERO

springcloud zuul网关持久化思路分析

本文于 1353 天之前发表,文中内容可能已经过时。

springcloud 最新的版本已经有了自己的 gateway 组件。

目前世面上都是基于netflix 出品 zuulgateway。 一般我们在生产上都希望能将路由动态化、持久化, 做动态管理。

基本设想思路 通过后台页面来管理路由 然后刷新配置 本文将探索一下如何进行 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 路由已刷新");
    }

}

评论系统未开启,无法评论!