Zuul简单使用以及原理

  简单研究下Zuul简单使用以及原理.

1. 使用

0. pom如下:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>cn.qz.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-gateway-zuul9526</artifactId>

    <dependencies>
        <!--zuul-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--引入自己抽取的工具包-->
        <dependency>
            <groupId>cn.qz.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--一般基础配置类-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>
View Code
复制代码

  最终zuul-core 是1.3.1 版本。

1. 新增filter

PreFilter 前置处理器

复制代码
package cn.qz.cloud.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@Component
public class PreFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return -1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 返回值好像没什么实际意义, @see com.netflix.zuul.FilterProcessor#runFilters(java.lang.String)
     *
     * @return 可以返回任意的对象,当前实现忽略。
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // RequestContext 继承自ConcurrentHashMap, 用于维护当前请求的一些上下文参数, 内部用threadLocal 维护当前请求的当前RequestContext。
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            String header = request.getHeader(key);
            System.out.println("key: " + key + "\tvalue: " + header);
        }
        return null;
    }
}
View Code
复制代码

PostFilter 后置处理器

复制代码
package cn.qz.cloud.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

@Component
public class PostFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return -1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return ctx.sendZuulResponse();
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        System.out.println("========cn.qz.cloud.filter.PostFilter.run");
        System.out.println(ctx);
        return null;
    }
}
View Code
复制代码

2. 主启动类

复制代码
package cn.qz.cloud;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
View Code
复制代码

3.  配置文件 application.yml

复制代码
server:
  port: 9526

spring:
  application:
    name: cloud-zuul
  servlet:
    multipart:
      max-file-size: 2048MB
      max-request-size: 2048MB


eureka:
  instance:
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://localhost:7001/eureka

