1. 程式人生 > >zuul實現所有介面對於帶指定字首和不帶字首的url均能相容訪問

zuul實現所有介面對於帶指定字首和不帶字首的url均能相容訪問

我們的專案裡通過zuul實現路由轉發,前幾日接到這麼一個需求,需要實現所有介面對於帶指定字首和不帶字首的url均能相容訪問,網上這方面的文件並不多,因此為了處理這個需求,捎帶著閱讀了一下zuul的部分原始碼 首先說一下結論,zuul本身便實現了這個功能,對於帶/zuul的字首的url會自動去掉該字首進行轉發,完美匹配這次需求。 接著開始理解原始碼,看一看zuul是怎麼實現的。 在springboot專案中使用zuul需要使用@EnableZuulServer或@EnableZuulProxy中的至少一個註解。其中@EnableZuulServer對應ZuulServerAutoConfiguration,@EnableZuulProxy對應ZuulProxyAutoConfiguration,ZuulServerAutoConfiguration是ZuulProxyAutoConfiguration的父類,因此可以簡單理解成@EnableZuulServer是@EnableZuulProxy的一個簡化版本。 如下圖:EnableZuulServer --> ZuulServerMarkerConfiguration --> ZuulServerAutoConfiguration,EnableZuulProxy也類似。 EnableZuulServer

ZuulServerMarkerConfiguration ZuulServerAutoConfiguration

在這裡主要針對EnableZuulProxy展開,我們開一下zuul是怎麼針對請求的url進行處理的。 在ZuulProxyAutoConfiguration類中,我們注入了PreDecorationFilter進行攔截。

// pre filters
@Bean
public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
	return new PreDecorationFilter(routeLocator, this.server.getServlet().getServletPrefix(), this.zuulProperties,
			proxyRequestHelper);
}

進入這個類我們可以看到該filter的型別是pre,如果已經處理過轉發邏輯的請求在不在攔截處理。

@Override
public int filterOrder() {
	return PRE_DECORATION_FILTER_ORDER; //5
}

@Override
public String filterType() {
	return PRE_TYPE;
}

@Override
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
}

接下來我們進入run方法仔細瞭解PreDecorationFilter的攔截邏輯。

@Override
public Object run() {
	//1.根據請求的url找到對應的路由Route
	RequestContext ctx = RequestContext.getCurrentContext();
	final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
	Route route = this.routeLocator.getMatchingRoute(requestURI);
	//2.根據Route進行相應的轉發
	if (route != null) {
		String location = route.getLocation();
		if (location != null) {
			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));
				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);
			}
			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()));
			}
		}
	}
	//3.Route為null,進行相應的fallback處理
	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;
}

其中Route route = this.routeLocator.getMatchingRoute(requestURI);是根據url路徑獲取路由的核心邏輯,我們繼續跟蹤,routeLocator是一個介面,對於getMatchingRoute方法有兩個實現類SimpleRouteLocator和CompositeRouteLocator,其中CompositeRouteLocator是遍歷routeLocator.getMatchingRoute方法,因此我們進入SimpleRouteLocator類的getMatchingRoute方法

//CompositeRouteLocator
@Override
public Route getMatchingRoute(String path) {
	for (RouteLocator locator : routeLocators) {
		Route route = locator.getMatchingRoute(path);
		if (route != null) {
			return route;
		}
	}
	return null;
}
//SimpleRouteLocator
@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);
	}

	// This is called for the initialization done in getRoutesMap()
	//1.獲取ZuulRoute的對映對
	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());
	}

	//2.對url路徑預處理
	String adjustedPath = adjustPath(path);
	
	//3.根據路徑獲取匹配的ZuulRoute
	ZuulRoute route = getZuulRoute(adjustedPath);
	
	//4.根據ZuulRoute組裝Route
	return getRoute(route, adjustedPath);
}

我們可以看到SimpleRouteLocator提供了protected型別的getSimpleMatchingRoute可以留給子類進行擴充套件,如果有需要,我們可以定義一個SimpleRouteLocator的子類自定義這部分的邏輯。 getRoutesMap();是這個類的一個核心方法方法,讀取我們的路由配置組成對映並儲存線上程本地變數中,我們留待下一篇在展開。 String adjustedPath = adjustPath(path);方法隊url路徑進行了預處理,是我們今天的重點,也是過濾/zuul字首的邏輯所在。

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;
}

這裡有兩個個判斷分支內都對url路徑進行了擷取,而且擷取的都是字串的前面一部分,這時候我們應該想到這兒和我們的需求有匹配之處,而這兩個判斷條件都用到RequestUtils類,我們繼續進入

public class RequestUtils {

	/**
	 * @deprecated use {@link org.springframework.cloud.netflix.zuul.filters.support.FilterConstants#IS_DISPATCHER_SERVLET_REQUEST_KEY}
	 */
	@Deprecated
	public static final String IS_DISPATCHERSERVLETREQUEST = IS_DISPATCHER_SERVLET_REQUEST_KEY;
	
	public static boolean isDispatcherServletRequest() {
		return RequestContext.getCurrentContext().getBoolean(IS_DISPATCHER_SERVLET_REQUEST_KEY);
	}
	
