閱讀程式碼深入原理14——Spring Cloud Netflix之Zuul

Zuul作為微服務閘道器,匯聚使用者端請求,根據路徑規則,轉發給後臺微服務。

由於@EnableZuulServer註解使ZuulServerAutoConfiguration自動配置類生效,但該配置類只配置了SendForwardFilter作為路由使用,而SendForwardFilter需要執行緒本地變數RequestContext存在“forward。to”,才會使用RequestDispatcher將請求轉發給servlet容器裡地當前應用地其它servlet/jsp/html,用途實在有限。

一般我們不用@EnableZuulServer註解,而是使用增強版的@EnableZuulProxy註解。

而@EnableZuulProxy註解使ZuulProxyAutoConfiguration自動配置類生效,該配置類繼承了ZuulServerAutoConfiguration,引入了RibbonCommandFactory,還額外定義了DiscoveryClientRouteLocator、PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter、ZuulDiscoveryRefreshListener、SimpleServiceRouteMapper等。

首先在ZuulServerAutoConfiguration定義了ZuulController作為spring mvc地handler,其次定義的ZuulHandlerMapping作為spring mvc的HandlerMapping,從RouteLocator裡獲取路徑與ZuulController關聯註冊到handlerMap,供後續查詢。

回到spring mvc處理過程,先是DispatcherServlet透過HandlerMapping獲取Handler,此處的HandlerMapping是ZuulServerAutoConfiguration定義的ZuulHandlerMapping,其getHandler會走到lookUpHandler,程式碼如下:

# spring-cloud-netflix-core-1。3。6。RELEASE。jar!/org。springframework。cloud。netflix。zuul。web。ZuulHandlerMapping protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { if (this。errorController != null && urlPath。equals(this。errorController。getErrorPath())) { return null; } String[] ignored = this。routeLocator。getIgnoredPaths()。toArray(new String[0]); // 1。 zuul。ignoredPattern預設未設定(見SimpleRouteLocator) if (PatternMatchUtils。simpleMatch(ignored, urlPath)) { return null; } RequestContext ctx = RequestContext。getCurrentContext(); if (ctx。containsKey(“forward。to”)) { // 2。 避免死迴圈 return null; } if (this。dirty) { synchronized (this) { if (this。dirty) { registerHandlers(); // 3。 關聯路徑和handler到handlerMap this。dirty = false; } } } return super。lookupHandler(urlPath, request); // 4。 從handlerMap匹配路徑返回handler } private void registerHandlers() { Collection routes = this。routeLocator。getRoutes(); //3。1 獲取路由資訊 if (routes。isEmpty()) { this。logger。warn(“No routes found from RouteLocator”); } else { for (Route route : routes) { registerHandler(route。getFullPath(), this。zuul); // 3。2 將路徑和handler關聯 } } }

接著詳細看下獲取路由這塊,ZuulServerAutoConfiguration定義了CompositeRouteLocator,它的功能由SimpleRouteLocator和DiscoveryClientRouteLocator來代理。

SimpleRouteLocator較為簡單,程式碼如下:

# spring-cloud-netflix-core-1。3。6。RELEASE。jar!/org。springframework。cloud。netflix。zuul。filters。SimpleRouteLocator @Override public List getRoutes() { if (this。routes。get() == null) { this。routes。set(locateRoutes()); } List values = new ArrayList<>(); for (String url : this。routes。get()。keySet()) { ZuulRoute route = this。routes。get()。get(url); String path = route。getPath(); values。add(getRoute(route, path)); } return values; } protected Map locateRoutes() { LinkedHashMap routesMap = new LinkedHashMap(); for (ZuulRoute route : this。properties。getRoutes()。values()) { // 1。 將“zuul。routes”配置作為路由資訊 routesMap。put(route。getPath(), route); } return routesMap; } 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()); // 2。 如果路徑和通用字首相同,且路由配置了去除字首,則目標路徑也去除字首 } 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, // 3。 以prefix和targetPath作為全路徑(fullpath)。location為url優先,其次serviceId retryable, route。isCustomSensitiveHeaders() ? route。getSensitiveHeaders() : null); }

然後再看下DiscoveryClientRouteLocator處理邏輯(重寫locateRoutes):

# spring-cloud-netflix-core-1。3。6。RELEASE。jar!/org。springframework。cloud。netflix。zuul。filters。discovery。DiscoveryClientRouteLocator protected LinkedHashMap locateRoutes() { LinkedHashMap routesMap = new LinkedHashMap(); routesMap。putAll(super。locateRoutes()); // 1。 仍以配置優先 if (this。discovery != null) { Map staticServices = new LinkedHashMap(); for (ZuulRoute route : routesMap。values()) { String serviceId = route。getServiceId(); if (serviceId == null) { serviceId = route。getId(); } if (serviceId != null) { staticServices。put(serviceId, route); // 2。 將路由的serviceId或id為key,route為value,放到staticServices } } // Add routes for discovery services by default List services = this。discovery。getServices(); // 3。 從eureka獲取服務名 String[] ignored = this。properties。getIgnoredServices() 。toArray(new String[0]); for (String serviceId : services) { // Ignore specifically ignored services and those that were manually // configured String key = “/” + mapRouteToService(serviceId) + “/**”; // 4。 以服務名作為路徑 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); // 5。 當配置中有服務,但沒有配置具體url時,且id/serviceId未配置時,用服務發現的服務名替換路由的location。(此處邏輯沒有場景可以進入?!) } } if (!PatternMatchUtils。simpleMatch(ignored, serviceId) && !routesMap。containsKey(key)) { // Not ignored routesMap。put(key, new ZuulRoute(key, serviceId)); // 6。 沒有配置明確忽略某個服務時,則作為路由項 } } } 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); } LinkedHashMap values = new LinkedHashMap<>(); for (Entry 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()); // 7。 確保path為“/”開頭,如果有通用字首,則路徑還需附件字首 } return values; }