zuul:
  routes:
    # 第一种配置方式,这种方式会创建两个 ZuulRoute 对象。 一个是: /api/v1/payment/** 路由转发到服务 cloud-payment-service; 第二个是 /cloud-payment-service/** 转发到服务 cloud-payment-service
    cloud-payment-service:
      path: /api/v1/payment/**
      sensitive-headers: Cookie,Set-Cookie
      service-id: CLOUD-PAYMENT-SERVICE
    # 下面这种方式等价于 /orders/** 开头的所有请求都交给orders 服务处理
    orders: /orders/**
    # URL 跳转的方式,不走ribbon 路由服务。访问: http://localhost:9526/guonei
    guonei:
      path: /guonei/**
      url: http://news.baidu.com/guonei

  retryable: false
  ribbon:
    eager-load:
      enabled: false
  servlet-path: /cloud-zuul

ribbon:
  ReadTimeout: 600000
  SocketTimeout: 600000
  ConnectTimeout: 80000
View Code
复制代码

4. 接下来就可以通过zuul 实现请求转发,测试如下:

复制代码
xxx /e/ideaspace/test (dev)
$ curl http://localhost:9526/api/v1/payment/pay/getServerPort
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    52    0    52    0     0   1040      0 --:--:-- --:--:-- --:--:--  1061{"success":true,"code":"200","msg":"","data":"8081"}

xxx /e/ideaspace/test (dev)
$ curl http://localhost:9526/cloud-zuul/api/v1/payment/pay/getServerPort
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    52    0    52    0     0    928      0 --:--:-- --:--:-- --:--:--   962{"success":true,"code":"200","msg":"","data":"8081"}

xxx /e/ideaspace/test (dev)
$ curl http://localhost:9526/cloud-payment-service/pay/getServerPort
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    52    0    52    0     0    269      0 --:--:-- --:--:-- --:--:--   270{"success":true,"code":"200","msg":"","data":"8081"}


xxx /e/ideaspace/test (dev)
$ curl http://localhost:9526/cloud-zuul/cloud-payment-service/pay/getServerPort
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    52    0    52    0     0   1209      0 --:--:-- --:--:-- --:--:--  1268{"success":true,"code":"200","msg":"","data":"8081"}
复制代码

  浏览器访问: http://localhost:9526/guonei 可以看到调整到百度新闻首页。

  /api/v1/payment/pay/getServerPort 和 /cloud-payment-service/pay/getServerPort 应该走的是一套机制; /cloud-zuul/api/v1/payment/pay/getServerPort 和/cloud-zuul/cloud-payment-service/pay/getServerPort 是一套机制。下面研究两套流程逻辑。

2. 原理

1. 前置

  基于SpringMVC的DispatcherServlet + zuul 的自己的Servlet + Filter 机制来实现。

  有两套机制: 第一套走SpringMVC(基于DispatcherServlet自定义HandlerMapping + Handler实现逻辑), 第二套不走SpingMVC机制(相当于直接向容器注册Servlet)。

2. 整合原理

  org.springframework.cloud.netflix.zuul.EnableZuulProxy 注解开启自动整合zuul。引org.springframework.cloud.netflix.zuul.ZuulProxyMarkerConfiguration配置类,实际上是注入一个Marker类,这也是Spring starter经常采用的一种方式。

复制代码
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {

}

------------

@Configuration(proxyBeanMethods = false)
public class ZuulProxyMarkerConfiguration {

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

    class Marker {

    }

}
复制代码

引入自动配置类:org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration

复制代码
@Configuration(proxyBeanMethods = false)
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
        HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {

    @SuppressWarnings("rawtypes")
    @Autowired(required = false)
    private List<RibbonRequestCustomizer> requestCustomizers = Collections.emptyList();

    @Autowired(required = false)
    private Registration registration;

    @Autowired
    private DiscoveryClient discovery;

    @Autowired
    private ServiceRouteMapper serviceRouteMapper;

    @Override
    public HasFeatures zuulFeature() {
        return HasFeatures.namedFeature("Zuul (Discovery)",
                ZuulProxyAutoConfiguration.class);
    }

    @Bean
    @ConditionalOnMissingBean(DiscoveryClientRouteLocator.class)
    public DiscoveryClientRouteLocator discoveryRouteLocator() {
        return new DiscoveryClientRouteLocator(this.server.getServlet().getContextPath(),
                this.discovery, this.zuulProperties, this.serviceRouteMapper,
                this.registration);
    }

    // pre filters
    @Bean
    @ConditionalOnMissingBean(PreDecorationFilter.class)
    public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator,
            ProxyRequestHelper proxyRequestHelper) {
        return new PreDecorationFilter(routeLocator,
                this.server.getServlet().getContextPath(), this.zuulProperties,
                proxyRequestHelper);
    }

    // route filters
    @Bean
    @ConditionalOnMissingBean(RibbonRoutingFilter.class)
    public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
            RibbonCommandFactory<?> ribbonCommandFactory) {
        RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory,
                this.requestCustomizers);
        return filter;
    }

    @Bean
    @ConditionalOnMissingBean({ SimpleHostRoutingFilter.class,
            CloseableHttpClient.class })
    public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper,
            ZuulProperties zuulProperties,
            ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
            ApacheHttpClientFactory httpClientFactory) {
        return new SimpleHostRoutingFilter(helper, zuulProperties,
                connectionManagerFactory, httpClientFactory);
    }

    @Bean
    @ConditionalOnMissingBean({ SimpleHostRoutingFilter.class })
    public SimpleHostRoutingFilter simpleHostRoutingFilter2(ProxyRequestHelper helper,
            ZuulProperties zuulProperties, CloseableHttpClient httpClient) {
        return new SimpleHostRoutingFilter(helper, zuulProperties, httpClient);
    }

    @Bean
    @ConditionalOnMissingBean(ServiceRouteMapper.class)
    public ServiceRouteMapper serviceRouteMapper() {
        return new SimpleServiceRouteMapper();
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("org.springframework.boot.actuate.health.Health")
    protected static class NoActuatorConfiguration {

        @Bean
        public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) {
            ProxyRequestHelper helper = new ProxyRequestHelper(zuulProperties);
            return helper;
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Health.class)
    protected static class EndpointConfiguration {

        @Autowired(required = false)
        private HttpTraceRepository traces;

        @Bean
        @ConditionalOnEnabledEndpoint
        public RoutesEndpoint routesEndpoint(RouteLocator routeLocator) {
            return new RoutesEndpoint(routeLocator);
        }

        @ConditionalOnEnabledEndpoint
        @Bean
        public FiltersEndpoint filtersEndpoint() {
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new FiltersEndpoint(filterRegistry);
        }

        @Bean
        public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) {
            TraceProxyRequestHelper helper = new TraceProxyRequestHelper(zuulProperties);
            if (this.traces != null) {
                helper.setTraces(this.traces);
            }
            return helper;
        }

    }

}
View Code
复制代码

父类org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration:

复制代码
@Configuration(proxyBeanMethods = false)
@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,
            ZuulController zuulController) {
        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
    @ConditionalOnMissingBean
    public FormBodyWrapperFilter formBodyWrapperFilter() {
        return new FormBodyWrapperFilter();
    }

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

    @Bean
    @ConditionalOnMissingBean
    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(proxyBeanMethods = false)
    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(proxyBeanMethods = false)
    @ConditionalOnClass(MeterRegistry.class)
    protected static class ZuulCounterFactoryConfiguration {

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

    }

    @Configuration(proxyBeanMethods = false)
    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();
        }

    }

}
View Code
复制代码

  可以看到上面配置,ZuulProxyAutoConfiguration 相比 ZuulServerAutoConfiguration 增加了一些Ribbon相关的代理,也就是支持从注册中心拿服务信息。这也是EnableZuulProxy注解和EnableZuulServer 的区别,两个分别引入下面配置类:

复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ZuulServerMarkerConfiguration.class)
public @interface EnableZuulServer {
}

--------
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
复制代码

官方解释如下:

​   Spring Cloud Netflix会根据使用哪个注释来启用Zuul安装多个过滤器。`@EnableZuulProxy`是`@EnableZuulServer`的超集。换句话说,`@EnableZuulProxy`包含`@EnableZuulServer`安装的所有过滤器。“代理”中的其他过滤器启用路由功能。如果你想要一个“空白”Zuul,你应该使用`@EnableZuulServer`。

   org.springframework.cloud.netflix.zuul.filters.ZuulProperties: 属性类,解析配置文件的配置,可以看到将配置信息维护到Map<String, ZuulRoute> routes对象中, 并且org.springframework.cloud.netflix.zuul.filters.ZuulProperties#init 用@PostConstruct 在初始化之前进行路径的规范化。

复制代码
@ConfigurationProperties("zuul")
public class ZuulProperties {

    /**
     * Headers that are generally expected to be added by Spring Security, and hence often
     * duplicated if the proxy and the backend are secured with Spring. By default they
     * are added to the ignored headers if Spring Security is present and
     * ignoreSecurityHeaders = true.
     */
    public static final List<String> SECURITY_HEADERS = Arrays.asList("Pragma",
            "Cache-Control", "X-Frame-Options", "X-Content-Type-Options",
            "X-XSS-Protection", "Expires");

    /**
     * A common prefix for all routes.
     */
    private String prefix = "";

    /**
     * Flag saying whether to strip the prefix from the path before forwarding.
     */
    private boolean stripPrefix = true;

    /**
     * Flag for whether retry is supported by default (assuming the routes themselves
     * support it).
     */
    private Boolean retryable = false;

    /**
     * Map of route names to properties.
     */
    private Map<String, ZuulRoute> routes = new LinkedHashMap<>();

    /**
     * Flag to determine whether the proxy adds X-Forwarded-* headers.
     */
    private boolean addProxyHeaders = true;

    /**
     * Flag to determine whether the proxy forwards the Host header.
     */
    private boolean addHostHeader = false;

    /**
     * Set of service names not to consider for proxying automatically. By default all
     * services in the discovery client will be proxied.
     */
    private Set<String> ignoredServices = new LinkedHashSet<>();

    private Set<String> ignoredPatterns = new LinkedHashSet<>();

    /**
     * Names of HTTP headers to ignore completely (i.e. leave them out of downstream
     * requests and drop them from downstream responses).
     */
    private Set<String> ignoredHeaders = new LinkedHashSet<>();

    /**
     * Flag to say that SECURITY_HEADERS are added to ignored headers if spring security
     * is on the classpath. By setting ignoreSecurityHeaders to false we can switch off
     * this default behaviour. This should be used together with disabling the default
     * spring security headers see
     * https://docs.spring.io/spring-security/site/docs/current/reference/html/headers.html#default-security-headers
     */
    private boolean ignoreSecurityHeaders = true;

    /**
     * Flag to force the original query string encoding when building the backend URI in
     * SimpleHostRoutingFilter. When activated, query string will be built using
     * HttpServletRequest getQueryString() method instead of UriTemplate. Note that this
     * flag is not used in RibbonRoutingFilter with services found via DiscoveryClient
     * (like Eureka).
     */
    private boolean forceOriginalQueryStringEncoding = false;

    /**
     * Path to install Zuul as a servlet (not part of Spring MVC). The servlet is more
     * memory efficient for requests with large bodies, e.g. file uploads.
     */
    private String servletPath = "/zuul";

    private boolean ignoreLocalService = true;

    /**
     * Host properties controlling default connection pool properties.
     */
    private Host host = new Host();

    /**
     * Flag to say that request bodies can be traced.
     */
    private boolean traceRequestBody = false;

    /**
     * Flag to say that path elements past the first semicolon can be dropped.
     */
    private boolean removeSemicolonContent = true;

    /**
     * Flag to indicate whether to decode the matched URL or use it as is.
     */
    private boolean decodeUrl = true;

    /**
     * List of sensitive headers that are not passed to downstream requests. Defaults to a
     * "safe" set of headers that commonly contain user credentials. It's OK to remove
     * those from the list if the downstream service is part of the same system as the
     * proxy, so they are sharing authentication data. If using a physical URL outside
     * your own domain, then generally it would be a bad idea to leak user credentials.
     */
    private Set<String> sensitiveHeaders = new LinkedHashSet<>(
            Arrays.asList("Cookie", "Set-Cookie", "Authorization"));

    /**
     * Flag to say whether the hostname for ssl connections should be verified or not.
     * Default is true. This should only be used in test setups!
     */
    private boolean sslHostnameValidationEnabled = true;

    private ExecutionIsolationStrategy ribbonIsolationStrategy = SEMAPHORE;

    private HystrixSemaphore semaphore = new HystrixSemaphore();

    private HystrixThreadPool threadPool = new HystrixThreadPool();

    /**
     * Setting for SendResponseFilter to conditionally set Content-Length header.
     */
    private boolean setContentLength = false;

    /**
     * Setting for SendResponseFilter to conditionally include X-Zuul-Debug-Header header.
     */
    private boolean includeDebugHeader = false;

    /**
     * Setting for SendResponseFilter for the initial stream buffer size.
     */
    private int initialStreamBufferSize = 8192;

    public Set<String> getIgnoredHeaders() {
        Set<String> ignoredHeaders = new LinkedHashSet<>(this.ignoredHeaders);
        if (ClassUtils.isPresent(
                "org.springframework.security.config.annotation.web.WebSecurityConfigurer",
                null) && Collections.disjoint(ignoredHeaders, SECURITY_HEADERS)
                && ignoreSecurityHeaders) {
            // Allow Spring Security in the gateway to control these headers
            ignoredHeaders.addAll(SECURITY_HEADERS);
        }
        return ignoredHeaders;
    }

    public void setIgnoredHeaders(Set<String> ignoredHeaders) {
        this.ignoredHeaders.addAll(ignoredHeaders);
    }

    @PostConstruct
    public void init() {
        for (Entry<String, ZuulRoute> entry : this.routes.entrySet()) {
            ZuulRoute value = entry.getValue();
            if (!StringUtils.hasText(value.getLocation())) {
                value.serviceId = entry.getKey();
            }
            if (!StringUtils.hasText(value.getId())) {
                value.id = entry.getKey();
            }
            if (!StringUtils.hasText(value.getPath())) {
                value.path = "/" + entry.getKey() + "/**";
            }
        }
    }

    public String getServletPattern() {
        String path = this.servletPath;
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        if (!path.contains("*")) {
            path = path.endsWith("/") ? (path + "*") : (path + "/*");
        }
        return path;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public boolean isStripPrefix() {
        return stripPrefix;
    }

    public void setStripPrefix(boolean stripPrefix) {
        this.stripPrefix = stripPrefix;
    }

    public Boolean getRetryable() {
        return retryable;
    }

    public void setRetryable(Boolean retryable) {
        this.retryable = retryable;
    }

    public Map<String, ZuulRoute> getRoutes() {
        return routes;
    }

    public void setRoutes(Map<String, ZuulRoute> routes) {
        this.routes = routes;
    }

    public boolean isAddProxyHeaders() {
        return addProxyHeaders;
    }

    public void setAddProxyHeaders(boolean addProxyHeaders) {
        this.addProxyHeaders = addProxyHeaders;
    }

    public boolean isAddHostHeader() {
        return addHostHeader;
    }

    public void setAddHostHeader(boolean addHostHeader) {
        this.addHostHeader = addHostHeader;
    }

    public Set<String> getIgnoredServices() {
        return ignoredServices;
    }

    public void setIgnoredServices(Set<String> ignoredServices) {
        this.ignoredServices = ignoredServices;
    }

    public Set<String> getIgnoredPatterns() {
        return ignoredPatterns;
    }

    public void setIgnoredPatterns(Set<String> ignoredPatterns) {
        this.ignoredPatterns = ignoredPatterns;
    }

    public boolean isIgnoreSecurityHeaders() {
        return ignoreSecurityHeaders;
    }

    public void setIgnoreSecurityHeaders(boolean ignoreSecurityHeaders) {
        this.ignoreSecurityHeaders = ignoreSecurityHeaders;
    }

    public boolean isForceOriginalQueryStringEncoding() {
        return forceOriginalQueryStringEncoding;
    }

    public void setForceOriginalQueryStringEncoding(
            boolean forceOriginalQueryStringEncoding) {
        this.forceOriginalQueryStringEncoding = forceOriginalQueryStringEncoding;
    }

    public String getServletPath() {
        return servletPath;
    }

    public void setServletPath(String servletPath) {
        this.servletPath = servletPath;
    }

    public boolean isIgnoreLocalService() {
        return ignoreLocalService;
    }

    public void setIgnoreLocalService(boolean ignoreLocalService) {
        this.ignoreLocalService = ignoreLocalService;
    }

    public Host getHost() {
        return host;
    }

    public void setHost(Host host) {
        this.host = host;
    }

    public boolean isTraceRequestBody() {
        return traceRequestBody;
    }

    public void setTraceRequestBody(boolean traceRequestBody) {
        this.traceRequestBody = traceRequestBody;
    }

    public boolean isRemoveSemicolonContent() {
        return removeSemicolonContent;
    }

    public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
        this.removeSemicolonContent = removeSemicolonContent;
    }

    public boolean isDecodeUrl() {
        return decodeUrl;
    }

    public void setDecodeUrl(boolean decodeUrl) {
        this.decodeUrl = decodeUrl;
    }

    public Set<String> getSensitiveHeaders() {
        return sensitiveHeaders;
    }

    public void setSensitiveHeaders(Set<String> sensitiveHeaders) {
        this.sensitiveHeaders = sensitiveHeaders;
    }

    public boolean isSslHostnameValidationEnabled() {
        return sslHostnameValidationEnabled;
    }

    public void setSslHostnameValidationEnabled(boolean sslHostnameValidationEnabled) {
        this.sslHostnameValidationEnabled = sslHostnameValidationEnabled;
    }

    public ExecutionIsolationStrategy getRibbonIsolationStrategy() {
        return ribbonIsolationStrategy;
    }

    public void setRibbonIsolationStrategy(
            ExecutionIsolationStrategy ribbonIsolationStrategy) {
        this.ribbonIsolationStrategy = ribbonIsolationStrategy;
    }

    public HystrixSemaphore getSemaphore() {
        return semaphore;
    }

    public void setSemaphore(HystrixSemaphore semaphore) {
        this.semaphore = semaphore;
    }

    public HystrixThreadPool getThreadPool() {
        return threadPool;
    }

    public void setThreadPool(HystrixThreadPool threadPool) {
        this.threadPool = threadPool;
    }

    public boolean isSetContentLength() {
        return setContentLength;
    }

    public void setSetContentLength(boolean setContentLength) {
        this.setContentLength = setContentLength;
    }

    public boolean isIncludeDebugHeader() {
        return includeDebugHeader;
    }

    public void setIncludeDebugHeader(boolean includeDebugHeader) {
        this.includeDebugHeader = includeDebugHeader;
    }

    public int getInitialStreamBufferSize() {
        return initialStreamBufferSize;
    }

    public void setInitialStreamBufferSize(int initialStreamBufferSize) {
        this.initialStreamBufferSize = initialStreamBufferSize;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ZuulProperties that = (ZuulProperties) o;
        return addHostHeader == that.addHostHeader
                && addProxyHeaders == that.addProxyHeaders
                && forceOriginalQueryStringEncoding == that.forceOriginalQueryStringEncoding
                && Objects.equals(host, that.host)
                && Objects.equals(ignoredHeaders, that.ignoredHeaders)
                && Objects.equals(ignoredPatterns, that.ignoredPatterns)
                && Objects.equals(ignoredServices, that.ignoredServices)
                && ignoreLocalService == that.ignoreLocalService
                && ignoreSecurityHeaders == that.ignoreSecurityHeaders
                && Objects.equals(prefix, that.prefix)
                && removeSemicolonContent == that.removeSemicolonContent
                && Objects.equals(retryable, that.retryable)
                && Objects.equals(ribbonIsolationStrategy, that.ribbonIsolationStrategy)
                && Objects.equals(routes, that.routes)
                && Objects.equals(semaphore, that.semaphore)
                && Objects.equals(sensitiveHeaders, that.sensitiveHeaders)
                && Objects.equals(servletPath, that.servletPath)
                && sslHostnameValidationEnabled == that.sslHostnameValidationEnabled
                && stripPrefix == that.stripPrefix
                && setContentLength == that.setContentLength
                && includeDebugHeader == that.includeDebugHeader
                && initialStreamBufferSize == that.initialStreamBufferSize
                && Objects.equals(threadPool, that.threadPool)
                && traceRequestBody == that.traceRequestBody;
    }

    @Override
    public int hashCode() {
        return Objects.hash(addHostHeader, addProxyHeaders,
                forceOriginalQueryStringEncoding, host, ignoredHeaders, ignoredPatterns,
                ignoredServices, ignoreLocalService, ignoreSecurityHeaders, prefix,
                removeSemicolonContent, retryable, ribbonIsolationStrategy, routes,
                semaphore, sensitiveHeaders, servletPath, sslHostnameValidationEnabled,
                stripPrefix, threadPool, traceRequestBody, setContentLength,
                includeDebugHeader, initialStreamBufferSize);
    }

    @Override
    public String toString() {
        return new StringBuilder("ZuulProperties{").append("prefix='").append(prefix)
                .append("', ").append("stripPrefix=").append(stripPrefix).append(", ")
                .append("retryable=").append(retryable).append(", ").append("routes=")
                .append(routes).append(", ").append("addProxyHeaders=")
                .append(addProxyHeaders).append(", ").append("addHostHeader=")
                .append(addHostHeader).append(", ").append("ignoredServices=")
                .append(ignoredServices).append(", ").append("ignoredPatterns=")
                .append(ignoredPatterns).append(", ").append("ignoredHeaders=")
                .append(ignoredHeaders).append(", ").append("ignoreSecurityHeaders=")
                .append(ignoreSecurityHeaders).append(", ")
                .append("forceOriginalQueryStringEncoding=")
                .append(forceOriginalQueryStringEncoding).append(", ")
                .append("servletPath='").append(servletPath).append("', ")
                .append("ignoreLocalService=").append(ignoreLocalService).append(", ")
                .append("host=").append(host).append(", ").append("traceRequestBody=")
                .append(traceRequestBody).append(", ").append("removeSemicolonContent=")
                .append(removeSemicolonContent).append(", ").append("sensitiveHeaders=")
                .append(sensitiveHeaders).append(", ")
                .append("sslHostnameValidationEnabled=")
                .append(sslHostnameValidationEnabled).append(", ")
                .append("ribbonIsolationStrategy=").append(ribbonIsolationStrategy)
                .append(", ").append("semaphore=").append(semaphore).append(", ")
                .append("threadPool=").append(threadPool).append(", ")
                .append("setContentLength=").append(setContentLength).append(", ")
                .append("includeDebugHeader=").append(includeDebugHeader).append(", ")
                .append("initialStreamBufferSize=").append(initialStreamBufferSize)
                .append(", ").append("}").toString();
    }

    /**
     * Represents a Zuul route.
     */
    public static class ZuulRoute {

        /**
         * The ID of the route (the same as its map key by default).
         */
        private String id;

        /**
         * The path (pattern) for the route, e.g. /foo/**.
         */
        private String path;

        /**
         * The service ID (if any) to map to this route. You can specify a physical URL or
         * a service, but not both.
         */
        private String serviceId;

        /**
         * A full physical URL to map to the route. An alternative is to use a service ID
         * and service discovery to find the physical address.
         */
        private String url;

        /**
         * Flag to determine whether the prefix for this route (the path, minus pattern
         * patcher) should be stripped before forwarding.
         */
        private boolean stripPrefix = true;

        /**
         * Flag to indicate that this route should be retryable (if supported). Generally
         * retry requires a service ID and ribbon.
         */
        private Boolean retryable;

        /**
         * List of sensitive headers that are not passed to downstream requests. Defaults
         * to a "safe" set of headers that commonly contain user credentials. It's OK to
         * remove those from the list if the downstream service is part of the same system
         * as the proxy, so they are sharing authentication data. If using a physical URL
         * outside your own domain, then generally it would be a bad idea to leak user
         * credentials.
         */
        private Set<String> sensitiveHeaders = new LinkedHashSet<>();

        private boolean customSensitiveHeaders = false;

        public ZuulRoute() {
        }

        public ZuulRoute(String id, String path, String serviceId, String url,
                boolean stripPrefix, Boolean retryable, Set<String> sensitiveHeaders) {
            this.id = id;
            this.path = path;
            this.serviceId = serviceId;
            this.url = url;
            this.stripPrefix = stripPrefix;
            this.retryable = retryable;
            this.sensitiveHeaders = sensitiveHeaders;
            this.customSensitiveHeaders = sensitiveHeaders != null;
        }

        public ZuulRoute(String text) {
            String location = null;
            String path = text;
            if (text.contains("=")) {
                String[] values = StringUtils
                        .trimArrayElements(StringUtils.split(text, "="));
                location = values[1];
                path = values[0];
            }
            this.id = extractId(path);
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            setLocation(location);
            this.path = path;
        }

        public ZuulRoute(String path, String location) {
            this.id = extractId(path);
            this.path = path;
            setLocation(location);
        }

        public String getLocation() {
            if (StringUtils.hasText(this.url)) {
                return this.url;
            }
            return this.serviceId;
        }

        public void setLocation(String location) {
            if (location != null
                    && (location.startsWith("http:") || location.startsWith("https:"))) {
                this.url = location;
            }
            else {
                this.serviceId = location;
            }
        }

        private String extractId(String path) {
            path = path.startsWith("/") ? path.substring(1) : path;
            path = path.replace("/*", "").replace("*", "");
            return path;
        }

        public Route getRoute(String prefix) {
            return new Route(this.id, this.path, getLocation(), prefix, this.retryable,
                    isCustomSensitiveHeaders() ? this.sensitiveHeaders : null,
                    this.stripPrefix);
        }

        public boolean isCustomSensitiveHeaders() {
            return this.customSensitiveHeaders;
        }

        public void setCustomSensitiveHeaders(boolean customSensitiveHeaders) {
            this.customSensitiveHeaders = customSensitiveHeaders;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getPath() {
            return path;
        }

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

        public String getServiceId() {
            return serviceId;
        }

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

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public boolean isStripPrefix() {
            return stripPrefix;
        }

        public void setStripPrefix(boolean stripPrefix) {
            this.stripPrefix = stripPrefix;
        }

        public Boolean getRetryable() {
            return retryable;
        }

        public void setRetryable(Boolean retryable) {
            this.retryable = retryable;
        }

        public Set<String> getSensitiveHeaders() {
            return sensitiveHeaders;
        }

        public void setSensitiveHeaders(Set<String> headers) {
            this.customSensitiveHeaders = true;
            this.sensitiveHeaders = new LinkedHashSet<>(headers);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ZuulRoute that = (ZuulRoute) o;
            return customSensitiveHeaders == that.customSensitiveHeaders
                    && Objects.equals(id, that.id) && Objects.equals(path, that.path)
                    && Objects.equals(retryable, that.retryable)
                    && Objects.equals(sensitiveHeaders, that.sensitiveHeaders)
                    && Objects.equals(serviceId, that.serviceId)
                    && stripPrefix == that.stripPrefix && Objects.equals(url, that.url);
        }

        @Override
        public int hashCode() {
            return Objects.hash(customSensitiveHeaders, id, path, retryable,
                    sensitiveHeaders, serviceId, stripPrefix, url);
        }

        @Override
        public String toString() {
            return new StringBuilder("ZuulRoute{").append("id='").append(id).append("', ")
                    .append("path='").append(path).append("', ").append("serviceId='")
                    .append(serviceId).append("', ").append("url='").append(url)
                    .append("', ").append("stripPrefix=").append(stripPrefix).append(", ")
                    .append("retryable=").append(retryable).append(", ")
                    .append("sensitiveHeaders=").append(sensitiveHeaders).append(", ")
                    .append("customSensitiveHeaders=").append(customSensitiveHeaders)
                    .append(", ").append("}").toString();
        }

    }

    /**
     * Represents a host.
     */
    public static class Host {

        /**
         * The maximum number of total connections the proxy can hold open to backends.
         */
        private int maxTotalConnections = 200;

        /**
         * The maximum number of connections that can be used by a single route.
         */
        private int maxPerRouteConnections = 20;

        /**
         * The socket timeout in millis. Defaults to 10000.
         */
        private int socketTimeoutMillis = 10000;

        /**
         * The connection timeout in millis. Defaults to 2000.
         */
        private int connectTimeoutMillis = 2000;

        /**
         * The timeout in milliseconds used when requesting a connection from the
         * connection manager. Defaults to -1, undefined use the system default.
         */
        private int connectionRequestTimeoutMillis = -1;

        /**
         * The lifetime for the connection pool.
         */
        private long timeToLive = -1;

        /**
         * The time unit for timeToLive.
         */
        private TimeUnit timeUnit = TimeUnit.MILLISECONDS;

        public Host() {
        }

        public Host(int maxTotalConnections, int maxPerRouteConnections,
                int socketTimeoutMillis, int connectTimeoutMillis, long timeToLive,
                TimeUnit timeUnit) {
            this.maxTotalConnections = maxTotalConnections;
            this.maxPerRouteConnections = maxPerRouteConnections;
            this.socketTimeoutMillis = socketTimeoutMillis;
            this.connectTimeoutMillis = connectTimeoutMillis;
            this.timeToLive = timeToLive;
            this.timeUnit = timeUnit;
        }

        public int getMaxTotalConnections() {
            return maxTotalConnections;
        }

        public void setMaxTotalConnections(int maxTotalConnections) {
            this.maxTotalConnections = maxTotalConnections;
        }

        public int getMaxPerRouteConnections() {
            return maxPerRouteConnections;
        }

        public void setMaxPerRouteConnections(int maxPerRouteConnections) {
            this.maxPerRouteConnections = maxPerRouteConnections;
        }

        public int getSocketTimeoutMillis() {
            return socketTimeoutMillis;
        }

        public void setSocketTimeoutMillis(int socketTimeoutMillis) {
            this.socketTimeoutMillis = socketTimeoutMillis;
        }

        public int getConnectTimeoutMillis() {
            return connectTimeoutMillis;
        }

        public void setConnectTimeoutMillis(int connectTimeoutMillis) {
            this.connectTimeoutMillis = connectTimeoutMillis;
        }

        public int getConnectionRequestTimeoutMillis() {
            return connectionRequestTimeoutMillis;
        }

        public void setConnectionRequestTimeoutMillis(
                int connectionRequestTimeoutMillis) {
            this.connectionRequestTimeoutMillis = connectionRequestTimeoutMillis;
        }

        public long getTimeToLive() {
            return timeToLive;
        }

        public void setTimeToLive(long timeToLive) {
            this.timeToLive = timeToLive;
        }

        public TimeUnit getTimeUnit() {
            return timeUnit;
        }

        public void setTimeUnit(TimeUnit timeUnit) {
            this.timeUnit = timeUnit;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Host host = (Host) o;
            return maxTotalConnections == host.maxTotalConnections
                    && maxPerRouteConnections == host.maxPerRouteConnections
                    && socketTimeoutMillis == host.socketTimeoutMillis
                    && connectTimeoutMillis == host.connectTimeoutMillis
                    && connectionRequestTimeoutMillis == host.connectionRequestTimeoutMillis
                    && timeToLive == host.timeToLive && timeUnit == host.timeUnit;
        }

        @Override
        public int hashCode() {
            return Objects.hash(maxTotalConnections, maxPerRouteConnections,
                    socketTimeoutMillis, connectTimeoutMillis,
                    connectionRequestTimeoutMillis, timeToLive, timeUnit);
        }

        @Override
        public String toString() {
            return new ToStringCreator(this)
                    .append("maxTotalConnections", maxTotalConnections)
                    .append("maxPerRouteConnections", maxPerRouteConnections)
                    .append("socketTimeoutMillis", socketTimeoutMillis)
                    .append("connectTimeoutMillis", connectTimeoutMillis)
                    .append("connectionRequestTimeoutMillis",
                            connectionRequestTimeoutMillis)
                    .append("timeToLive", timeToLive).append("timeUnit", timeUnit)
                    .toString();
        }

    }

    /**
     * Represents Hystrix Sempahores.
     */
    public static class HystrixSemaphore {

        /**
         * The maximum number of total semaphores for Hystrix.
         */
        private int maxSemaphores = 100;

        public HystrixSemaphore() {
        }

        public HystrixSemaphore(int maxSemaphores) {
            this.maxSemaphores = maxSemaphores;
        }

        public int getMaxSemaphores() {
            return maxSemaphores;
        }

        public void setMaxSemaphores(int maxSemaphores) {
            this.maxSemaphores = maxSemaphores;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            HystrixSemaphore that = (HystrixSemaphore) o;
            return maxSemaphores == that.maxSemaphores;
        }

        @Override
        public int hashCode() {
            return Objects.hash(maxSemaphores);
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("HystrixSemaphore{");
            sb.append("maxSemaphores=").append(maxSemaphores);
            sb.append('}');
            return sb.toString();
        }

    }

    /**
     * Represents Hystrix ThreadPool.
     */
    public static class HystrixThreadPool {

        /**
         * Flag to determine whether RibbonCommands should use separate thread pools for
         * hystrix. By setting to true, RibbonCommands will be executed in a hystrix's
         * thread pool that it is associated with. Each RibbonCommand will be associated
         * with a thread pool according to its commandKey (serviceId). As default, all
         * commands will be executed in a single thread pool whose threadPoolKey is
         * "RibbonCommand". This property is only applicable when using THREAD as
         * ribbonIsolationStrategy
         */
        private boolean useSeparateThreadPools = false;

        /**
         * A prefix for HystrixThreadPoolKey of hystrix's thread pool that is allocated to
         * each service Id. This property is only applicable when using THREAD as
         * ribbonIsolationStrategy and useSeparateThreadPools = true
         */
        private String threadPoolKeyPrefix = "";

        public boolean isUseSeparateThreadPools() {
            return useSeparateThreadPools;
        }

        public void setUseSeparateThreadPools(boolean useSeparateThreadPools) {
            this.useSeparateThreadPools = useSeparateThreadPools;
        }

        public String getThreadPoolKeyPrefix() {
            return threadPoolKeyPrefix;
        }

        public void setThreadPoolKeyPrefix(String threadPoolKeyPrefix) {
            this.threadPoolKeyPrefix = threadPoolKeyPrefix;
        }

    }

}
View Code
复制代码

 这里只关注和Zuul流程相关的几个重要的Bean:

