zoukankan      html  css  js  c++  java
  • 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暴露出去了!

  • 相关阅读:
    单机千万级MQTT连接服务器测试报告
    Esptouch移植xamarin记要
    ubuntu16.04之mongodb自动备份
    Mongodb4.0副本集构建
    golang项目git-subtree完美解决差异包管理
    linux系统优化配置
    IE外挂
    aliyun install Discourse log
    打包前端WebSite到Go程序
    golang channel string 信号乱码
  • 原文地址:https://www.cnblogs.com/likui360/p/11617816.html
Copyright © 2011-2022 走看看