在使用SpringCloud的時候準備使用Zuul作為微服務的網關,Zuul的默認路由方式主要是兩種,一種是在配置 文件裏直接指定靜態路由,另一種是根據註冊在Eureka的服務名自動匹配。比如如果有一個名為service1的服 務,通過 domain.com/service1/xxx 就能訪問到這個服務。但是這和我預想的需求還是有些差距。 網上有許多有關動態路由的實現方法,大致思想是不從Eureka拉取註冊服務信息,而是在資料庫裏自己維護一 份路由表,定時讀取資料庫拉取路由,來實現自動更新。而我的需求更進一步,我希望對外暴露的網關介面是 一個固定的url,如domain.com/gateway ,然後根據一個頭信息service來指定想要訪問的服務,而不是在 url後面拼接服務名。同時我也不想將我註冊到Eureka的服務名直接暴露在api中,而是做一層映射,讓我可以 靈活指定serivce名。

例如:

  • 訪問 domain.com/gateway ,headers包含service=service.a,調用後端service1
  • 訪問 domain.com/gateway ,headers包含service=service.b,調用後端service2

現在研究一下Zuul的源碼來看看怎麼實現這個功能。

我們都知道在Springboot啟動類上加一個@EnableZuulProxy就啟用了Zuul。從這個註解點進去看看:

@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

它引入了ZuulProxyMarkerConfiguration這個配置,進去看下。

/**
* Responsible for adding in a marker bean to trigger activation of
* {@link ZuulProxyAutoConfiguration}.
*
* @author Biju Kunjummen
*/

@Configuration
public class ZuulProxyMarkerConfiguration {

@Bean
public Marker zuulProxyMarkerBean() {
return new Marker();
}

class Marker {

}

}

從注釋中看到這個是用於激活ZuulProxyAutoConfiguration,看看這個類

@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
//...
}

這個配置下面註冊了很多組件,不過先暫時不看,它同時繼承自ZuulServerAutoConfiguration,看看這個類:

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ ZuulServlet.class, ZuulServletFilter.class })
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
// FIXME @Import(ServerPropertiesAutoConfiguration.class)
public class ZuulServerAutoConfiguration {

@Autowired
protected ZuulProperties zuulProperties;

@Autowired
protected ServerProperties server;

@Autowired(required = false)
private ErrorController errorController;

private Map<String, CorsConfiguration> corsConfigurations;

@Autowired(required = false)
private List<WebMvcConfigurer> configurers = emptyList();

@Bean
public HasFeatures zuulFeature() {
return HasFeatures.namedFeature("Zuul (Simple)",
ZuulServerAutoConfiguration.class);
}

@Bean
@Primary
public CompositeRouteLocator primaryRouteLocator(
Collection<RouteLocator> routeLocators) {
return new CompositeRouteLocator(routeLocators);
}

@Bean
@ConditionalOnMissingBean(SimpleRouteLocator.class)
public SimpleRouteLocator simpleRouteLocator() {
return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
this.zuulProperties);
}

@Bean
public ZuulController zuulController() {
return new ZuulController();
}

@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
mapping.setErrorController(this.errorController);
mapping.setCorsConfigurations(getCorsConfigurations());
return mapping;
}

protected final Map<String, CorsConfiguration> getCorsConfigurations() {
if (this.corsConfigurations == null) {
ZuulCorsRegistry registry = new ZuulCorsRegistry();
this.configurers.forEach(configurer -> configurer.addCorsMappings(registry));
this.corsConfigurations = registry.getCorsConfigurations();
}
return this.corsConfigurations;
}

@Bean
public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
return new ZuulRefreshListener();
}

@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true)
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(
new ZuulServlet(), this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesnt
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}

@Bean
@ConditionalOnMissingBean(name = "zuulServletFilter")
@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "true", matchIfMissing = false)
public FilterRegistrationBean zuulServletFilter() {
final FilterRegistrationBean<ZuulServletFilter> filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setUrlPatterns(
Collections.singleton(this.zuulProperties.getServletPattern()));
filterRegistration.setFilter(new ZuulServletFilter());
filterRegistration.setOrder(Ordered.LOWEST_PRECEDENCE);
// The whole point of exposing this servlet is to provide a route that doesnt
// buffer requests.
filterRegistration.addInitParameter("buffer-requests", "false");
return filterRegistration;
}

// pre filters