1. 程序入口

1. ZuulHandlerMapping, 相当于自定义SpringMVC的HandlerMapping
2. ZuulController,相当于自定义Controller,继承自org.springframework.web.servlet.mvc.Controller。内部将处理逻辑委托给ZuulServlet,这种方式走的是SpringMVC的处理机制,只不过内部将出炉逻辑委托给ZuulServlet。
3. ZuulServlet,处理代码的Servlet,继承自HttpServlet。

  上面1、2、3 方式注入到Spring之后,相当于增加了另一套经过SpringMVC的访问机制,和我们自定义的@GetMapping 是平级的一套机制。

4. ServletRegistrationBean 注册的方式,将ZuulServlet 到tomcat的servlet列表。相当于和SpringMVC的DispatcherServlet 平级的一个Servlet, 调用的时候先过该Servlet,路由匹配失败后走SpringMVC的默认的DispatcherServlet 。

​    上面1,2,3,4 是zuul 拦截程序的入口。一种依赖于SpringMVC,一种相当于走Zuul的自己的路径,默认是/zuul, 可以自己通过zuul.servletPath 进行配置。参考:org.springframework.cloud.netflix.zuul.filters.ZuulProperties

2. RouteLocator 路由查询器

  可以认为这部分是进行路由匹配的一系列Locator。包括一系列根据服务名替换路由等规则是走的这里的配置。 其实根本是读的org.springframework.cloud.netflix.zuul.filters.ZuulProperties#routes 维护的信息。

  org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration#discoveryRouteLocator 注入一个Discover 路由器, 这个内部维护了ZuulProperties 解析的路由信息。               org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration#primaryRouteLocator 这里注入一个CompositeRouteLocator 对象作为Primary的路由器。也就是作为路径查找器供后面使用。

