首先,引入 spring-cloud-starter-zuul 以後會間接引入:spring
hystrix依賴已經引入,那麼何種狀況下使用hystrix呢?sql
在Zuul的自動配置類 ZuulServerAutoConfiguration 和 ZuulProxyAutoConfiguration 中總共會向Spring容器注入3個Zuul的RouteFilter,分別是架構
SimpleHostRoutingFilter 簡單路由,經過HttpClient向預約的URL發送請求 生效條件: RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse() 一、RequestContext中的routeHost不爲空,routeHost就是URL,即便用URL直連 二、RequestContext中的sendZuulResponse爲true,便是否將response發送給客戶端,默認爲true併發
RibbonRoutingFilter 使用Ribbon、Hystrix和可插入的http客戶端發送請求 生效條件: (RequestContext.getRouteHost() == null && RequestContext.get(SERVICE_ID_KEY) != null && RequestContext.sendZuulResponse()) 一、RequestContext中的routeHost爲空,即URL爲空 二、RequestContext中的serviceId不爲空 三、RequestContext中的sendZuulResponse爲true,便是否將response發送給客戶端,默認爲true分佈式
SendForwardFilter forward到本地URL 生效條件: RequestContext.containsKey(FORWARD_TO_KEY) && !RequestContext.getBoolean(SEND_FORWARD_FILTER_RAN, false) 一、RequestContext中包含FORWARD_TO_KEY,即URL使用 forward: 映射 二、RequestContext中SEND_FORWARD_FILTER_RAN爲false,SEND_FORWARD_FILTER_RAN意爲「send forward是否運行過了」,在SendForwardFilter#run()時會 ctx.set(SEND_FORWARD_FILTER_RAN, true)ide
綜上所述,在使用serviceId映射的方法路由轉發的時候,會使用Ribbon+Hystrix高併發
而哪一種路由配置方式是「URL映射」,哪一種配置方式又是「serviceId映射」呢?性能
Zuul有一個前置過濾器 PreDecorationFilter 用於經過 RouteLocator路由定位器 決定在什麼時候以何種方式路由轉發學習
RouteLocator是用於經過請求地址匹配到Route路由的,以後 PreDecorationFilter 再經過Route信息設置RequestContext上下文,決定後續使用哪一個RouteFilter作路由轉發this
因此就引出如下問題:
什麼是Route
RouteLocator路由定位器如何根據請求路徑匹配路由
匹配到路由後,PreDecorationFilter如何設置RequestContext請求上下文
什麼是Route
我總共見到兩個和Route相關的類
ZuulProperties.ZuulRoute ,用於和zuul配置文件關聯,保存相關信息
org.springframework.cloud.netflix.zuul.filters.Route , RouteLocator找到的路由信息就是這個類,用於路由轉發
public static class ZuulRoute { private String id; //ZuulRoute的id private String path; //路由的pattern,如 /foo/** private String serviceId; //要映射到此路由的服務id private String url; //要映射到路由的完整物理URL private boolean stripPrefix = true; //用於肯定在轉發以前是否應剝離此路由前綴的標誌位 private Boolean retryable; //此路由是否能夠重試,一般重試須要serviceId和ribbon private Set<String> sensitiveHeaders = new LinkedHashSet(); //不會傳遞給下游請求的敏感標頭列表 private boolean customSensitiveHeaders = false; //是否自定義了敏感頭列表 }
public class Route { private String id; private String fullPath; private String path; private String location; //多是 url 或 serviceId private String prefix; private Boolean retryable; private Set<String> sensitiveHeaders = new LinkedHashSet<>(); private boolean customSensitiveHeaders; }
能夠看到 org.springframework.cloud.netflix.zuul.filters.Route 和 ZuulProperties.ZuulRoute 基本一致,只是Route用於路由轉發定位的屬性location根據不一樣的狀況,多是一個具體的URL,多是一個serviceId
RouteLocator路由定位器如何根據請求路徑匹配路由
Zuul在自動配置加載時注入了2個RouteLocator
CompositeRouteLocator : 組合的RouteLocator,在 getMatchingRoute() 時會依次調用其它的RouteLocator,先找到先返回;CompositeRouteLocator的routeLocators集合中只有DiscoveryClientRouteLocator
DiscoveryClientRouteLocator : 能夠將靜態的、已配置的路由與來自DiscoveryClient服務發現的路由組合在一塊兒,來自DiscoveryClient的路由優先;SimpleRouteLocator的子類(SimpleRouteLocator 基於加載到 ZuulProperties 中的配置定位Route路由信息)
其中CompositeRouteLocator是 @Primary 的,它是組合多個RouteLocator的Locator,其 getMatchingRoute() 方法會分別調用其它全部RouteLocator的getMatchingRoute()方法,經過請求路徑匹配路由信息,只要匹配到了就立刻返回
默認CompositeRouteLocator混合路由定位器的routeLocators只有一個DiscoveryClientRouteLocator,故只需分析 DiscoveryClientRouteLocator#getMatchingRoute(path)
//----------DiscoveryClientRouteLocator是SimpleRouteLocator子類,實際上是調用的SimpleRouteLocator##getMatchingRoute(path) @Override public Route getMatchingRoute(final String path) { return getSimpleMatchingRoute(path); } protected Route getSimpleMatchingRoute(final String path) { if (log.isDebugEnabled()) { log.debug("Finding route for path: " + path); } // routes是保存路由信息的map,若是此時還未加載,調用locateRoutes() if (this.routes.get() == null) { this.routes.set(locateRoutes()); } 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()); } /** * 下面的方法主要是先對path作微調 * 再根據path到routes中匹配到ZuulRoute * 最後根據 ZuulRoute 和 adjustedPath 生成 Route */ String adjustedPath = adjustPath(path); ZuulRoute route = getZuulRoute(adjustedPath); return getRoute(route, adjustedPath); }
下面咱們來看看 locateRoutes() 是如何加載靜態的、已配置的路由與來自DiscoveryClient服務發現的路由的
//----------DiscoveryClientRouteLocator#locateRoutes() 服務發現路由定位器的locateRoutes() @Override protected LinkedHashMap<String, ZuulRoute> locateRoutes() { //保存ZuulRoute的LinkedHashMap LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>(); //調用父類SimpleRouteLocator#locateRoutes() //加載ZuulProperties中的全部配置文件中的路由信息 routesMap.putAll(super.locateRoutes()); //若是服務發現客戶端discovery存在 if (this.discovery != null) { //將routesMap已經存在的配置文件中的ZuulRoute放入staticServices<serviceId, ZuulRoute> Map<String, ZuulRoute> staticServices = new LinkedHashMap<String, ZuulRoute>(); for (ZuulRoute route : routesMap.values()) { String serviceId = route.getServiceId(); //若是serviceId爲null,以id做爲serviceId,此狀況適合 zuul.routes.xxxx=/xxxx/** 的狀況 if (serviceId == null) { serviceId = route.getId(); } if (serviceId != null) { staticServices.put(serviceId, route); } } // Add routes for discovery services by default List<String> services = this.discovery.getServices(); //到註冊中心找到全部service String[] ignored = this.properties.getIgnoredServices() .toArray(new String[0]); //遍歷services for (String serviceId : services) { // Ignore specifically ignored services and those that were manually // configured String key = "/" + mapRouteToService(serviceId) + "/**"; //若是註冊中心的serviceId在staticServices集合中,而且此路由沒有配置URL //那麼,更新路由的location爲serviceId if (staticServices.containsKey(serviceId) && staticServices.get(serviceId).getUrl() == null) { // Explicitly configured with no URL, cannot be ignored // all static routes are already in routesMap // Update location using serviceId if location is null ZuulRoute staticRoute = staticServices.get(serviceId); if (!StringUtils.hasText(staticRoute.getLocation())) { staticRoute.setLocation(serviceId); } } //若是註冊中心的serviceId不在忽略範圍內,且routesMap中尚未包含,添加到routesMap if (!PatternMatchUtils.simpleMatch(ignored, serviceId) && !routesMap.containsKey(key)) { // Not ignored routesMap.put(key, new ZuulRoute(key, serviceId)); } } } // 若是routesMap中有 /** 的默認路由配置 if (routesMap.get(DEFAULT_ROUTE) != null) { ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE); // Move the defaultServiceId to the end routesMap.remove(DEFAULT_ROUTE); routesMap.put(DEFAULT_ROUTE, defaultRoute); } //將routesMap中的數據微調後,放到values<String, ZuulRoute>,返回 LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>(); for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) { String path = entry.getKey(); // Prepend with slash if not already present. if (!path.startsWith("/")) { path = "/" + path; } if (StringUtils.hasText(this.properties.getPrefix())) { path = this.properties.getPrefix() + path; if (!path.startsWith("/")) { path = "/" + path; } } values.put(path, entry.getValue()); } return values; }
此方法運行後就已經加載了配置文件中全部路由信息,以及註冊中心中的服務路由信息,有的經過URL路由,有的經過serviceId路由
只需根據本次請求的requestURI與 路由的pattern匹配找到對應的路由
匹配到路由後,PreDecorationFilter如何設置RequestContext請求上下文
//----------PreDecorationFilter前置過濾器 @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest()); Route route = this.routeLocator.getMatchingRoute(requestURI); //找到匹配的路由 //----------------到上面爲止是已經分析過的,根據requestURI找到匹配的Route信息 // ==== 匹配到路由信息 if (route != null) { String location = route.getLocation(); if (location != null) { ctx.put(REQUEST_URI_KEY, route.getPath());//RequestContext設置 requestURI:路由的pattern路徑 ctx.put(PROXY_KEY, route.getId());//RequestContext設置 proxy:路由id //設置須要忽略的敏感頭信息,要麼用全局默認的,要麼用路由自定義的 if (!route.isCustomSensitiveHeaders()) { this.proxyRequestHelper .addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0])); } else { this.proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0])); } //設置重試信息 if (route.getRetryable() != null) { ctx.put(RETRYABLE_KEY, route.getRetryable()); } //若是location是 http/https開頭的,RequestContext設置 routeHost:URL //若是location是 forward:開頭的,RequestContext設置 forward信息、routeHost:null //其它 RequestContext設置 serviceId、routeHost:null、X-Zuul-ServiceId if (location.startsWith(HTTP_SCHEME+":") || location.startsWith(HTTPS_SCHEME+":")) { ctx.setRouteHost(getUrl(location)); ctx.addOriginResponseHeader(SERVICE_HEADER, location); } else if (location.startsWith(FORWARD_LOCATION_PREFIX)) { ctx.set(FORWARD_TO_KEY, StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath())); ctx.setRouteHost(null); return null; } else { // set serviceId for use in filters.route.RibbonRequest ctx.set(SERVICE_ID_KEY, location); ctx.setRouteHost(null); ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location); } //是否添加代理頭信息 X-Forwarded-For if (this.properties.isAddProxyHeaders()) { addProxyHeaders(ctx, route); String xforwardedfor = ctx.getRequest().getHeader(X_FORWARDED_FOR_HEADER); String remoteAddr = ctx.getRequest().getRemoteAddr(); if (xforwardedfor == null) { xforwardedfor = remoteAddr; } else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates xforwardedfor += ", " + remoteAddr; } ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor); } //是否添加Host頭信息 if (this.properties.isAddHostHeader()) { ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest())); } } } // ==== 沒有匹配到路由信息 else { log.warn("No route found for uri: " + requestURI); String fallBackUri = requestURI; String fallbackPrefix = this.dispatcherServletPath; // default fallback // servlet is // DispatcherServlet if (RequestUtils.isZuulServletRequest()) { // remove the Zuul servletPath from the requestUri log.debug("zuulServletPath=" + this.properties.getServletPath()); fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), ""); log.debug("Replaced Zuul servlet path:" + fallBackUri); } else { // remove the DispatcherServlet servletPath from the requestUri log.debug("dispatcherServletPath=" + this.dispatcherServletPath); fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, ""); log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri); } if (!fallBackUri.startsWith("/")) { fallBackUri = "/" + fallBackUri; } String forwardURI = fallbackPrefix + fallBackUri; forwardURI = forwardURI.replaceAll("//", "/"); ctx.set(FORWARD_TO_KEY, forwardURI); } return null; }
總結:
只要引入了spring-cloud-starter-zuul就會間接引入Ribbon、Hystrix
路由信息多是從配置文件中加載的,也多是經過DiscoveryClient從註冊中心加載的
zuul是經過前置過濾器PreDecorationFilter找到與當前requestURI匹配的路由信息,並在RequestContext中設置相關屬性的,後續的Route Filter會根據RequestContext中的這些屬性判斷如何路由轉發
Route Filter主要使用 SimpleHostRoutingFilter 和 RibbonRoutingFilter
當RequestContext請求上下文中存在routeHost,即URL直連信息時,使用SimpleHostRoutingFilter簡單Host路由
當RequestContext請求上下文中存在serviceId,即服務id時(可能會與註冊中心關聯獲取服務列表,或者讀取配置文件中serviceId.ribbon.listOfServers的服務列表),使用RibbonRoutingFilter,會使用Ribbon、Hystrix
歡迎工做一到五年的Java工程師朋友們加入Java架構開發: 855835163
羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!