在springcloud 最新的版本已經有了本身的gateway組件 java
目前世面上都是基於netflix 出品 zuul 的 gateway 通常咱們在生產上 都但願能將路由動態化 持久化 作動態管理git
基本設想思路 經過後臺頁面來管理路由 而後 刷新配置 本文將探索一下如何進行 zuul 路由的數據庫持久化 動態化 建議從github上下載spring-cloud-netflix源碼 github
根據一些教程很容易知道 配置路由映射 經過spring
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 又是個什麼玩意兒呢:json
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;
上面這些就是咱們須要進行 持久化 的東西api
你能夠用你知道的持久化方式 來實現 固然 這些能夠加入緩存來減小IO提升性能 這裏只說一個思路具體本身能夠實現緩存
當持久化完成後 咱們如何讓網關來刷新這些配置呢 每一次的curd 能迅速生效呢安全
這個就要 路由加載的機制和原理 路由是由路由定位器來 從配置中 獲取路由表 進行匹配的app
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 路由已刷新"); } }