复制代码
package org.springframework.cloud.netflix.zuul.filters;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.Assert;

/**
 * RouteLocator that composes multiple RouteLocators.
 *
 * @author Johannes Edmeier
 *
 */
public class CompositeRouteLocator implements RefreshableRouteLocator {

    private final Collection<? extends RouteLocator> routeLocators;

    private ArrayList<RouteLocator> rl;

    public CompositeRouteLocator(Collection<? extends RouteLocator> routeLocators) {
        Assert.notNull(routeLocators, "'routeLocators' must not be null");
        rl = new ArrayList<>(routeLocators);
        AnnotationAwareOrderComparator.sort(rl);
        this.routeLocators = rl;
    }

    @Override
    public Collection<String> getIgnoredPaths() {
        List<String> ignoredPaths = new ArrayList<>();
        for (RouteLocator locator : routeLocators) {
            ignoredPaths.addAll(locator.getIgnoredPaths());
        }
        return ignoredPaths;
    }

    @Override
    public List<Route> getRoutes() {
        List<Route> route = new ArrayList<>();
        for (RouteLocator locator : routeLocators) {
            route.addAll(locator.getRoutes());
        }
        return route;
    }

    @Override
    public Route getMatchingRoute(String path) {
        for (RouteLocator locator : routeLocators) {
            Route route = locator.getMatchingRoute(path);
            if (route != null) {
                return route;
            }
        }
        return null;
    }

    @Override
    public void refresh() {
        for (RouteLocator locator : routeLocators) {
            if (locator instanceof RefreshableRouteLocator) {
                ((RefreshableRouteLocator) locator).refresh();
            }
        }
    }

}
View Code
复制代码

  在org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping#setDirty 会调用上面的refresh 方法。

    public void setDirty(boolean dirty) {
        this.dirty = dirty;
        if (this.routeLocator instanceof RefreshableRouteLocator) {
            ((RefreshableRouteLocator) this.routeLocator).refresh();
        }
    }

 refresh 调用到: org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator#locateRoutes

复制代码
    @Override
    protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
        routesMap.putAll(super.locateRoutes());
        if (this.discovery != null) {
            Map<String, ZuulRoute> staticServices = new LinkedHashMap<>();
            for (ZuulRoute route : routesMap.values()) {
                String serviceId = route.getServiceId();
                if (serviceId == null) {
                    serviceId = route.getId();
                }
                if (serviceId != null) {
                    staticServices.put(serviceId, route);
                }
            }
            // Add routes for discovery services by default
            List<String> services = this.discovery.getServices();
            String[] ignored = this.properties.getIgnoredServices()
                    .toArray(new String[0]);
            for (String serviceId : services) {
                // Ignore specifically ignored services and those that were manually
                // configured
                String key = "/" + mapRouteToService(serviceId) + "/**";
                if (staticServices.containsKey(serviceId)
                        && staticServices.get(serviceId).getUrl() == null) {
                    // Explicitly configured with no URL, cannot be ignored
                    // all static routes are already in routesMap
                    // Update location using serviceId if location is null
                    ZuulRoute staticRoute = staticServices.get(serviceId);
                    if (!StringUtils.hasText(staticRoute.getLocation())) {
                        staticRoute.setLocation(serviceId);
                    }
                }
                if (!PatternMatchUtils.simpleMatch(ignored, serviceId)
                        && !routesMap.containsKey(key)) {
                    // Not ignored
                    routesMap.put(key, new ZuulRoute(key, serviceId));
                }
            }
        }
        if (routesMap.get(DEFAULT_ROUTE) != null) {
            ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE);
            // Move the defaultServiceId to the end
            routesMap.remove(DEFAULT_ROUTE);
            routesMap.put(DEFAULT_ROUTE, defaultRoute);
        }
        LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
        for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            // Prepend with slash if not already present.
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.properties.getPrefix())) {
                path = this.properties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, entry.getValue());
        }
        return values;
    }
View Code
复制代码

  这里相当于是刷新org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator#routes 路由信息,可以看到上面代码会针对每个服务也生成一个路由信息(/cloud-payment-service/** 其path不满足 /serviceName/**, 所以又生成了一个路由规则)。最终生成的路由信息如下:

3. filter 用于执行处理流程的一系列过滤器。

1. FormBodyWrapperFilter、ServletDetectionFilter、SendResponseFilter 一系列的Filter, com.netflix.zuul.ZuulFilter#filterType 方法标记其处理的类型。从源码org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 也可以看出其包含的filter 类型包括:

复制代码
    /**
     * {@link ZuulFilter#filterType()} error type.
     */
    public static final String ERROR_TYPE = "error";

    /**
     * {@link ZuulFilter#filterType()} post type.
     */
    public static final String POST_TYPE = "post";

    /**
     * {@link ZuulFilter#filterType()} pre type.
     */
    public static final String PRE_TYPE = "pre";

    /**
     * {@link ZuulFilter#filterType()} route type.
     */
    public static final String ROUTE_TYPE = "route";
复制代码

2. 注入的重要的filter

前置过滤器

- `ServletDetectionFilter`:检测请求是否通过Spring调度程序。使用键`FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY`设置布尔值。
- `FormBodyWrapperFilter`:解析表单数据,并为下游请求重新编码它。
- `DebugFilter`:如果设置`debug`请求参数,则此过滤器将`RequestContext.setDebugRouting()`和`RequestContext.setDebugRequest()`设置为true。

路由过滤器

- `SendForwardFilter`:此过滤器使用Servlet `RequestDispatcher`转发请求。转发位置存储在`RequestContext`属性`FilterConstants.FORWARD_TO_KEY`中。这对于转发到当前应用程序中的端点很有用。

过滤器:

- `SendResponseFilter`:将代理请求的响应写入当前响应。

错误过滤器:

- `SendErrorFilter`:如果`RequestContext.getThrowable()`不为null,则转发到/错误(默认情况下)。可以通过设置`error.path`属性来更改默认转发路径(`/error`)。

3. org.springframework.cloud.netflix.zuul.ZuulFilterInitializer 是维护ZuulFilter的一个重要类,自动注入容器中的ZuulFilter 对象,然后进行分类后维护到com.netflix.zuul.FilterLoader 对象内,相关源码:

(1) ZuulFilterInitializer

复制代码
public class ZuulFilterInitializer {

    private static final Log log = LogFactory.getLog(ZuulFilterInitializer.class);

    private final Map<String, ZuulFilter> filters;

    private final CounterFactory counterFactory;

    private final TracerFactory tracerFactory;

    private final FilterLoader filterLoader;

    private final FilterRegistry filterRegistry;

    public ZuulFilterInitializer(Map<String, ZuulFilter> filters,
            CounterFactory counterFactory, TracerFactory tracerFactory,
            FilterLoader filterLoader, FilterRegistry filterRegistry) {
        this.filters = filters;
        this.counterFactory = counterFactory;
        this.tracerFactory = tracerFactory;
        this.filterLoader = filterLoader;
        this.filterRegistry = filterRegistry;
    }

    @PostConstruct
    public void contextInitialized() {
        log.info("Starting filter initializer");

        TracerFactory.initialize(tracerFactory);
        CounterFactory.initialize(counterFactory);

        for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
            filterRegistry.put(entry.getKey(), entry.getValue());
        }
    }
    ...
View Code
复制代码

(2) com.netflix.zuul.FilterLoader

复制代码
public class FilterLoader {
    final static FilterLoader INSTANCE = new FilterLoader();

    private static final Logger LOG = LoggerFactory.getLogger(FilterLoader.class);

    private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>();
    private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>();
    private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>();
    private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
    ...
View Code
复制代码

最后维护到hashFiltersByType 的filters 如下:

 3. 调用原理

1. 前置

  filter 是zuul 处理流程的重要组件。ZuulServlet 是程序入口;ZuulFilter 是程序处理的逻辑,以链条的设计模式进行调用;其中RouteLocator 在Filter 中发挥作用,用于匹配解析相关的路由,然后存到RequestContext, RequestContext 继承自ConcurrentHashMap, 用于维护当前请求的一些上下文参数, 内部用threadLocal 维护当前请求的当前RequestContext。整个ZullFilter 链条 用RequestContext 进行数据传递,且key 在常量类中定义。

  参考: com.netflix.zuul.context.RequestContext 和 org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 类。

不管哪种方式的调用最终都会经过ZuulServlet。ZuulServlet 代码如下:

复制代码
public class ZuulServlet extends HttpServlet {

    private static final long serialVersionUID = -3374242278843351500L;
    private ZuulRunner zuulRunner;


    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        String bufferReqsStr = config.getInitParameter("buffer-requests");
        boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;

        zuulRunner = new ZuulRunner(bufferReqs);
    }

    @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 {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

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

    /**
     * executes "post" ZuulFilters
     *
     * @throws ZuulException
     */
    void postRoute() throws ZuulException {
        zuulRunner.postRoute();
    }

    /**
     * executes "route" filters
     *
     * @throws ZuulException
     */
    void route() throws ZuulException {
        zuulRunner.route();
    }

    /**
     * executes "pre" filters
     *
     * @throws ZuulException
     */
    void preRoute() throws ZuulException {
        zuulRunner.preRoute();
    }

    /**
     * initializes request
     *
     * @param servletRequest
     * @param servletResponse
     */
    void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        zuulRunner.init(servletRequest, servletResponse);
    }

    /**
     * sets error context info and executes "error" filters
     *
     * @param e
     */
    void error(ZuulException e) {
        RequestContext.getCurrentContext().setThrowable(e);
        zuulRunner.error();
    }

    @RunWith(MockitoJUnitRunner.class)
    public static class UnitTest {

        @Mock
        HttpServletRequest servletRequest;
        @Mock
        HttpServletResponseWrapper servletResponse;
        @Mock
        FilterProcessor processor;
        @Mock
        PrintWriter writer;

        @Before
        public void before() {
            MockitoAnnotations.initMocks(this);
        }

        @Test
        public void testProcessZuulFilter() {

            ZuulServlet zuulServlet = new ZuulServlet();
            zuulServlet = spy(zuulServlet);
            RequestContext context = spy(RequestContext.getCurrentContext());


            try {
                FilterProcessor.setProcessor(processor);
                RequestContext.testSetCurrentContext(context);
                when(servletResponse.getWriter()).thenReturn(writer);

                zuulServlet.init(servletRequest, servletResponse);
                verify(zuulServlet, times(1)).init(servletRequest, servletResponse);
                assertTrue(RequestContext.getCurrentContext().getRequest() instanceof HttpServletRequestWrapper);
                assertTrue(RequestContext.getCurrentContext().getResponse() instanceof HttpServletResponseWrapper);

                zuulServlet.preRoute();
                verify(processor, times(1)).preRoute();

                zuulServlet.postRoute();
                verify(processor, times(1)).postRoute();
//                verify(context, times(1)).unset();

                zuulServlet.route();
                verify(processor, times(1)).route();
                RequestContext.testSetCurrentContext(null);

            } catch (Exception e) {
                e.printStackTrace();
            }


        }
    }

}
View Code
复制代码

2. /api/v1/payment/pay/getServerPort 路由原理

  这种配置方式经过SpringMVC 路由,由SpringMVC的DispatcherServlet 进行转发。调用链如下:

 接下来研究其调用过程:

  从 com.netflix.zuul.http.ZuulServlet#service 代码看出,其过程分为路由前、路由、路由后, 如果发生错误就走error类型的filter。其中RequestContext 用于整个流程中的数据共享。各种filter的运行会走到ZuulRunner。

