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也類似。
在這裡主要針對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路徑的值。