即配置了路徑到某個服務,或者以服務名開頭的路徑能夠匹配到handler。

然後是HandlerAdapter是否匹配此Handler。此處應當由SimpleControllerHandlerAdapter(預設的HandlerAdapter之一)來處理,即handler本身實現了Controller介面,而ZuulController為ServletWrappingController子類,符合情況。

透過此HandlerAdapter執行handle,即SimpleControllerHandlerAdapter,此時觸發org。springframework。cloud。netflix。zuul。web。ZuulController的執行

# spring-cloud-netflix-core-1。3。6。RELEASE。jar!/org。springframework。cloud。netflix。zuul。web。ZuulController public ZuulController() { setServletClass(ZuulServlet。class); setServletName(“zuul”); setSupportedMethods((String[]) null); // Allow all } public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { try { // We don‘t care about the other features of the base class, just want to // handle the request return super。handleRequestInternal(request, response); // 1。 觸發ZuulServlet。service } finally { // @see com。netflix。zuul。context。ContextLifecycleFilter。doFilter RequestContext。getCurrentContext()。unset(); } }

ZuulServlet的執行比較簡單,程式碼如下:

# zuul-core-1。3。0。jar!/com。netflix。zuul。http。ZuulServlet public void service(javax。servlet。ServletRequest servletRequest, javax。servlet。ServletResponse servletResponse) throws ServletException, IOException { try { init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); // Marks this request as having passed through the “Zuul engine”, as opposed to servlets // explicitly bound in web。xml, for which requests will not have the same data attached RequestContext context = RequestContext。getCurrentContext(); context。setZuulEngineRan(); try { preRoute(); // 1。 執行ZuulRunner。preRoute,觸發FilterProcessor。preRoute,導致從FilterLoad獲取“pre”型別ZuulFilter,執行ZuulFilter。runFilter,如果IZuulFilter。shouldFilter為true,則執行IZuulFilter。run } catch (ZuulException e) { error(e); postRoute(); return; } try { route(); // 2。 同preRoute,只是FilterLoader獲取ZuulFilter的型別為“route” } catch (ZuulException e) { error(e); postRoute(); return; } try { postRoute(); // 3。 同preRoute,只是FilterLoader獲取ZuulFilter的型別為“post” } catch (ZuulException e) { error(e); return; } } catch (Throwable e) { error(new ZuulException(e, 500, “UNHANDLED_EXCEPTION_” + e。getClass()。getName())); // 4。 同preRoute,只是FilterLoader獲取ZuulFilter的型別為“error” } finally { RequestContext。getCurrentContext()。unset(); } } void preRoute() throws ZuulException { zuulRunner。preRoute(); }

上面講自動配置漏掉的一處是ZuulServerAutoConfiguration有定義ZuulFilterInitializer,此bean在初始化後會將所有ZuulFilter註冊到FilterRegistry,而FilterLoad的ZuulFilter正是來源於FilterRegistry。(FilterLoad、FilterRegistry以static方式提供單例物件)

回到zuul本身,PreDecorationFilter用於決定將請求交給哪個路由,程式碼如下:

# spring-cloud-netflix-core-1。3。6。RELEASE。jar!/org。springframework。cloud。netflix。zuul。filters。pre。PreDecorationFilter public boolean shouldFilter() { RequestContext ctx = RequestContext。getCurrentContext(); return !ctx。containsKey(FORWARD_TO_KEY) // a filter has already forwarded && !ctx。containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId } @Override public Object run() { RequestContext ctx = RequestContext。getCurrentContext(); final String requestURI = this。urlPathHelper。getPathWithinApplication(ctx。getRequest()); Route route = this。routeLocator。getMatchingRoute(requestURI); // 1。 去除servlet路徑,匹配route if (route != null) { String location = route。getLocation(); if (location != null) { // url或serviceId存在時 ctx。put(REQUEST_URI_KEY, route。getPath()); ctx。put(PROXY_KEY, route。getId()); 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()); } if (location。startsWith(HTTP_SCHEME+“:”) || location。startsWith(HTTPS_SCHEME+“:”)) { ctx。setRouteHost(getUrl(location)); // 2。 配置了“zuul。routes。{id}。url”,設定routeHost,路由交給SimpleHostRoutingFilter ctx。addOriginResponseHeader(SERVICE_HEADER, location); } else if (location。startsWith(FORWARD_LOCATION_PREFIX)) { // 3。 如果路徑以“forward”開頭,清除routeHost,路由交給SendForwardFilter 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); // 4。 配置了“zuul。routes。{id}。serviceId”,路由交給RibbonRoutingFilter ctx。setRouteHost(null); ctx。addOriginResponseHeader(SERVICE_ID_HEADER, location); } 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); } 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; }

無論是SimpleHostRoutingFilter使用apache httpClient直接發出請求,還是RibbonRoutingFilter服務發現做客戶端負載均衡,可以參考我之前的文章,此處不做更多涉及。