复制代码
    /**
     * sets HttpServlet request and HttpResponse
     *
     * @param servletRequest
     * @param servletResponse
     */
    public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {

        RequestContext ctx = RequestContext.getCurrentContext();
        if (bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else {
            ctx.setRequest(servletRequest);
        }

        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
    }

    /**
     * executes "post" filterType  ZuulFilters
     *
     * @throws ZuulException
     */
    public void postRoute() throws ZuulException {
        FilterProcessor.getInstance().postRoute();
    }

    /**
     * executes "route" filterType  ZuulFilters
     *
     * @throws ZuulException
     */
    public void route() throws ZuulException {
        FilterProcessor.getInstance().route();
    }

    /**
     * executes "pre" filterType  ZuulFilters
     *
     * @throws ZuulException
     */
    public void preRoute() throws ZuulException {
        FilterProcessor.getInstance().preRoute();
    }

    /**
     * executes "error" filterType  ZuulFilters
     */
    public void error() {
        FilterProcessor.getInstance().error();
    }
View Code
复制代码

    继续调用到FilterProcessor相关代码

复制代码
    /**
     * runs "post" filters which are called after "route" filters. ZuulExceptions from ZuulFilters are thrown.
     * Any other Throwables are caught and a ZuulException is thrown out with a 500 status code
     *
     * @throws ZuulException
     */
    public void postRoute() throws ZuulException {
        try {
            runFilters("post");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName());
        }
    }

    /**
     * runs all "error" filters. These are called only if an exception occurs. Exceptions from this are swallowed and logged so as not to bubble up.
     */
    public void error() {
        try {
            runFilters("error");
        } catch (Throwable e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * Runs all "route" filters. These filters route calls to an origin.
     *
     * @throws ZuulException if an exception occurs.
     */
    public void route() throws ZuulException {
        try {
            runFilters("route");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
        }
    }

    /**
     * runs all "pre" filters. These filters are run before routing to the orgin.
     *
     * @throws ZuulException
     */
    public void preRoute() throws ZuulException {
        try {
            runFilters("pre");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
        }
    }

    /**
     * runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
     *
     * @param sType the filterType.
     * @return
     * @throws Throwable throws up an arbitrary exception
     */
    public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }
        boolean bResult = false;
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ZuulFilter zuulFilter = list.get(i);
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
    }

    /**
     * Processes an individual ZuulFilter. This method adds Debug information. Any uncaught Thowables are caught by this method and converted to a ZuulException with a 500 status code.
     *
     * @param filter
     * @return the return value for that filter
     * @throws ZuulException
     */
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

        RequestContext ctx = RequestContext.getCurrentContext();
        boolean bDebug = ctx.debugRouting();
        final String metricPrefix = "zuul.filter-";
        long execTime = 0;
        String filterName = "";
        try {
            long ltime = System.currentTimeMillis();
            filterName = filter.getClass().getSimpleName();
            
            RequestContext copy = null;
            Object o = null;
            Throwable t = null;

            if (bDebug) {
                Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
                copy = ctx.copy();
            }
            
            ZuulFilterResult result = filter.runFilter();
            ExecutionStatus s = result.getStatus();
            execTime = System.currentTimeMillis() - ltime;

            switch (s) {
                case FAILED:
                    t = result.getException();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                    break;
                case SUCCESS:
                    o = result.getResult();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                    if (bDebug) {
                        Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                        Debug.compareContextState(filterName, copy);
                    }
                    break;
                default:
                    break;
            }
            
            if (t != null) throw t;

            usageNotifier.notify(filter, s);
            return o;

        } catch (Throwable e) {
            if (bDebug) {
                Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
            }
            usageNotifier.notify(filter, ExecutionStatus.FAILED);
            if (e instanceof ZuulException) {
                throw (ZuulException) e;
            } else {
                ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                throw ex;
            }
        }
    }
View Code
复制代码

    可以看到runFilters 是类似于链条模式的调用方法,遍历集合中所有的过滤器,然后逐个进行调用。

1. preFilter

  包含6个前置处理器。

1. org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter

复制代码
public class ServletDetectionFilter extends ZuulFilter {

    public ServletDetectionFilter() {
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    /**
     * Must run before other filters that rely on the difference between DispatcherServlet
     * and ZuulServlet.
     */
    @Override
    public int filterOrder() {
        return SERVLET_DETECTION_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        if (!(request instanceof HttpServletRequestWrapper)
                && isDispatcherServletRequest(request)) {
            ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
        }
        else {
            ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
        }

        return null;
    }

    private boolean isDispatcherServletRequest(HttpServletRequest request) {
        return request.getAttribute(
                DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null;
    }

}
View Code
复制代码

  这个filter 判断请求是来自SpringMVC的Dispatcher还是直接来自ZuulServlet,也就是如果以/cloud-zuul/ 开头的路由这个是false,否则放的状态位为true。

  检测依据是 isDispatcherServletRequest 方法 根据request 域中是否有org.springframework.web.servlet.DispatcherServlet#WEB_APPLICATION_CONTEXT_ATTRIBUTE 相关的属性, 如果走SptingMVC 会在org.springframework.web.servlet.DispatcherServlet#doService 放置相关属性:

        // Make framework objects available to handlers and view objects.
        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

2. org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter

    这个ilter 根据request 请求来源选择性包装,如果是来自SpringMVC 就包装,否则就不包装。

复制代码
public class Servlet30WrapperFilter extends ZuulFilter {

    private Field requestField = null;

    public Servlet30WrapperFilter() {
        this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class,
                "req", HttpServletRequest.class);
        Assert.notNull(this.requestField,
                "HttpServletRequestWrapper.req field not found");
        this.requestField.setAccessible(true);
    }

    protected Field getRequestField() {
        return this.requestField;
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return SERVLET_30_WRAPPER_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return true; // TODO: only if in servlet 3.0 env
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        if (request instanceof HttpServletRequestWrapper) {
            request = (HttpServletRequest) ReflectionUtils.getField(this.requestField,
                    request);
            ctx.setRequest(new Servlet30RequestWrapper(request));
        }
        else if (RequestUtils.isDispatcherServletRequest()) {
            // If it's going through the dispatcher we need to buffer the body
            ctx.setRequest(new Servlet30RequestWrapper(request));
        }
        return null;
    }

}
View Code
复制代码

3. cn.qz.cloud.filter.PreFilter

    这是自己的前置过滤器,打印一些日志。

4. org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter

    对于来自SpringMVC的请求并且Content-Type是multipart/form-data(文件上传)、或者Content-Type = application/x-www-form-urlencoded (表单提交)的请求进行包装。

复制代码
public class FormBodyWrapperFilter extends ZuulFilter {

    private FormHttpMessageConverter formHttpMessageConverter;

    private Field requestField;

    private Field servletRequestField;

    public FormBodyWrapperFilter() {
        this(new AllEncompassingFormHttpMessageConverter());
    }

    public FormBodyWrapperFilter(FormHttpMessageConverter formHttpMessageConverter) {
        this.formHttpMessageConverter = formHttpMessageConverter;
        this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class,
                "req", HttpServletRequest.class);
        this.servletRequestField = ReflectionUtils.findField(ServletRequestWrapper.class,
                "request", ServletRequest.class);
        Assert.notNull(this.requestField,
                "HttpServletRequestWrapper.req field not found");
        Assert.notNull(this.servletRequestField,
                "ServletRequestWrapper.request field not found");
        this.requestField.setAccessible(true);
        this.servletRequestField.setAccessible(true);
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FORM_BODY_WRAPPER_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String contentType = request.getContentType();
        // Don't use this filter on GET method
        if (contentType == null) {
            return false;
        }
        // Only use this filter for form data and only for multipart data in a
        // DispatcherServlet handler
        try {
            MediaType mediaType = MediaType.valueOf(contentType);
            return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)
                    || (isDispatcherServletRequest(request)
                            && MediaType.MULTIPART_FORM_DATA.includes(mediaType));
        }
        catch (InvalidMediaTypeException ex) {
            return false;
        }
    }

    private boolean isDispatcherServletRequest(HttpServletRequest request) {
        return request.getAttribute(
                DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        FormBodyRequestWrapper wrapper = null;
        if (request instanceof HttpServletRequestWrapper) {
            HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils
                    .getField(this.requestField, request);
            wrapper = new FormBodyRequestWrapper(wrapped);
            ReflectionUtils.setField(this.requestField, request, wrapper);
            if (request instanceof ServletRequestWrapper) {
                ReflectionUtils.setField(this.servletRequestField, request, wrapper);
            }
        }
        else {
            wrapper = new FormBodyRequestWrapper(request);
            ctx.setRequest(wrapper);
        }
        if (wrapper != null) {
            ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
        }
        return null;
    }

    private class FormBodyRequestWrapper extends Servlet30RequestWrapper {

        private HttpServletRequest request;

        private volatile byte[] contentData;

        private MediaType contentType;

        private int contentLength;

        FormBodyRequestWrapper(HttpServletRequest request) {
            super(request);
            this.request = request;
        }

        @Override
        public String getContentType() {
            if (this.contentData == null) {
                buildContentData();
            }
            return this.contentType.toString();
        }

        @Override
        public int getContentLength() {
            if (super.getContentLength() <= 0) {
                return super.getContentLength();
            }
            if (this.contentData == null) {
                buildContentData();
            }
            return this.contentLength;
        }

        public long getContentLengthLong() {
            return getContentLength();
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (this.contentData == null) {
                buildContentData();
            }
            return new ServletInputStreamWrapper(this.contentData);
        }

        private synchronized void buildContentData() {
            if (this.contentData != null) {
                return;
            }
            try {
                MultiValueMap<String, Object> builder = RequestContentDataExtractor
                        .extract(this.request);
                FormHttpOutputMessage data = new FormHttpOutputMessage();

                this.contentType = MediaType.valueOf(this.request.getContentType());
                data.getHeaders().setContentType(this.contentType);
                FormBodyWrapperFilter.this.formHttpMessageConverter.write(builder,
                        this.contentType, data);
                // copy new content type including multipart boundary
                this.contentType = data.getHeaders().getContentType();
                byte[] input = data.getInput();
                this.contentLength = input.length;
                this.contentData = input;
            }
            catch (Exception e) {
                throw new IllegalStateException("Cannot convert form data", e);
            }
        }

        private class FormHttpOutputMessage implements HttpOutputMessage {

            private HttpHeaders headers = new HttpHeaders();

            private ByteArrayOutputStream output = new ByteArrayOutputStream();

            @Override
            public HttpHeaders getHeaders() {
                return this.headers;
            }

            @Override
            public OutputStream getBody() throws IOException {
                return this.output;
            }

            public byte[] getInput() throws IOException {
                this.output.flush();
                return this.output.toByteArray();
            }

        }

    }

}
View Code
复制代码

5. org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter

复制代码
public class DebugFilter extends ZuulFilter {

    private static final DynamicBooleanProperty ROUTING_DEBUG = DynamicPropertyFactory
            .getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, false);

    private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory
            .getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "debug");

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return DEBUG_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
            return true;
        }
        return ROUTING_DEBUG.get();
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.setDebugRouting(true);
        ctx.setDebugRequest(true);
        return null;
    }

}
View Code
复制代码

  debug 设置debug参数。默认是false, 不走该filter。

6. org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter

  这个filter比较重要,会放一些路由相关的信息,从RouteLocator 获取路由信息。转发或者以服务名路由都是在这里进行提取的。提取到后面的服务名称,然后用key org.springframework.cloud.netflix.zuul.filters.support.FilterConstants#SERVICE_ID_KEY 维护到RequestContext 中。

复制代码
public class PreDecorationFilter extends ZuulFilter {

    private static final Log log = LogFactory.getLog(PreDecorationFilter.class);

    /**
     * @deprecated use {@link FilterConstants#PRE_DECORATION_FILTER_ORDER}
     */
    @Deprecated
    public static final int FILTER_ORDER = PRE_DECORATION_FILTER_ORDER;

    /**
     * A double slash pattern.
     */
    public static final Pattern DOUBLE_SLASH = Pattern.compile("//");

    private RouteLocator routeLocator;

    private String dispatcherServletPath;

    private ZuulProperties properties;

    private UrlPathHelper urlPathHelper = new UrlPathHelper();

    private ProxyRequestHelper proxyRequestHelper;

