在使用SpringCloud的時候準備使用Zuul做爲微服務的網關,Zuul的默認路由方式主要是兩種,一種是在配置 文件裏直接指定靜態路由,另外一種是根據註冊在Eureka的服務名自動匹配。好比若是有一個名爲service1的服 務,經過 http://www.domain.com/service1/xxx 就能訪問到這個服務。可是這和我預想的需求仍是有些差距。 網上有許多有關動態路由的實現方法,大體思想是不從Eureka拉取註冊服務信息,而是在數據庫裏本身維護一 份路由表,定時讀取數據庫拉取路由,來實現自動更新。而個人需求更進一步,我但願對外暴露的網關接口是 一個固定的url,如http://www.domain.com/gateway ,而後根據一個頭信息service來指定想要訪問的服務,而不是在 url後面拼接服務名。同時我也不想將我註冊到Eureka的服務名直接暴露在api中,而是作一層映射,讓我能夠 靈活指定serivce名。java
例如:web
如今研究一下Zuul的源碼來看看怎麼實現這個功能。spring
咱們都知道在Springboot啓動類上加一個@EnableZuulProxy就啓用了Zuul。從這個註解點進去看看:數據庫
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
它引入了ZuulProxyMarkerConfiguration這個配置,進去看下。apache
/**
* 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,看看這個類:api
@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 doesn't
// 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 doesn't
// 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:app
還有一些其餘的Filter,不一一看了。cors
其中,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();
它們各自的含義與功能總結以下:
下圖源自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());
}
}
好了!這樣每次輸入http://www.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:http://www.domain.com/gateway,全部的服務都從這裏調用,同時經過傳入一個service的頭信息來指定調用具體 的服務,服務列表能夠維護在其它地方動態刷新,這樣就不會將serviceName暴露出去了!