@Bean
public ServletDetectionFilter servletDetectionFilter() {
return new ServletDetectionFilter();
}

@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}

@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}

@Bean
public Servlet30WrapperFilter servlet30WrapperFilter() {
return new Servlet30WrapperFilter();
}

// post filters

@Bean
public SendResponseFilter sendResponseFilter(ZuulProperties properties) {
return new SendResponseFilter(zuulProperties);
}

@Bean
public SendErrorFilter sendErrorFilter() {
return new SendErrorFilter();
}

@Bean
public SendForwardFilter sendForwardFilter() {
return new SendForwardFilter();
}

@Bean
@ConditionalOnProperty("zuul.ribbon.eager-load.enabled")
public ZuulRouteApplicationContextInitializer zuulRoutesApplicationContextInitiazer(
SpringClientFactory springClientFactory) {
return new ZuulRouteApplicationContextInitializer(springClientFactory,
zuulProperties);
}

@Configuration
protected static class ZuulFilterConfiguration {

@Autowired
private Map<String, ZuulFilter> filters;

@Bean
public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory,
TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory,
filterLoader, filterRegistry);
}

}

@Configuration
@ConditionalOnClass(MeterRegistry.class)
protected static class ZuulCounterFactoryConfiguration {

@Bean
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnMissingBean(CounterFactory.class)
public CounterFactory counterFactory(MeterRegistry meterRegistry) {
return new DefaultCounterFactory(meterRegistry);
}

}

@Configuration
protected static class ZuulMetricsConfiguration {

@Bean
@ConditionalOnMissingClass("io.micrometer.core.instrument.MeterRegistry")
@ConditionalOnMissingBean(CounterFactory.class)
public CounterFactory counterFactory() {
return new EmptyCounterFactory();
}

@ConditionalOnMissingBean(TracerFactory.class)
@Bean
public TracerFactory tracerFactory() {
return new EmptyTracerFactory();
}

}

private static class ZuulRefreshListener
implements ApplicationListener<ApplicationEvent> {

@Autowired
private ZuulHandlerMapping zuulHandlerMapping;

private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof RoutesRefreshedEvent
|| event instanceof InstanceRegisteredEvent) {
reset();
}
else if (event instanceof ParentHeartbeatEvent) {
ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
else if (event instanceof HeartbeatEvent) {
HeartbeatEvent e = (HeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
}

private void resetIfNeeded(Object value) {
if (this.heartbeatMonitor.update(value)) {
reset();
}
}

private void reset() {
this.zuulHandlerMapping.setDirty(true);
}

}

private static class ZuulCorsRegistry extends CorsRegistry {

@Override
protected Map<String, CorsConfiguration> getCorsConfigurations() {
return super.getCorsConfigurations();
}

}

}

這個配置類裏註冊了很多bean:

  • SimpleRouteLocator:默認的路由定位器,主要負責維護配置文件中的路由配置。
  • DiscoveryClientRouteLocator:繼承自SimpleRouteLocator,該類會將配置文件中的靜態路由配置以及服務發現(比如eureka)中的路由信息進行合併,主要是靠它路由到具體服務。
  • CompositeRouteLocator:組合路由定位器,看入參就知道應該是會保存好多個RouteLocator,構造過程中其實僅包括一個DiscoveryClientRouteLocator。
  • ZuulController:Zuul創建的一個Controller,用於將請求交由ZuulServlet處理。
  • ZuulHandlerMapping:這個會添加到SpringMvc的HandlerMapping鏈中,只有選擇了ZuulHandlerMapping的請求才能出發到Zuul的後續流程。

還有一些其他的Filter,不一一看了。

其中,ZuulServlet是整個流程的核心,請求的過程是具體這樣的,當Zuulservlet收到請求後, 會創建一個ZuulRunner對象,該對象中初始化了RequestContext:作為存儲整個請求的一些數據,並被所有的Zuulfilter共享。ZuulRunner中還有一個 FilterProcessor,FilterProcessor作為執行所有的Zuulfilter的管理器。FilterProcessor從filterloader 中獲取zuulfilter,而zuulfilter是被filterFileManager所 載入,並支持groovy熱載入,採用了輪詢的方式熱載入。有了這些filter之後,zuulservelet首先執行的Pre類型的過濾器,再執行route類型的過濾器, 最後執行的是post 類型的過濾器,如果在執行這些過濾器有錯誤的時候則會執行error類型的過濾器。執行完這些過濾器,最終將請求的結果返回給客戶端。 RequestContext就是會一直跟著整個請求週期的上下文對象,filters之間有什麼信息需要傳遞就set一些值進去就行了。