    public PreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath,
            ZuulProperties properties, ProxyRequestHelper proxyRequestHelper) {
        this.routeLocator = routeLocator;
        this.properties = properties;
        this.urlPathHelper
                .setRemoveSemicolonContent(properties.isRemoveSemicolonContent());
        this.urlPathHelper.setUrlDecode(properties.isDecodeUrl());
        this.dispatcherServletPath = dispatcherServletPath;
        this.proxyRequestHelper = proxyRequestHelper;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER;
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
                && !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined
        // serviceId
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        final String requestURI = this.urlPathHelper
                .getPathWithinApplication(ctx.getRequest());
        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());
                }

                if (location.startsWith(HTTP_SCHEME + ":")
                        || location.startsWith(HTTPS_SCHEME + ":")) {
                    ctx.setRouteHost(getUrl(location));
                    ctx.addOriginResponseHeader(SERVICE_HEADER, location);
                }
                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;
                }
                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);

            ctx.set(FORWARD_TO_KEY, forwardURI);
        }
        return null;
    }

    /* for testing */ String getForwardUri(String requestURI) {
        // default fallback servlet is DispatcherServlet
        String fallbackPrefix = this.dispatcherServletPath;

        String fallBackUri = requestURI;
        if (RequestUtils.isZuulServletRequest()) {
            // remove the Zuul servletPath from the requestUri
            log.debug("zuulServletPath=" + this.properties.getServletPath());
            fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), "");
            log.debug("Replaced Zuul servlet path:" + fallBackUri);
        }
        else if (this.dispatcherServletPath != null) {
            // remove the DispatcherServlet servletPath from the requestUri
            log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
            fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
            log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
        }
        if (!fallBackUri.startsWith("/")) {
            fallBackUri = "/" + fallBackUri;
        }

        String forwardURI = (fallbackPrefix == null) ? fallBackUri
                : fallbackPrefix + fallBackUri;
        forwardURI = DOUBLE_SLASH.matcher(forwardURI).replaceAll("/");
        return forwardURI;
    }

    private void addProxyHeaders(RequestContext ctx, Route route) {
        HttpServletRequest request = ctx.getRequest();
        String host = toHostHeader(request);
        String port = String.valueOf(request.getServerPort());
        String proto = request.getScheme();
        if (hasHeader(request, X_FORWARDED_HOST_HEADER)) {
            host = request.getHeader(X_FORWARDED_HOST_HEADER) + "," + host;
        }
        if (!hasHeader(request, X_FORWARDED_PORT_HEADER)) {
            if (hasHeader(request, X_FORWARDED_PROTO_HEADER)) {
                StringBuilder builder = new StringBuilder();
                for (String previous : StringUtils.commaDelimitedListToStringArray(
                        request.getHeader(X_FORWARDED_PROTO_HEADER))) {
                    if (builder.length() > 0) {
                        builder.append(",");
                    }
                    builder.append(
                            HTTPS_SCHEME.equals(previous) ? HTTPS_PORT : HTTP_PORT);
                }
                builder.append(",").append(port);
                port = builder.toString();
            }
        }
        else {
            port = request.getHeader(X_FORWARDED_PORT_HEADER) + "," + port;
        }
        if (hasHeader(request, X_FORWARDED_PROTO_HEADER)) {
            proto = request.getHeader(X_FORWARDED_PROTO_HEADER) + "," + proto;
        }
        ctx.addZuulRequestHeader(X_FORWARDED_HOST_HEADER, host);
        ctx.addZuulRequestHeader(X_FORWARDED_PORT_HEADER, port);
        ctx.addZuulRequestHeader(X_FORWARDED_PROTO_HEADER, proto);
        addProxyPrefix(ctx, route);
    }

    private boolean hasHeader(HttpServletRequest request, String name) {
        return StringUtils.hasLength(request.getHeader(name));
    }

    private void addProxyPrefix(RequestContext ctx, Route route) {
        String forwardedPrefix = ctx.getRequest().getHeader(X_FORWARDED_PREFIX_HEADER);
        String contextPath = ctx.getRequest().getContextPath();
        String prefix = StringUtils.hasLength(forwardedPrefix) ? forwardedPrefix
                : (StringUtils.hasLength(contextPath) ? contextPath : null);
        if (StringUtils.hasText(route.getPrefix())) {
            StringBuilder newPrefixBuilder = new StringBuilder();
            if (prefix != null) {
                if (prefix.endsWith("/") && route.getPrefix().startsWith("/")) {
                    newPrefixBuilder.append(prefix, 0, prefix.length() - 1);
                }
                else {
                    newPrefixBuilder.append(prefix);
                }
            }
            newPrefixBuilder.append(route.getPrefix());
            prefix = newPrefixBuilder.toString();
        }
        if (prefix != null) {
            ctx.addZuulRequestHeader(X_FORWARDED_PREFIX_HEADER, prefix);
        }
    }

    private String toHostHeader(HttpServletRequest request) {
        int port = request.getServerPort();
        if ((port == HTTP_PORT && HTTP_SCHEME.equals(request.getScheme()))
                || (port == HTTPS_PORT && HTTPS_SCHEME.equals(request.getScheme()))) {
            return request.getServerName();
        }
        else {
            return request.getServerName() + ":" + port;
        }
    }

    private URL getUrl(String target) {
        try {
            return new URL(target);
        }
        catch (MalformedURLException ex) {
            throw new IllegalStateException("Target URL is malformed", ex);
        }
    }

}
View Code
复制代码

2. routeFilter 路由处理器

1. RibbonRoutingFilter

  使用ribbon替换服务名称为实际的IP和port, 然后用httpclient 发送请求。

复制代码
public class RibbonRoutingFilter extends ZuulFilter {

    private static final Log log = LogFactory.getLog(RibbonRoutingFilter.class);

    protected ProxyRequestHelper helper;

    protected RibbonCommandFactory<?> ribbonCommandFactory;

    protected List<RibbonRequestCustomizer> requestCustomizers;

    private boolean useServlet31 = true;

    public RibbonRoutingFilter(ProxyRequestHelper helper,
            RibbonCommandFactory<?> ribbonCommandFactory,
            List<RibbonRequestCustomizer> requestCustomizers) {
        this.helper = helper;
        this.ribbonCommandFactory = ribbonCommandFactory;
        this.requestCustomizers = requestCustomizers;
        // To support Servlet API 3.1 we need to check if getContentLengthLong exists
        // Spring 5 minimum support is 3.0, so this stays
        try {
            HttpServletRequest.class.getMethod("getContentLengthLong");
        }
        catch (NoSuchMethodException e) {
            useServlet31 = false;
        }
    }

    @Deprecated
    // TODO Remove in 2.1.x
    public RibbonRoutingFilter(RibbonCommandFactory<?> ribbonCommandFactory) {
        this(new ProxyRequestHelper(), ribbonCommandFactory, null);
    }

    /* for testing */ boolean isUseServlet31() {
        return useServlet31;
    }

    @Override
    public String filterType() {
        return ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        return RIBBON_ROUTING_FILTER_ORDER;
    }

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

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        this.helper.addIgnoredHeaders();
        try {
            RibbonCommandContext commandContext = buildCommandContext(context);
            ClientHttpResponse response = forward(commandContext);
            setResponse(response);
            return response;
        }
        catch (ZuulException ex) {
            throw new ZuulRuntimeException(ex);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
    }

    protected RibbonCommandContext buildCommandContext(RequestContext context) {
        HttpServletRequest request = context.getRequest();

        MultiValueMap<String, String> headers = this.helper
                .buildZuulRequestHeaders(request);
        MultiValueMap<String, String> params = this.helper
                .buildZuulRequestQueryParams(request);
        String verb = getVerb(request);
        InputStream requestEntity = getRequestBody(request);
        if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) {
            context.setChunkedRequestBody();
        }

        String serviceId = (String) context.get(SERVICE_ID_KEY);
        Boolean retryable = (Boolean) context.get(RETRYABLE_KEY);
        Object loadBalancerKey = context.get(LOAD_BALANCER_KEY);

        String uri = this.helper.buildZuulRequestURI(request);

        // remove double slashes
        uri = uri.replace("//", "/");

        long contentLength = useServlet31 ? request.getContentLengthLong()
                : request.getContentLength();

        return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params,
                requestEntity, this.requestCustomizers, contentLength, loadBalancerKey);
    }

    protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
        Map<String, Object> info = this.helper.debug(context.getMethod(),
                context.getUri(), context.getHeaders(), context.getParams(),
                context.getRequestEntity());

        RibbonCommand command = this.ribbonCommandFactory.create(context);
        try {
            ClientHttpResponse response = command.execute();
            this.helper.appendDebug(info, response.getRawStatusCode(),
                    response.getHeaders());
            return response;
        }
        catch (HystrixRuntimeException ex) {
            return handleException(info, ex);
        }

    }

    protected ClientHttpResponse handleException(Map<String, Object> info,
            HystrixRuntimeException ex) throws ZuulException {
        int statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
        Throwable cause = ex;
        String message = ex.getFailureType().toString();

        ClientException clientException = findClientException(ex);
        if (clientException == null) {
            clientException = findClientException(ex.getFallbackException());
        }

        if (clientException != null) {
            if (clientException
                    .getErrorType() == ClientException.ErrorType.SERVER_THROTTLED) {
                statusCode = HttpStatus.SERVICE_UNAVAILABLE.value();
            }
            cause = clientException;
            message = clientException.getErrorType().toString();
        }
        info.put("status", String.valueOf(statusCode));
        throw new ZuulException(cause, "Forwarding error", statusCode, message);
    }

    protected ClientException findClientException(Throwable t) {
        if (t == null) {
            return null;
        }
        if (t instanceof ClientException) {
            return (ClientException) t;
        }
        return findClientException(t.getCause());
    }

    protected InputStream getRequestBody(HttpServletRequest request) {
        InputStream requestEntity = null;
        try {
            requestEntity = (InputStream) RequestContext.getCurrentContext()
                    .get(REQUEST_ENTITY_KEY);
            if (requestEntity == null) {
                requestEntity = request.getInputStream();
            }
        }
        catch (IOException ex) {
            log.error("Error during getRequestBody", ex);
        }
        return requestEntity;
    }

    protected String getVerb(HttpServletRequest request) {
        String method = request.getMethod();
        if (method == null) {
            return "GET";
        }
        return method;
    }

    protected void setResponse(ClientHttpResponse resp)
            throws ClientException, IOException {
        RequestContext.getCurrentContext().set("zuulResponse", resp);
        this.helper.setResponse(resp.getRawStatusCode(),
                resp.getBody() == null ? null : resp.getBody(), resp.getHeaders());
    }

}
View Code
复制代码

  可以看出核心是创建一个RibbonCommand 对象(实际类型是HttpClientRibbonCommand),然后开始转发请求(这实际是zuul对Ribbon 做的封装)。然后会调用到HttpClientRibbonCommand 父类org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand#run 方法

复制代码
    @Override
    protected ClientHttpResponse run() throws Exception {
        final RequestContext context = RequestContext.getCurrentContext();

        RQ request = createRequest();
        RS response;

        boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
                && ((AbstractLoadBalancingClient) this.client)
                        .isClientRetryable((ContextAwareRequest) request);

        if (retryableClient) {
            response = this.client.execute(request, config);
        }
        else {
            response = this.client.executeWithLoadBalancer(request, config);
        }
        context.set("ribbonResponse", response);

        // Explicitly close the HttpResponse if the Hystrix command timed out to
        // release the underlying HTTP connection held by the response.
        //
        if (this.isResponseTimedOut()) {
            if (response != null) {
                response.close();
            }
        }

        return new RibbonHttpResponse(response);
    }
View Code
复制代码

    然后调用到com.netflix.client.AbstractLoadBalancerAwareClient#executeWithLoadBalancer(S, com.netflix.client.config.IClientConfig), 开始走Ribbon 进行调用。(这里之后就相当于请求交给Ribbon 去发起请求)

复制代码
    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        try {
            return command.submit(
                new ServerOperation<T>() {
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ClientException) {
                throw (ClientException) t;
            } else {
                throw new ClientException(e);
            }
        }
        
    }
View Code
复制代码

(1) finalUri 是生成最后的uri, 替换掉服务名称的uri

(2) 继续调用org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient#execute

复制代码
    public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request,
            final IClientConfig configOverride) throws Exception {
        IClientConfig config = configOverride != null ? configOverride : this.config;
        RibbonProperties ribbon = RibbonProperties.from(config);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(ribbon.connectTimeout(this.connectTimeout))
                .setSocketTimeout(ribbon.readTimeout(this.readTimeout))
                .setRedirectsEnabled(ribbon.isFollowRedirects(this.followRedirects))
                .setContentCompressionEnabled(ribbon.isGZipPayload(this.gzipPayload))
                .build();

        request = getSecureRequest(request, configOverride);
        final HttpUriRequest httpUriRequest = request.toRequest(requestConfig);
        final HttpResponse httpResponse = this.delegate.execute(httpUriRequest);
        return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI());
    }
View Code
复制代码

  这里实际就是委托给相应的delegate 客户端去发送请求,默认走的是org.apache.http.impl.client.CloseableHttpClient#execute(org.apache.http.client.methods.HttpUriRequest) -》 位于HttpClient 包的客户端。

 

  响应回来之后org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter#setResponse 将响应记录到RequestContext 中便于后面响应结果。

 

2. org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter

    这个filter 实际负责不需要负载均衡的路由的转发,也就是不走Ribbon 机制的一些路由转发。比如上面走 /guoji/ 的服务就是从这个类进行发送请求的。

复制代码
package org.springframework.cloud.netflix.zuul.filters.route;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.http.HttpServletRequest;

import com.netflix.client.ClientException;
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.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;