	public static boolean isZuulServletRequest() {
		//extra check for dispatcher since ZuulServlet can run from ZuulController
		return !isDispatcherServletRequest() && RequestContext.getCurrentContext().getZuulEngineRan();
	}	
}

繼續進入RequestContext,可以發現RequestContext實際上是一個繼承了ConcurrentHashMap<String, Object>的對映對。判斷上述兩個判斷條件是否成立的方法實際上就是判斷“isDispatcherServletRequest”和“zuulEngineRan”這兩個key值對應的value是否為true。 因此,我們上述的程式碼對url的請求路徑進行預處理的邏輯是: 1.如果isDispatcherServletRequest對應的value值為true,並且路徑中包含dispatcherServletPath,直接擷取。 2.步驟1不成立,且zuulEngineRan對應的value為true,並且路徑中包含zuulServletPath,直接擷取。 3.上訴步驟都不成立,不處理。 其中我們一開始所說的解決方案對應的就是步驟2,zuul對應zuulServletPath。 那麼dispatcherServletPath和zuulServletPath在哪裡設定呢?在ZuulServerAutoConfiguration中我們生成並注入了SimpleRouteLocator類的例項。

```
@Bean
@ConditionalOnMissingBean(SimpleRouteLocator.class)
public SimpleRouteLocator simpleRouteLocator() {
	return new SimpleRouteLocator(this.server.getServlet().getServletPrefix(),
			this.zuulProperties);
}
```
//SimpleRouteLocator的構造方法
public SimpleRouteLocator(String servletPath, ZuulProperties properties) {
	this.properties = properties;
	if (StringUtils.hasText(servletPath)) {
		this.dispatcherServletPath = servletPath;
	}

	this.zuulServletPath = properties.getServletPath();
}

在ZuulProperties類中servletPath預設值為/zuul,當然我們可以通過配置zuul.servletPath進行修改。 dispatcherServletPath也同理,在ServerProperties的內部類Servlet的path屬性獲得,

/**
 * Path to install Zuul as a servlet (not part of Spring MVC). The servlet is more
 * memory efficient for requests with large bodies, e.g. file uploads.
 */
private String servletPath = "/zuul";
public String getServletPrefix() {
	String result = this.path;
	int index = result.indexOf('*');
	if (index != -1) {
		result = result.substring(0, index);
	}
	if (result.endsWith("/")) {
		result = result.substring(0, result.length() - 1);
	}
	return result;
}

上面我們知道了怎麼獲取到servletPath和dispatcherServletPath的值,那麼zuul在上面時候上面情況下設定RequestContext中對應的兩個屬性值是否為true呢?

在ZuulServerAutoConfiguration還注入了一個pre型別且order為-3的ZuulFilter :ServletDetectionFilter,它是最早執行的ZuulFilter,對所有請求生效。從它的的名字我們就可以看出它的主要作用是檢測當前請求是通過Spring的DispatcherServlet處理執行,還是通過ZuulServlet來處理執行的。

@Override
public Object run() {
	RequestContext ctx = RequestContext.getCurrentContext();
	HttpServletRequest request = ctx.getRequest();
	if (!(request instanceof HttpServletRequestWrapper) 
			&& isDispatcherServletRequest(request)) {
		ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
	} else {
		ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
	}

	return null;
}

ServletDetectionFilter設定了isDispatcherServletRequest屬性,而ZuulServlet類通過context.setZuulEngineRan();設定了“zuulEngineRan”屬性。

@Override
   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();
           } catch (ZuulException e) {
               error(e);
               postRoute();
               return;
           }
           try {
               route();
           } catch (ZuulException e) {
               error(e);
               postRoute();
               return;
           }
           try {
               postRoute();
           } catch (ZuulException e) {
               error(e);
               return;
           }

       } catch (Throwable e) {
           error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
       } finally {
           RequestContext.getCurrentContext().unset();
       }
   }

最後,我們明確一點: 在SpringMvc中,我們將請求交由DispatcherServlet進行處理。 而使用了zuul後,對於/zuul字首的url會交由ZuulServlet進行處理。 事實上,這兩個字首也對應我們前面說SimpleRouteLocator的dispatcherServletPath和servletPath屬性。

除了帶/zuul能實現我們一開始的需求以外,如果需要類似的需要在路由轉發中對路徑進行處理的邏輯,根據上面的分析,我們也可以通過下面幾種方式實現: 1.定義一個繼承SimpleRouteLocator的子類並注入spring,重寫getSimpleMatchingRoute實現自己的自定義邏輯實現路徑的預處理。 2.定義一個實現ZuulFilter的類並注入spring,要求filterType為pre,並且order小於PreDecorationFilter的order(5),獲取到請求路徑後進行處理,其餘邏輯可以模仿PreDecorationFilter。 3.定義一個實現ZuulFilter的類並注入spring,要求filterType為pre,並且order大於PreDecorationFilter的order(5),對經過PreDecorationFilter處理後的請求再次攔截,修改RequestContext中的“requestURI”和“proxy” 對應的值,這兩者對應route的path和id,確定了轉換後url路徑的值。