有個示例圖可以幫助理解一下:

ZuulServlet中的service方法:

@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 {
//執行pre階段的filters
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
//執行route階段的filters
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
//執行post階段的filters
postRoute();
} catch (ZuulException e) {
error(e);
return;
}

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

可以順帶說明一下ZuulFilter,他包括4個基本特徵:過濾類型、執行順序、執行條件、具體操作。

String filterType();

int filterOrder();

boolean shouldFilter();

Object run();

它們各自的含義與功能總結如下:

  • filterType:該函數需要返回一個字元串來代表過濾器的類型,而這個類型就是在HTTP請求過程中定義的各個階段。在Zuul中默認定義了四種不同生命週期的過濾器類型,具體如下:
  • pre:可以在請求被路由之前調用。
  • routing:在路由請求時候被調用。
  • post:在routing和error過濾器之後被調用。
  • error:處理請求時發生錯誤時被調用。
  • filterOrder:通過int值來定義過濾器的執行順序,數值越小優先順序越高。
  • shouldFilter:返回一個boolean類型來判斷該過濾器是否要執行。我們可以通過此方法來指定過濾器的有效範圍。
  • run:過濾器的具體邏輯。在該函數中,我們可以實現自定義的過濾邏輯,來確定是否要攔截當前的請求,不對其進行後續的路由,或是在請求路由返回結果之後,對處理結果做一些加工等。

下圖源自Zuul的官方WIKI中關於請求生命週期的圖解,它描述了一個HTTP請求到達API網關之後,如何在各個不同類型的過濾器之間流轉的詳細過程。

Zuul默認實現了一批過濾器,如下:

|過濾器 |order |描述 |類型 |:---|:---:|:---:|---:| |ServletDetectionFilter| -3| 檢測請求是用 DispatcherServlet還是 ZuulServlet| pre| |Servlet30WrapperFilter| -2| 在Servlet 3.0 下,包裝 requests| pre| |FormBodyWrapperFilter| -1| 解析表單數據| pre| |SendErrorFilter| 0| 如果中途出現錯誤| error| |DebugFilter| 1| 設置請求過程是否開啟debug| pre| |PreDecorationFilter| 5| 根據uri決定調用哪一個route過濾器| pre| |RibbonRoutingFilter| 10| 如果寫配置的時候用ServiceId則用這個route過濾器,該過濾器可以用Ribbon 做負載均衡,用hystrix做熔斷| route| |SimpleHostRoutingFilter| 100| 如果寫配置的時候用url則用這個route過濾| route| |SendForwardFilter| 500| 用RequestDispatcher請求轉發| route| |SendResponseFilter| 1000| 用RequestDispatcher請求轉發| post|

回到我的需求,我不需要靜態配置,所有請求都是調用在eureka註冊的服務,所以每次請求都要在route階段轉到RibbonRoutingFilter,由它使用Ribbon向其它服務發起請求,因此看一下這個類的shouldFilter()方法:

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}

原來進入這個Filter的條件是RequestContext中getRouteHost為空且ctx.get(SERVICE_ID_KEY)不為空,即serviceId有值! 那麼Zuul在默認情況下是怎麼選擇route階段的Filter的呢?看到上面的pre階段有一個PreDecorationFilter,這個類主要就是根據uri來給RequestContext添加不同的內容來控制之後走哪個route過濾器。 看下它的Run方法:

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper
.getPathWithinApplication(ctx.getRequest());
//已經包含的路由配置裏是否有能匹配到的route
Route route = this.routeLocator.getMatchingRoute(requestURI);
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());
}
//根據各種情況設置context
//http:開頭的
if (location.startsWith(HTTP_SCHEME + ":")
|| location.startsWith(HTTPS_SCHEME + ":")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
//forward:開頭的
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;
}
//這裡設置了serviceId,走Ribbon
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()));
}
}
}
else {
log.warn("No route found for uri: " + requestURI);
String forwardURI = getForwardUri(requestURI);
//都不滿足的話,設置一個forward.to,走SendForwardFilter
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}