import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.Host;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import org.springframework.context.ApplicationListener;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.REQUEST_ENTITY_KEY;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ROUTE_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SIMPLE_HOST_ROUTING_FILTER_ORDER;

/**
 * Route {@link ZuulFilter} that sends requests to predetermined URLs via apache
 * {@link HttpClient}. URLs are found in {@link RequestContext#getRouteHost()}.
 *
 * @author Spencer Gibb
 * @author Dave Syer
 * @author Bilal Alp
 * @author Gang Li
 * @author Denys Ivano
 */
public class SimpleHostRoutingFilter extends ZuulFilter
        implements ApplicationListener<EnvironmentChangeEvent> {

    private static final Log log = LogFactory.getLog(SimpleHostRoutingFilter.class);

    private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("/{2,}");

    private final Timer connectionManagerTimer = new Timer(
            "SimpleHostRoutingFilter.connectionManagerTimer", true);

    private boolean sslHostnameValidationEnabled;

    private boolean forceOriginalQueryStringEncoding;

    private ProxyRequestHelper helper;

    private Host hostProperties;

    private ApacheHttpClientConnectionManagerFactory connectionManagerFactory;

    private ApacheHttpClientFactory httpClientFactory;

    private HttpClientConnectionManager connectionManager;

    private CloseableHttpClient httpClient;

    private boolean customHttpClient = false;

    private boolean useServlet31 = true;

    @Override
    @SuppressWarnings("Deprecation")
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        onPropertyChange(event);
    }

    @Deprecated
    public void onPropertyChange(EnvironmentChangeEvent event) {
        if (!customHttpClient) {
            boolean createNewClient = false;

            for (String key : event.getKeys()) {
                if (key.startsWith("zuul.host.")) {
                    createNewClient = true;
                    break;
                }
            }

            if (createNewClient) {
                try {
                    this.httpClient.close();
                }
                catch (IOException ex) {
                    log.error("error closing client", ex);
                }
                // Re-create connection manager (may be shut down on HTTP client close)
                try {
                    this.connectionManager.shutdown();
                }
                catch (RuntimeException ex) {
                    log.error("error shutting down connection manager", ex);
                }
                this.connectionManager = newConnectionManager();
                this.httpClient = newClient();
            }
        }
    }

    public SimpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties properties,
            ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
            ApacheHttpClientFactory httpClientFactory) {
        this.helper = helper;
        this.hostProperties = properties.getHost();
        this.sslHostnameValidationEnabled = properties.isSslHostnameValidationEnabled();
        this.forceOriginalQueryStringEncoding = properties
                .isForceOriginalQueryStringEncoding();
        this.connectionManagerFactory = connectionManagerFactory;
        this.httpClientFactory = httpClientFactory;
        checkServletVersion();
    }

    public SimpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties properties,
            CloseableHttpClient httpClient) {
        this.helper = helper;
        this.hostProperties = properties.getHost();
        this.sslHostnameValidationEnabled = properties.isSslHostnameValidationEnabled();
        this.forceOriginalQueryStringEncoding = properties
                .isForceOriginalQueryStringEncoding();
        this.httpClient = httpClient;
        this.customHttpClient = true;
        checkServletVersion();
    }

    @PostConstruct
    private void initialize() {
        if (!customHttpClient) {
            this.connectionManager = newConnectionManager();
            this.httpClient = newClient();
            this.connectionManagerTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    if (SimpleHostRoutingFilter.this.connectionManager == null) {
                        return;
                    }
                    SimpleHostRoutingFilter.this.connectionManager
                            .closeExpiredConnections();
                }
            }, 30000, 5000);
        }
    }

    @PreDestroy
    public void stop() {
        this.connectionManagerTimer.cancel();
    }

    @Override
    public String filterType() {
        return ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        return SIMPLE_HOST_ROUTING_FILTER_ORDER;
    }

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

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        MultiValueMap<String, String> headers = this.helper
                .buildZuulRequestHeaders(request);
        MultiValueMap<String, String> params = this.helper
                .buildZuulRequestQueryParams(request);
        String verb = getVerb(request);
        InputStream requestEntity = getRequestBody(request);
        if (getContentLength(request) < 0) {
            context.setChunkedRequestBody();
        }

        String uri = this.helper.buildZuulRequestURI(request);
        this.helper.addIgnoredHeaders();

        try {
            CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
                    headers, params, requestEntity);
            setResponse(response);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(handleException(ex));
        }
        return null;
    }

    protected ZuulException handleException(Exception ex) {
        int statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
        Throwable cause = ex;
        String message = ex.getMessage();

        ClientException clientException = findClientException(ex);

        if (clientException != null) {
            if (clientException
                    .getErrorType() == ClientException.ErrorType.SERVER_THROTTLED) {
                statusCode = HttpStatus.SERVICE_UNAVAILABLE.value();
            }
            cause = clientException;
            message = clientException.getErrorType().toString();
        }
        return new ZuulException(cause, "Forwarding error", statusCode, message);
    }

    protected ClientException findClientException(Throwable t) {
        if (t == null) {
            return null;
        }
        if (t instanceof ClientException) {
            return (ClientException) t;
        }
        return findClientException(t.getCause());
    }

    protected void checkServletVersion() {
        // To support Servlet API 3.1 we need to check if getContentLengthLong exists
        // Spring 5 minimum support is 3.0, so this stays
        try {
            HttpServletRequest.class.getMethod("getContentLengthLong");
            useServlet31 = true;
        }
        catch (NoSuchMethodException e) {
            useServlet31 = false;
        }
    }

    protected void setUseServlet31(boolean useServlet31) {
        this.useServlet31 = useServlet31;
    }

    protected HttpClientConnectionManager getConnectionManager() {
        return connectionManager;
    }

    protected HttpClientConnectionManager newConnectionManager() {
        return connectionManagerFactory.newConnectionManager(
                !this.sslHostnameValidationEnabled,
                this.hostProperties.getMaxTotalConnections(),
                this.hostProperties.getMaxPerRouteConnections(),
                this.hostProperties.getTimeToLive(), this.hostProperties.getTimeUnit(),
                null);
    }

    protected CloseableHttpClient newClient() {
        final RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(
                        this.hostProperties.getConnectionRequestTimeoutMillis())
                .setSocketTimeout(this.hostProperties.getSocketTimeoutMillis())
                .setConnectTimeout(this.hostProperties.getConnectTimeoutMillis())
                .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
        return httpClientFactory.createBuilder().setDefaultRequestConfig(requestConfig)
                .setConnectionManager(this.connectionManager).disableRedirectHandling()
                .build();
    }

    private CloseableHttpResponse forward(CloseableHttpClient httpclient, String verb,
            String uri, HttpServletRequest request, MultiValueMap<String, String> headers,
            MultiValueMap<String, String> params, InputStream requestEntity)
            throws Exception {
        Map<String, Object> info = this.helper.debug(verb, uri, headers, params,
                requestEntity);
        URL host = RequestContext.getCurrentContext().getRouteHost();
        HttpHost httpHost = getHttpHost(host);
        uri = StringUtils.cleanPath(
                MULTIPLE_SLASH_PATTERN.matcher(host.getPath() + uri).replaceAll("/"));
        long contentLength = getContentLength(request);

        ContentType contentType = null;

        if (request.getContentType() != null) {
            contentType = ContentType.parse(request.getContentType());
        }

        InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength,
                contentType);

        HttpRequest httpRequest = buildHttpRequest(verb, uri, entity, headers, params,
                request);
        try {
            log.debug(httpHost.getHostName() + " " + httpHost.getPort() + " "
                    + httpHost.getSchemeName());
            CloseableHttpResponse zuulResponse = forwardRequest(httpclient, httpHost,
                    httpRequest);
            this.helper.appendDebug(info, zuulResponse.getStatusLine().getStatusCode(),
                    revertHeaders(zuulResponse.getAllHeaders()));
            return zuulResponse;
        }
        finally {
            // When HttpClient instance is no longer needed,
            // shut down the connection manager to ensure
            // immediate deallocation of all system resources
            // httpclient.getConnectionManager().shutdown();
        }
    }

    protected HttpRequest buildHttpRequest(String verb, String uri,
            InputStreamEntity entity, MultiValueMap<String, String> headers,
            MultiValueMap<String, String> params, HttpServletRequest request) {
        HttpRequest httpRequest;
        String uriWithQueryString = uri + (this.forceOriginalQueryStringEncoding
                ? getEncodedQueryString(request) : this.helper.getQueryString(params));

        switch (verb.toUpperCase()) {
        case "POST":
            HttpPost httpPost = new HttpPost(uriWithQueryString);
            httpRequest = httpPost;
            httpPost.setEntity(entity);
            break;
        case "PUT":
            HttpPut httpPut = new HttpPut(uriWithQueryString);
            httpRequest = httpPut;
            httpPut.setEntity(entity);
            break;
        case "PATCH":
            HttpPatch httpPatch = new HttpPatch(uriWithQueryString);
            httpRequest = httpPatch;
            httpPatch.setEntity(entity);
            break;
        case "DELETE":
            BasicHttpEntityEnclosingRequest entityRequest = new BasicHttpEntityEnclosingRequest(
                    verb, uriWithQueryString);
            httpRequest = entityRequest;
            entityRequest.setEntity(entity);
            break;
        default:
            httpRequest = new BasicHttpRequest(verb, uriWithQueryString);
            log.debug(uriWithQueryString);
        }

        httpRequest.setHeaders(convertHeaders(headers));
        return httpRequest;
    }

    private String getEncodedQueryString(HttpServletRequest request) {
        String query = request.getQueryString();
        return (query != null) ? "?" + query : "";
    }

    private MultiValueMap<String, String> revertHeaders(Header[] headers) {
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        for (Header header : headers) {
            String name = header.getName();
            if (!map.containsKey(name)) {
                map.put(name, new ArrayList<String>());
            }
            map.get(name).add(header.getValue());
        }
        return map;
    }

    private Header[] convertHeaders(MultiValueMap<String, String> headers) {
        List<Header> list = new ArrayList<>();
        for (String name : headers.keySet()) {
            for (String value : headers.get(name)) {
                list.add(new BasicHeader(name, value));
            }
        }
        return list.toArray(new BasicHeader[0]);
    }

    private CloseableHttpResponse forwardRequest(CloseableHttpClient httpclient,
            HttpHost httpHost, HttpRequest httpRequest) throws IOException {
        return httpclient.execute(httpHost, httpRequest);
    }

    private HttpHost getHttpHost(URL host) {
        HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(),
                host.getProtocol());
        return httpHost;
    }

    protected InputStream getRequestBody(HttpServletRequest request) {
        InputStream requestEntity = null;
        try {
            requestEntity = (InputStream) RequestContext.getCurrentContext()
                    .get(REQUEST_ENTITY_KEY);
            if (requestEntity == null) {
                requestEntity = request.getInputStream();
            }
        }
        catch (IOException ex) {
            log.error("error during getRequestBody", ex);
        }
        return requestEntity;
    }

    private String getVerb(HttpServletRequest request) {
        String sMethod = request.getMethod();
        return sMethod.toUpperCase();
    }

    private void setResponse(HttpResponse response) throws IOException {
        RequestContext.getCurrentContext().set("zuulResponse", response);
        this.helper.setResponse(response.getStatusLine().getStatusCode(),
                response.getEntity() == null ? null : response.getEntity().getContent(),
                revertHeaders(response.getAllHeaders()));
    }

    /**
     * Add header names to exclude from proxied response in the current request.
     * @param names names of headers to exclude
     */
    protected void addIgnoredHeaders(String... names) {
        this.helper.addIgnoredHeaders(names);
    }

    /**
     * Determines whether the filter enables the validation for ssl hostnames.
     * @return true if enabled
     */
    boolean isSslHostnameValidationEnabled() {
        return this.sslHostnameValidationEnabled;
    }

    // Get the header value as a long in order to more correctly proxy very large requests
    protected long getContentLength(HttpServletRequest request) {
        if (useServlet31) {
            return request.getContentLengthLong();
        }
        String contentLengthHeader = request.getHeader(HttpHeaders.CONTENT_LENGTH);
        if (contentLengthHeader != null) {
            try {
                return Long.parseLong(contentLengthHeader);
            }
            catch (NumberFormatException e) {
            }
        }
        return request.getContentLength();
    }

}
View Code
复制代码

    其创建如下: org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration#simpleHostRoutingFilter2

复制代码
    @Bean
    @ConditionalOnMissingBean({ SimpleHostRoutingFilter.class })
    public SimpleHostRoutingFilter simpleHostRoutingFilter2(ProxyRequestHelper helper,
            ZuulProperties zuulProperties, CloseableHttpClient httpClient) {
        return new SimpleHostRoutingFilter(helper, zuulProperties, httpClient);
    }
