SpringCloud-Zuul源碼分析和路由改造

在使用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

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

還有一些其餘的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();

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

  • 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());
    }
}

好了!這樣每次輸入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暴露出去了!

相關文章
相關標籤/搜索