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

在使用SpringCloud的时候准备使用Zuul作为微服务的网关,Zuul的默认路由方式主要是两种,一种是在配置 文件里直接指定静态路由,另一种是根据注册在Eureka的服务名自动匹配。比如如果有一个名为service1的服 务,通过  就能访问到这个服务。但是这和我预想的需求还是有些差距。 网上有许多有关动态路由的实现方法,大致思想是不从Eureka拉取注册服务信息,而是在数据库里自己维护一 份路由表,定时读取数据库拉取路由,来实现自动更新。而我的需求更进一步,我希望对外暴露的网关接口是 一个固定的url,如 ,然后根据一个头信息service来指定想要访问的服务,而不是在 url后面拼接服务名。同时我也不想将我注册到Eureka的服务名直接暴露在api中,而是做一层映射,让我可以 灵活指定serivce名。

例如:

现在研究一下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 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:

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

好了!这样每次输入 的时候,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:,所有的服务都从这里调用,同时通过传入一个service的头信息来指定调用具体 的服务,服务列表可以维护在其它地方动态刷新,这样就不会将serviceName暴露出去了!

posted @ 2019-10-02 17:17  扫地猿  阅读(806)  评论(0编辑  收藏  举报