View Code
复制代码

  可以看出其构造方法直接指定了httpClient 客户端,也就是直接使用httpCLient 进行转发请求。

3. org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter

  这个后台路由转发的一些处理。也就是以forward 或者什么路径设置的路由。

复制代码
public class SendForwardFilter extends ZuulFilter {

    protected static final String SEND_FORWARD_FILTER_RAN = "sendForwardFilter.ran";

    @Override
    public String filterType() {
        return ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        return SEND_FORWARD_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return ctx.containsKey(FORWARD_TO_KEY)
                && !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false);
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            String path = (String) ctx.get(FORWARD_TO_KEY);
            RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path);
            if (dispatcher != null) {
                ctx.set(SEND_FORWARD_FILTER_RAN, true);
                if (!ctx.getResponse().isCommitted()) {
                    dispatcher.forward(ctx.getRequest(), ctx.getResponse());
                    ctx.getResponse().flushBuffer();
                }
            }
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }

}
View Code
复制代码

3. postFilter

1. cn.qz.cloud.filter.PostFilter

    这个是我们自己的测试的后置过滤器。 到这一步 RequestContext 信息如下:(可以看到包含的一些信息,包括request 等信息)

 2. org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter

  如果没错误信息且有响应数据就走该过滤器,run 方法增加一些响应头、然后通过writeResponse 通过response.outputStream 进行写入数据。

复制代码
public class SendResponseFilter extends ZuulFilter {

    private static final Log log = LogFactory.getLog(SendResponseFilter.class);

    private boolean useServlet31 = true;

    private ZuulProperties zuulProperties;

    private ThreadLocal<byte[]> buffers;

    @Deprecated
    public SendResponseFilter() {
        this(new ZuulProperties());
    }

    public SendResponseFilter(ZuulProperties zuulProperties) {
        this.zuulProperties = zuulProperties;
        // To support Servlet API 3.1 we need to check if setContentLengthLong exists
        // minimum support in Spring 5 is 3.0 so we need to keep tihs
        try {
            HttpServletResponse.class.getMethod("setContentLengthLong", long.class);
        }
        catch (NoSuchMethodException e) {
            useServlet31 = false;
        }
        buffers = ThreadLocal
                .withInitial(() -> new byte[zuulProperties.getInitialStreamBufferSize()]);
    }

    /* for testing */ boolean isUseServlet31() {
        return useServlet31;
    }

    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return SEND_RESPONSE_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        return context.getThrowable() == null
                && (!context.getZuulResponseHeaders().isEmpty()
                        || context.getResponseDataStream() != null
                        || context.getResponseBody() != null);
    }

    @Override
    public Object run() {
        try {
            addResponseHeaders();
            writeResponse();
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }

    private void writeResponse() throws Exception {
        RequestContext context = RequestContext.getCurrentContext();
        // there is no body to send
        if (context.getResponseBody() == null
                && context.getResponseDataStream() == null) {
            return;
        }
        HttpServletResponse servletResponse = context.getResponse();
        if (servletResponse.getCharacterEncoding() == null) { // only set if not set
            servletResponse.setCharacterEncoding("UTF-8");
        }

        String servletResponseContentEncoding = getResponseContentEncoding(context);
        OutputStream outStream = servletResponse.getOutputStream();
        InputStream is = null;
        try {
            if (context.getResponseBody() != null) {
                String body = context.getResponseBody();
                is = new ByteArrayInputStream(
                        body.getBytes(servletResponse.getCharacterEncoding()));
            }
            else {
                is = context.getResponseDataStream();
                if (is != null && context.getResponseGZipped()) {
                    // if origin response is gzipped, and client has not requested gzip,
                    // decompress stream before sending to client
                    // else, stream gzip directly to client
                    if (isGzipRequested(context)) {
                        servletResponseContentEncoding = "gzip";
                    }
                    else {
                        servletResponseContentEncoding = null;
                        is = handleGzipStream(is);
                    }
                }
            }
            if (servletResponseContentEncoding != null) {
                servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING,
                        servletResponseContentEncoding);
            }

            if (is != null) {
                writeResponse(is, outStream);
            }
        }
        finally {
            /**
             * We must ensure that the InputStream provided by our upstream pooling
             * mechanism is ALWAYS closed even in the case of wrapped streams, which are
             * supplied by pooled sources such as Apache's
             * PoolingHttpClientConnectionManager. In that particular case, the underlying
             * HTTP connection will be returned back to the connection pool iif either
             * close() is explicitly called, a read error occurs, or the end of the
             * underlying stream is reached. If, however a write error occurs, we will end
             * up leaking a connection from the pool without an explicit close()
             *
             * @author Johannes Edmeier
             */
            if (is != null) {
                try {
                    is.close();
                }
                catch (Exception ex) {
                    log.warn("Error while closing upstream input stream", ex);
                }
            }

            // cleanup ThreadLocal when we are all done
            if (buffers != null) {
                buffers.remove();
            }

            try {
                Object zuulResponse = context.get("zuulResponse");
                if (zuulResponse instanceof Closeable) {
                    ((Closeable) zuulResponse).close();
                }
                outStream.flush();
                // The container will close the stream for us
            }
            catch (IOException ex) {
                log.warn("Error while sending response to client: " + ex.getMessage());
            }
        }
    }

    protected InputStream handleGzipStream(InputStream in) throws Exception {
        // Record bytes read during GZip initialization to allow to rewind the stream if
        // needed
        //
        RecordingInputStream stream = new RecordingInputStream(in);
        try {
            return new GZIPInputStream(stream);
        }
        catch (java.util.zip.ZipException | java.io.EOFException ex) {

            if (stream.getBytesRead() == 0) {
                // stream was empty, return the original "empty" stream
                return in;
            }
            else {
                // reset the stream and assume an unencoded response
                log.warn(
                        "gzip response expected but failed to read gzip headers, assuming unencoded response for request "
                                + RequestContext.getCurrentContext().getRequest()
                                        .getRequestURL().toString());

                stream.reset();
                return stream;
            }
        }
        finally {
            stream.stopRecording();
        }
    }

    protected boolean isGzipRequested(RequestContext context) {
        final String requestEncoding = context.getRequest()
                .getHeader(ZuulHeaders.ACCEPT_ENCODING);

        return requestEncoding != null
                && HTTPRequestUtils.getInstance().isGzipped(requestEncoding);
    }

    private String getResponseContentEncoding(RequestContext context) {
        List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
        if (zuulResponseHeaders != null) {
            for (Pair<String, String> it : zuulResponseHeaders) {
                if (ZuulHeaders.CONTENT_ENCODING.equalsIgnoreCase(it.first())) {
                    return it.second();
                }
            }
        }
        return null;
    }

    private void writeResponse(InputStream zin, OutputStream out) throws Exception {
        byte[] bytes = buffers.get();
        int bytesRead = -1;
        while ((bytesRead = zin.read(bytes)) != -1) {
            out.write(bytes, 0, bytesRead);
        }
    }

    private void addResponseHeaders() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletResponse servletResponse = context.getResponse();
        if (this.zuulProperties.isIncludeDebugHeader()) {
            @SuppressWarnings("unchecked")
            List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY);
            if (rd != null) {
                StringBuilder debugHeader = new StringBuilder();
                for (String it : rd) {
                    debugHeader.append("[[[" + it + "]]]");
                }
                servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString());
            }
        }
        List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
        if (zuulResponseHeaders != null) {
            for (Pair<String, String> it : zuulResponseHeaders) {
                if (!ZuulHeaders.CONTENT_ENCODING.equalsIgnoreCase(it.first())) {
                    servletResponse.addHeader(it.first(), it.second());
                }
            }
        }
        if (includeContentLengthHeader(context)) {
            Long contentLength = context.getOriginContentLength();
            if (useServlet31) {
                servletResponse.setContentLengthLong(contentLength);
            }
            else {
                // Try and set some kind of content length if we can safely convert the
                // Long to an int
                if (isLongSafe(contentLength)) {
                    servletResponse.setContentLength(contentLength.intValue());
                }
            }
        }
    }

    private boolean isLongSafe(long value) {
        return value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE;
    }

    protected boolean includeContentLengthHeader(RequestContext context) {
        // Not configured to forward the header
        if (!this.zuulProperties.isSetContentLength()) {
            return false;
        }

        // Only if Content-Length is provided
        if (context.getOriginContentLength() == null) {
            return false;
        }

        // If response is compressed, include header only if we are not about to
        // decompress it
        if (context.getResponseGZipped()) {
            return context.isGzipRequested();
        }

        // Forward it in all other cases
        return true;
    }

    /**
     * InputStream recording bytes read to allow for a reset() until recording is stopped.
     */
    private static class RecordingInputStream extends InputStream {

        private InputStream delegate;

        private ByteArrayOutputStream buffer = new ByteArrayOutputStream();

        RecordingInputStream(InputStream delegate) {
            super();
            this.delegate = Objects.requireNonNull(delegate);
        }

        @Override
        public int read() throws IOException {
            int read = delegate.read();

            if (buffer != null && read != -1) {
                buffer.write(read);
            }

            return read;
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            int read = delegate.read(b, off, len);

            if (buffer != null && read != -1) {
                buffer.write(b, off, read);
            }

            return read;
        }

        public void reset() {
            if (buffer == null) {
                throw new IllegalStateException("Stream is not recording");
            }

            this.delegate = new SequenceInputStream(
                    new ByteArrayInputStream(buffer.toByteArray()), delegate);
            this.buffer = new ByteArrayOutputStream();
        }

        public int getBytesRead() {
            return (buffer == null) ? -1 : buffer.size();
        }

        public void stopRecording() {
            this.buffer = null;
        }

        @Override
        public void close() throws IOException {
            this.delegate.close();
        }

    }

}
View Code
复制代码

3. /cloud-zuul/api/v1/payment/pay/getServerPort 路由原理

  这种配置方式不经过SpringMVC 路由,从tomcat 直接路由到后端对应的ZuulServlet。查看其调用链如下:

 

   可以看出其调用链没经过SpringMVC的DispatcherServlet。

  这种方式经过的Filter和上面的一样,只是这里不经过DispatcherServlet, 所以ServletDetectionFilter 放置的IS_DISPATCHER_SERVLET_REQUEST_KEY 为false。 后面Servlet30WrapperFilter 也不会对Request 进行包装。

  到cn.qz.cloud.filter.PostFilter 的RequestContext 信息如下:

 

4. 文件上传

  下面研究文件上传在两种模式下的区别。

  过SpringMVC和不过MVC的两种方式都可以上传文件。官方建议是大文件上传绕过SpringMVC,直接走第二种ZuulServlet 的方式(/cloud-zuul/** 路径方式),因为过SpringMVC的话会占用内存,相当于会把文件解析一下。

两者区别:

1. 过SpringMVC:

(1) 如果是文件上传请求,会包装request 对象并提取相关参数:在 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法中会调用checkMultipart 方法,然后监测如果是文件上传的请求(根据请求类型判断StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) 就将request 包装为 org.springframework.web.multipart.support.StandardMultipartHttpServletRequest(传到ZuulServlet.service 方法是request 是该类型), 并将相关的文件信息(相对于Tomcat的路径等信息)抽取到 org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#multipartParameterNames 属性中

 (2) org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter#run 前置过滤器会包装表单,这里会占用内存:解析文件(自己测试是在这里调用 解析占用内存), 调用wrapper.getContentType() 会调用org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter.FormBodyRequestWrapper#buildContentData 解析内容

2. 不过SpringMVC

  这种不会对request 进行包装,也不会过FormBodyWrapperFilter,传到ZuulServlet.service 方法的request的实际类型是 org.apache.catalina.connector.RequestFacade。 这种也不会占用内存,对于大文件上传可以采用这种方式。

 

补充:SpringMVC 也可以关掉文件上传解析模块

  如果是文件上传,文件会在SpringMVC的中央处理器入口就提取文件参数,并转换Request。(源码位置: org.springframework.web.servlet.DispatcherServlet#checkMultipart)。 也可以关掉SpringMVC的文件上传(关掉之后文件解析的Resolver 为null,就不会解析文件),yml 配置spring.servlet.multipart.enabled=false。 参考自动配置类org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration。

 

参考: https://www.springcloud.cc/spring-cloud-netflix.html

 

posted @   QiaoZhi  阅读(1084)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2019-04-12 Web项目容器集成ActiveMQ & SpringBoot整合ActiveMQ
2019-04-12 Consumer高级特性
点击右上角即可分享
微信分享提示