情況比較複雜,實際根據我的需求,我只要讓route階段時候使用RibbonRoutingFilter,因此我只要保證進入route階段時RequestContext裏包含對應服務的serviceId就行了。 我可以在pre階段將請求頭內的service轉化為所需要的服務serviceId,設置到context內,同時移除context中其它有影響的值就行了。

聽上去挺簡單的,我們自定義一個pre階段的Filter。

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public class HeaderPreDecorationFilter extends ZuulFilter {

private static final Log log = LogFactory.getLog(HeaderPreDecorationFilter.class);
private RouteLocator routeLocator;
private UrlPathHelper urlPathHelper = new UrlPathHelper();

private Map<String, Service> serviceMap = new HashMap();

public HeaderPreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath) {
this.routeLocator = routeLocator;
//舉個小例子,假如我在後端有一個名為platform-server的服務,服務內有一個/mwd/client/test的介面
serviceMap.put("mwd.service.test", new Service("platform-server", "/mwd/client/test"));
}

public String filterType() {
return "pre";
}

public int filterOrder() {
return 6;
}

public boolean shouldFilter() {
return true;
}

public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
//取得頭信息
String serviceName = request.getHeader("service");
//獲取頭信息映射成對應的服務信息
Service service = serviceMap.get(serviceName);
String serviceURI = service.getServiceId() + service.getPath();
//TODO 判斷服務是否存在,可以做額外異常處理
Route route = this.routeLocator.getMatchingRoute("/" + serviceURI);
//設置context
ctx.set("serviceId", service.getServiceId());
ctx.put("requestURI", service.getPath());
ctx.put("proxy", service.getServiceId());
ctx.put("retryable", false);
// ctx.remove("forward.to");
log.info(String.format("send %s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}

class Service {

public Service(String serviceId, String path) {
this.serviceId = serviceId;
this.path = path;
}

String serviceId;
String path;

public String getServiceId() {
return serviceId;
}

public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}
}
}

然後可以將之前的PreDecorationFilter禁用,以免它對RequestContext的操作影響我們,例如,如果沒有匹配到任何規則,它會在RequestContext中添加一個forward.to 這個key會調用post階段的SendForwardFilter導致報錯。

在配置文件設置zuul.PreDecorationFilter.pre.disable=true即可。

現在將這個類納入spring容器中,寫法可以參照ZuulProxyAutoConfiguration中其它Filter的實例化方式,我們也做一個自己的配置類:

@Configuration
@EnableConfigurationProperties({ZuulProperties.class})
public class Config {

@Autowired
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;

@Bean
public HeaderPreDecorationFilter geaderPreDecorationFilter(RouteLocator routeLocator) {
return new HeaderPreDecorationFilter(routeLocator, this.server.getServlet().getContextPath());
}
}

這樣每次請求進來後,在pre階段會去取service頭信息,然後匹配成對應的serviceId(取不到或者匹配不到自然就報錯了),在route階段就直接觸發RibbonRoutingFilter調用服務返回了!

現在還剩一個網關入口的問題,我是想讓所有的請求走一個固定的url,先試著直接訪問一下:localhost:8080/gateway ,直接報404了。很正常,我們還沒有做這個url path的映射! SpringMvc的DispatcherServlet沒有查到這個path的處理方法自然報404了!怎樣才能讓gateway這個路由進入zuul中呢?

我們記得在上面Zuul的配置類中有一個ZuulHandlerMapping, 當一個請求進入SpringMvc的DispatchServlet後,會根據路由看能否匹配到ZuulHandlerMapping,匹配成功才會走zuul後續的流程。

以下是DispatcherServlet中doDispatch方法的代碼:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
try {
ModelAndView mv = null;
Object dispatchException = null;

try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//這裡選擇ZuulHandlerMapping,如果路由匹配成功,會返回包含ZuulController的ha
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());

// ... 省略代碼

//從這裡進入調用ZuulController
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}

this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}

} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}

}
}

那麼怎樣才能讓請求進入ZuulHandlerMapping呢,看下DispatchServlet中的的這個方法:

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
//按順序遍歷所有的HandlerMapping,直到取得一個
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}

return null;
}

我們需要ZuulHandlerMapping在mapping.getHandler的時候返回非空。研究下ZuulHandlerMapping,看下它的結構先:

ZuulHandlerMapping繼承了AbstractUrlHandlerMapping,AbstractUrlHandlerMapping又繼承自AbstractHandlerMapping。在上面的方法中調用ZuulHandlerMapping的mapping.getHandler(request)的時候 實際會調用到AbstractHandlerMapping的getHandlerInternal(request),再進入ZuulHandlerMapping的lookupHandler(String urlPath, HttpServletRequest request)這個方法。

看下這個方法:

@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request)
throws Exception {
if (this.errorController != null
&& urlPath.equals(this.errorController.getErrorPath())) {
return null;
}
if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) {
return null;
}
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey("forward.to")) {
return null;
}
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}
//實際會調用這裡
return super.lookupHandler(urlPath, request);
}

調用父類的AbstractUrlHandlerMapping.lookupHandler(urlPath, request)。

這個方法裏代碼比較多,其中的關鍵信息是:this.handlerMap.get(urlPath),也就是說我們輸入的url path只要能從handlerMap裏匹配到,就可以了! 現在需要看下ZuulHandlerMapping裏的這個handlerMap是怎麼維護的。類中有這麼一個方法:

private void registerHandlers() {
Collection<Route> routes = this.routeLocator.getRoutes();
if (routes.isEmpty()) {
this.logger.warn("No routes found from RouteLocator");
}
else {
for (Route route : routes) {
registerHandler(route.getFullPath(), this.zuul);
}
}
}

它會從routeLocator裏取出所有的route,一個一個註冊到handlerMap裏。這樣的話就簡單了,我只要自己定義一個RouteLocator,把我想要的路由設置好,再讓它自動被註冊進去就行了吧!

定義一個GatewayRouteLocator:

public class GatewayRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

public final static Logger logger = LoggerFactory.getLogger(GatewayRouteLocator.class);

public GatewayRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
}
public void refresh() {
doRefresh();
}

@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<String, ZuulProperties.ZuulRoute>();
routesMap.put("gateway", new ZuulProperties.ZuulRoute());
return routesMap;
}

@Override
public List<Route> getRoutes() {
//假設我希望網關API為http://www.domain.com/gateway
List<Route> values = new ArrayList<Route>();
values.add(new Route("gateway1", "/gateway/", "/gateway", "", true, new HashSet<String>()));
values.add(new Route("gateway2", "/gateway", "/gateway", "", true, new HashSet<String>()));
return values;
}
}

現在我要將這個類也實例化到spring容器中。

觀察下ZuulProxyAutoConfiguration中的RouteLocator是怎麼實例化的,照葫蘆畫瓢弄一下,把這個類也添加到配置類裏:

@Configuration
@EnableConfigurationProperties({ZuulProperties.class})
public class Config {

@Autowired
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;

@Bean
public GatewayRouteLocator gatewayRouteLocator() {
return new GatewayRouteLocator(this.server.getServlet().getContextPath(), zuulProperties);
}

@Bean
public HeaderPreDecorationFilter geaderPreDecorationFilter(RouteLocator routeLocator) {
return new HeaderPreDecorationFilter(routeLocator, this.server.getServlet().getContextPath());
}
}

好了!這樣每次輸入domain.com/gateway 的時候,DispatchServlet就會為我們匹配到ZuulHandlerMapping,進而往下走到ZuulController中了。

再看下ZuulController的代碼:

ZuulController:

public class ZuulController extends ServletWrappingController {

public ZuulController() {
//在這裡已經設置了ZuulServlet
setServletClass(ZuulServlet.class);
setServletName("zuul");
setSupportedMethods((String[]) null); // Allow all
}

@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
try {
//在這裡面會調用ZuulServlet的service方法
return super.handleRequestInternal(request, response);
}
finally {
// @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
RequestContext.getCurrentContext().unset();
}
}

}

就是將Request送入ZuulServlet,這樣就跟上面的流程銜接上了!

總結一下,一次請求流程為 DispatcherServlet->ZuulHandlerMapping->ZuulController->ZuulServlet->ZuulRunner-> FilterProcessor->ZuulFilter->PreDecorationFilter(替換為自定義的HeaderPreDecorationFilter)->RibbonRoutingFilter

至此,對Zuul的改造就完成了!現在我對外暴露一個統一的api:domain.com/gateway,所有的服務都從這裡調用,同時通過傳入一個service的頭信息來指定調用具體 的服務,服務列表可以維護在其它地方動態刷新,這樣就不會將serviceName暴露出去了!


推薦閱讀:
相關文章