spring cloud zuul

1,609 阅读13分钟

1. Router and Filter: Zuul(路由和过滤:Zuul)

路由是微服务架构不缺少的一部分。例如“/”可能映射到web服务,“/api/users”映射到用户管理服务,而“/api/shop”映射到采购服务。Zuul是Netflix中的一个基于JVM的路由器,也是一个服务端负载均衡器。

zuul有下列用途:

  • Authentication(权限验证)
  • Insights
  • Stress Testing(压力测试)
  • Canary Testing(金丝雀测试)
  • Dynamic Routing(动态路由)
  • Service Migration(服务迁移)
  • Load Shedding(负载削减)
  • Security(安全机制)
  • Static Response handling(静态响应处理)
  • Active/Active traffic management(流量管理)

Zuul的规则引擎使规则和过滤器基本上可以用任何JVM语言编写,并具有对Java和Groovy的内置支持。

  • 配置属性zuul.max.host.connections已被两个新属性zuul.host.maxTotalConnectionszuul.host.maxPerRouteConnections取代,它们分别默认为200和20。
  • Hystrix对所有路由的默认隔离模式(ExecutionIsolationStrategy)为SEMAPHORE(信号量隔离)。如果首选该隔离模式,则可以将zuul.ribbonIsolationStrategy更改为THREAD。

1.1 How to Include Zuul(依赖)

要将Zuul包含在您的项目中,请使用group ID为org.springframework.cloud的启动程序,以及artifact ID为spring-cloud-starter-netflix-zuul的工件。有关使用当前Spring Cloud Release Train设置构建系统的详细信息,请参见Spring Cloud Project页面。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

1.2 启用zuul的反向代理

Spring Cloud创建了一个嵌入式Zuul代理,以简化UI应用程序要对一个或多个后端服务进行代理调用的常见用例的开发。此功能对于用户界面代理所需的后端服务很有用,从而避免了为所有后端独立管理CORS和身份验证问题的需求。

要启用它,请使用@EnableZuulProxy注释一个Spring Boot主类。这样做会将请求转发到适当的服务。默认情况下,/ users开头的请求会被代理到id为users的服务(前缀除去)。代理使用功能区来定位要通过发现转发到的实例。所有请求均在hystrix command中执行,因此失败会显示在Hystrix metrics中。当断路器打开时,代理将不会重试连接后端服务。

Zuul starter不包括服务发现客户端,基于service IDs的路由规则,需要提供一个服务发现客户端(Eureka是其中一个选择)。

为了防止自动添加服务,可以设置zuul.ignored-services参数来避免。如果一个服务匹配到一个忽略表达式,并且又在路由映射中明确指定了,那么它就不会被忽略。例如(application.yml):

application.yml

zuul:
  ignoredServices: '*'
  routes:
    users: /myusers/**

在前面的示例中,除users服务外,所有服务均被忽略。

要增加或更改代理路由,可以添加外部配置,如下所示:

application.yml.

zuul:
  routes:
    users: /myusers/**

前面的示例意味着对/myusers的HTTP调用请求将转发到users服务(例如/myusers/101被转发到/101)。

要对路由进行更细粒度的控制,可以分别指定路径和serviceId,如下所示:

application.yml

zuul:
  routes:
    users:
      path: /myusers/**
      serviceId: users_service

前面的示例意味着对/myusers的HTTP调用将转发到users_service服务。该路由必须具有可指定为ant风格模式的路径,/ myusers/*仅匹配一级路径,而/myusers/**匹配任意多级路径。

可以将后端的请求地址指定为serviceId(用于服务发现)或url(用于物理位置),如以下示例所示:

application.yml

zuul:
  routes:
    users:
      path: /myusers/**
      url: https://example.com/users_service

这些简单的url路由不会作为HystrixCommand执行,也不会通过Ribbon负载均衡。想要在HystrixCommand执行或者Ribbon负载均衡,可以指定带有服务器静态列表的serviceId,如下所示:

application.yml

zuul:
  routes:
    echo:
      path: /myusers/**
      serviceId: myusers-service
      stripPrefix: true

hystrix:
  command:
    myusers-service:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: ...

myusers-service:
  ribbon:
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    listOfServers: https://example1.com,http://example2.com
    ConnectTimeout: 1000
    ReadTimeout: 3000
    MaxTotalHttpConnections: 500
    MaxConnectionsPerHost: 100

另一个方法是指定一个服务路由并且为serviceId配置Ribbon客户端(这么做需要在Ribbon中禁用Eureka),如下:

application.yml

zuul:
  routes:
    users:
      path: /myusers/**
      serviceId: users

ribbon:
  eureka:
    enabled: false

users:
  ribbon:
    listOfServers: example.com,google.com

您可以使用regexmapper在serviceId和路由之间提供约定。它使用正则表达式命名组从serviceId中提取变量,并将其注入到路由模式中,如以下示例所示:

ApplicationConfiguration.java

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
    return new PatternServiceRouteMapper(
        "(?<name>^.+)-(?<version>v.+$)",
        "${version}/${name}");
}

前面的示例意味着serviceId为myusers-v1的serviceId映射到路由/v1/myusers/**。可以接受任何正则表达式,但所有命名组必须同时存在于servicePattern和routePattern中。如果servicePattern与serviceId不匹配,则使用默认行为。在前面的示例中,myusers的serviceId被映射到“/myusers/**”路由。默认情况下,此功能是禁用的,仅适用于发现的服务。

要为所有映射添加前缀,请将zuul.prefix设置为一个值,例如/api。默认情况下,在转发请求之前,将从请求中去除代理前缀(您可以使用zuul.stripPrefix=false将此行为关闭)。您还可以关闭从单个路由中去除特定于服务的前缀,如以下示例所示:

application.yml

zuul:
  routes:
    users:
      path: /myusers/**
      stripPrefix: false

zuul.stripPrefix仅适用于zuul.prefix中设置的前缀。它对给定path中定义的前缀没有任何影响。

在前面的示例中,对/myusers/101的请求被转发到users服务上的/myusers/101。

zuul.routes配置实际上绑定到类型为ZuulProperties的对象上。如果查看该对象的属性,则可以看到它也具有retryable标志。将它设置为true,可以在请求失败时使用Ribbon客户端重试。当需要修改使用Ribbon客户端配置的重试操作的参数时,也可以将该标志设置为true。

默认情况下,X-Forwarded-Host头被添加到转发的请求中。要关闭它,请设置zuul.addProxyHeaders = false。默认情况下,前缀路径被去除,转发的请求头中将会有一个X-Forwarded-Prefix头(例如上面例子中的/myusers)。

如果设置默认路由(/),则具有@EnableZuulProxy的应用程序可以充当独立服务器。例如,zuul.route.home:/会将所有流量(“ /**”)路由到“ home”服务。

如果需要更细粒度的忽略,则可以指定要忽略的特定模式。这些模式在路线定位过程开始时进行评估,这意味着模式中应包含前缀以保证匹配。被忽略的模式跨越所有服务,并取代任何其他路由。以下示例显示了如何创建忽略的模式:

application.yml

zuul:
  ignoredPatterns: /**/admin/**
  routes:
    users: /myusers/**

前面的示例意味着所有调用(例如/myusers/101)都被转发到users服务上的/101。但是包含/admin/在内的调用不会转发。

如果您需要保留路由的顺序,则需要使用YAML文件,因为使用properties文件时顺序会丢失。以下示例显示了这样的YAML文件:

application.yml

zuul:
  routes:
    users:
      path: /myusers/**
    legacy:
      path: /**

如果要使用properties文件,则legacy路径可能位于users路径的前面,从而使users路径无法访问。

1.3 Zuul Http Client

Zuul使用的默认HTTP客户端现在是Apache HTTP客户端(而不是已弃用的Ribbon RestClient)支持。要使用RestClient或okhttp3.OkHttpClient,请分别设置ribbon.restclient.enabled = trueribbon.okhttp.enabled = true。如果要自定义Apache HTTP客户端或OK HTTP客户端,请提供ClosableHttpClient或OkHttpClient类型的Bean类。

1.4 Cookies and Sensitive(敏感) Headers

您可以在同一系统中的服务之间共享headers,但是您可能不希望敏感headers泄漏到下游到外部服务器中。您可以在路由配置中指定忽略的headers。Cookies发挥着特殊的作用,因为它们在浏览器中具有定义明确的语义,并且始终将它们视为敏感内容。如果代理的使用者是浏览器,那么下游服务的cookie也会给用户带来麻烦,因为它们都混杂在一起(所有下游服务看起来都来自同一位置)。

如果您对服务的设计很谨慎(例如,如果只有一个下游服务设置cookie),则可以让它们从后端一直流到调用者。另外,如果您的代理设置cookie,并且所有后端服务都属于同一系统,则自然可以简单地共享它们(例如,使用Spring Session将它们链接到某些共享状态)。除此之外,由下游服务设置的任何cookie可能对调用者都不有用,因此建议您将(至少)Set-Cookie和Cookie设置为不属于您域的路由的敏感headers。即使对于属于您网域的路由,在让Cookie在它们和代理之间传送之前,也请尝试仔细考虑其含义。

可以将敏感头配置为每个路由的逗号分隔列表,如以下示例所示:

application.yml

zuul:
  routes:
    users:
      path: /myusers/**
      sensitiveHeaders: Cookie,Set-Cookie,Authorization
      url: https://downstream

这是sensitiveHeaders的默认值,因此除非您想修改,否则无需进行设置。这是Spring Cloud Netflix 1.1中的新增功能(在1.0中,用户无法设置headers,并且所有cookie都双向流动)。

sensitiveHeaders是黑名单,默认值不为空。因此,要使Zuul发送所有headers(除非被ignored标记的),必须将其显式设置为空列表。如果要将Cookie或授权headers传递到后端,则必须这样做。以下示例显示如何使用sensitiveHeaders:

application.yml

zuul:
  routes:
    users:
      path: /myusers/**
      sensitiveHeaders:
      url: https://downstream

您还可以通过设置zuul.sensitiveHeaders来设置敏感headers。如果在某个路由上设置了sensitiveHeaders,它将覆盖全局的sensitiveHeaders设置。

1.5 Ignored Headers(忽略头)

除了route-sensitive headers之外,您还可以为与下游服务交互应丢弃的值(请求和响应)设置一个名为zuul.ignoredHeaders的全局值。默认情况下,如果Spring Security不在classpath中,则它们为空。否则,它们将初始化为一组由Spring Security指定的众所周知的“security” headers(例如,缓存)。在这种情况下,假设下游服务也可以添加这些headers,但是我们需要来自代理的值。要在Spring Security位于classpath上时不丢弃这些security headers,可以将zuul.ignoreSecurityHeaders设置为false。如果您在Spring Security中禁用了HTTP Security响应headers,并且想要下游服务提供的值,则这样做很有用。

1.6 Management Endpoints(管理断点)

默认情况下,如果您将@EnableZuulProxy与Spring Boot Actuator结合使用,则会启用两个附加端点:

  • Routes
  • Filters

1.6.1 Routes Endpoint

到/routes路径处的路由端点的GET请求返回已映射路由的列表:

GET /routes.

{
  /stores/**: "http://localhost:8081"
}

可以通过向/routes添加?format = details查询字符串来请求路由详细信息。这样做会产生以下输出:

GET /routes/details.

{
  "/stores/**": {
    "id": "stores",
    "fullPath": "/stores/**",
    "location": "http://localhost:8081",
    "path": "/**",
    "prefix": "/stores",
    "retryable": false,
    "customSensitiveHeaders": false,
    "prefixStripped": true
  }
}

对/routes的POST请求强制刷新现有路由(例如,当服务目录中发生更改时)。您可以通过将endpoints.routes.enabled设置为false来禁用此端点。

路由应该自动响应服务目录中的更改,但是对/routes的POST请求是强制立即进行更新的方法。

1.6.2 Filters Endpoint(过滤器断点)

到/filters处的filter端点的GET请求将按类型返回Zuul过滤器的映射。对于地图中的每种过滤器类型,您将获得该类型的所有过滤器的列表以及它们的详细信息。

1.7 Strangulation Patterns and Local Forwards(压缩模式和本地转发)

逐步替代旧的接口是一种通用的迁移现有应用程序或者API的方式, 使用不同的具体实现逐步替换它们. Zuul代理是一种很有用的工具, 因为你可以使用这种方式处理所有客户端到旧接口的请求. 只是重定向了一些请求到新的接口.

以下示例显示了“扼杀”方案的配置详细信息:

application.yml

zuul:
  routes:
    first:
      path: /first/**
      url: https://first.example.com
    second:
      path: /second/**
      url: forward:/second
    third:
      path: /third/**
      url: forward:/3rd
    legacy:
      path: /**
      url: https://legacy.example.com

在这个例子中我们逐步替换除了部分请求外所有到"legacy"应用的请求. 路径 /first/** 指向了一个额外的URL. 并且路径 /second/** 是一个跳转, 所以请求可以被本地处理. 比如, 带有Spring注解的 @RequestMapping . 路径 /third/** 也是一个跳转, 但是属于一个不同的前缀. (比如 /third/foo 跳转到 /3rd/foo )

忽略表达式并不是完全的忽略请求, 只是配置这个代理不处理这些请求(所以他们也是跳转执行本地处理)

1.8 Uploading Files through Zuul(通过Zuul上传文件)

如果你使用 @EnableZuulProxy , 你可以使用代理路径上传文件, 它能够一直正常工作只要小文件. 对于大文件有可选的路径"/zuul/"绕过Spring DispatcherServlet (避免处理multipart). 比如对于 zuul.routes.customers=/customers/** , 你可以使用 "/zuul/customers/" 去上传大文件. Servlet路径通过 zuul.servletPath 指定. 如果使用Ribbon负载均衡器的代理路由, 在处理非常大的文件时, 仍然需要提高超时配置. 比如:

application.yml

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

注意: 对于大文件的上传流, 你应该在请求中使用块编码. (有些浏览器默认不这么做). 比如在命令行中:

$ curl -v -H "Transfer-Encoding: chunked" \
    -F "file=@mylarge.iso" localhost:9999/zuul/simple/file

1.9 Query String Encoding(查询字符串编码)

在处理传入请求时,查询参数将被解码,它们可以在Zuul过滤器中进行修改。然后将它们重新编码,在路由过滤器中重建后端请求。例如,如果结果是使用Javascript的encodeURIComponent()方法编码的,则结果可能与原始输入不同。尽管这在大多数情况下不会引起问题,但是某些Web服务器可能对复杂查询字符串的编码很挑剔。

要强制查询字符串的原始编码,可以将特殊标志传递给ZuulProperties,以便使用HttpServletRequest :: getQueryString方法按原样获取查询字符串,如以下示例所示:

application.yml

zuul:
  forceOriginalQueryStringEncoding: true

注意:这个特殊的标志只对SimpleHostRoutingFilter有效,另外可以通过RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)来覆盖查询字符串。

1.10 Request URI Encoding(请求URI编码)

处理传入请求时,在将请求URI与路由匹配之前,先对其进行解码。然后在路由过滤器中重建后端请求时,将对请求URI进行重新编码。如果您的URI包含编码的"/"字符,则可能导致某些意外行为。

要使用原始请求URI,可以向'ZuulProperties'传递一个特殊标志,以便可以使用HttpServletRequest :: getRequestURI方法按原样使用该URI,如以下示例所示:

application.yml

zuul:
  decodeUrl: false

如果使用requestURI RequestContext属性覆盖请求URI,并且此标志设置为false,则将不对在请求上下文中设置的URL进行编码。确保URL已被编码是您的责任。

1.11 Plain Embedded Zuul(纯内置Zuul)

如果你使用 @EnableZuulServer (替代 @EnableZuulProxy ), 这样就可以运行一个没有代理功能的Zuul服务, 或者有选择的开关部分代理功能, 你添加的任何 ZuulFilter 类型的bean都会被自动加载, 和使用 @EnableZuulProxy 一样, 但不会自动加载任何代理过滤器。

在以下例子中, Zuul服务中的路由仍然是按照 "zuul.routes.*"指定, 但是没有服务发现和代理, 因此"serviceId"和"url"配置会被忽略. 比如:

application.yml

zuul:
  routes:
    api: /api/**

1.12 Disable Zuul Filters(禁用zuul过滤器)

在proxy和server模式下, 对于Spring Cloud, Zuul默认加载了一批ZuulFilter类. 查阅 the zuul filters package 去获取可能开启的过滤器. 如果你想关闭其中一个, 可以简单的设置 zuul.<SimpleClassName>.<filterType>.disable=true . 按照约定, 在 filter 后面的包是Zuul过滤器类. 比如关闭 org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter , 可设置zuul.SendResponseFilter.post.disable=true.

1.13 Providing Hystrix Fallbacks For Routes(为路由提供Hystrix Fallbacks)

当Zuul中给定路由的发生熔断时,可以通过创建FallbackProvider类型的bean提供回退响应。在此bean中,您需要指定回退的路由ID,并提供一个ClientHttpResponse作为回退返回。以下示例显示了一个相对简单的FallbackProvider实现:

class MyFallbackProvider implements FallbackProvider {

    @Override
    public String getRoute() {
        return "customers";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
        if (cause instanceof HystrixTimeoutException) {
            return response(HttpStatus.GATEWAY_TIMEOUT);
        } else {
            return response(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private ClientHttpResponse response(final HttpStatus status) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return status;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return status.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return status.getReasonPhrase();
            }

            @Override
            public void close() {
            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("fallback".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

下面的例子是对应上面例子的路由配置:

zuul:
  routes:
    customers: /customers/**

如果要为所有路由提供默认回退,则可以创建FallbackProvider类型的Bean,并使getRoute方法返回*或null,如以下示例所示:

class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("fallback".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

1.14 Zuul Timeouts(zuul超时时间)

如果要为通过Zuul代理的请求配置socket timeouts和read timeouts,则根据您的配置,有两种选择:

  • 如果Zuul使用服务发现(如serviceId方式),则需要使用ribbon.ReadTimeout和ribbon.SocketTimeout Ribbon属性配置这些超时。
  • 如果通过URL方式配置了Zuul路由,则需要使用zuul.host.connect-timeout-millis和zuul.host.socket-timeout-millis。

1.15 Rewriting the Location header(重写location header)

如果Zuul在Web应用程序的前面,则当Web应用程序通过HTTP状态代码3XX重定向时,您可能需要重新编写Location header。 否则,浏览器将重定向到Web应用程序的URL,而不是Zuul URL。您可以配置LocationRewriteFilter Zuul过滤器,以将Location header重写为Zuul的URL。它还会添加全局前缀和特定于路由的前缀。以下示例使用Spring Configuration文件添加过滤器:

import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
...

@Configuration
@EnableZuulProxy
public class ZuulConfig {
    @Bean
    public LocationRewriteFilter locationRewriteFilter() {
        return new LocationRewriteFilter();
    }
}

警告:小心使用此过滤器。这个过滤器作用于所有3XX响应代码的Location header,在某些情况下(例如,将用户重定向到外部URL时),这可能并不适用。

1.16 Enabling Cross Origin Requests(启用跨域请求)

默认情况下,Zuul将所有跨域请求(CORS)路由到服务。如果您想让Zuul处理这些请求,可以通过提供自定义WebMvcConfigurer bean来完成:

@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/path-1/**")
                    .allowedOrigins("https://allowed-origin.com")
                    .allowedMethods("GET", "POST");
        }
    };
}

在上面的示例中,我们允许来自https://allowed-origin.com的GET和POST方法将跨域请求发送到以path-1开头的端点。您可以使用/**映射将CORS配置应用于特定的路径模式或整个应用程序的全局路径。您可以通过此配置来自定义属性:allowedOrigins,allowedMethods,allowedHeaders,exposedHeaders,allowCredentials和maxAge。

1.17 Metrics(指标)

Zuul将在Actuator metrics端点下提供指标,以解决路由请求时可能发生的任何故障。可以通过点击/actuator/metrics查看这些指标。指标的名称格式为ZUUL::EXCEPTION:errorCause:statusCode。

1.18 Zuul Developer Guide(zuul开发者导航)

有关Zuul的工作原理的一般概述,请参见Zuul Wiki

1.18.1 The Zuul Servlet

Zuul是Servlet的一个实现。对于一般情况,Zuul已嵌入到Spring Dispatch机制中。这使Spring MVC可以控制路由。在这种情况下,Zuul缓冲请求。如果需要在不缓冲请求的情况下进行Zuul操作(例如,上传大文件),则Servlet也会安装在Spring Dispatcher的外部。缺省情况下,该servlet的地址为/zuul。可以使用zuul.servlet-path属性更改此路径。

1.18.2 Zuul RequestContext

为了在过滤器之间传递信息,Zuul使用了RequestContext。它的数据保存在每个请求特定的ThreadLocal中。有关在何处路由请求,错误以及实际的HttpServletRequest和HttpServletResponse的信息存储在此处。RequestContext扩展了ConcurrentHashMap,因此任何内容都可以存储在上下文中。FilterConstants包含由Spring Cloud Netflix安装的过滤器使用的密钥(稍后会详细介绍)。

1.18.3 @EnableZuulProxy vs. @EnableZuulServer

Spring Cloud Netflix安装了许多过滤器,具体取决于用于启用Zuul的注解。@EnableZuulProxy是@EnableZuulServer的超集。换句话说,@EnableZuulProxy包含@EnableZuulServer安装的所有过滤器。“代理”中的其他过滤器启用路由功能。如果您需要“空白” Zuul,则应使用@EnableZuulServer。

1.18.4 @EnableZuulServer Filters

@EnableZuulServer创建一个SimpleRouteLocator,该加载器从Spring Boot配置文件中加载路由定义。 安装了以下过滤器(作为普通的Spring Bean):

  • Pre filters:

    • PreDecorationFilter:根据提供的RouteLocator确定路由到哪里。它还为下游请求设置了各种与代理相关的header。
  • Route filters:

    • RibbonRoutingFilter:使用Ribbon,Hystrix和可插拔HTTP客户端发送请求。Service IDs在RequestContext属性,FilterConstants.SERVICE_ID_KEY中可以找到。此过滤器可以使用不同的HTTP客户端:
      • Apache HttpClient:默认客户端。
      • Squareup OkHttpClient v3:添加com.squareup.okhttp3:okhttp依赖,并且设置ribbon.okhttp.enabled=true。
      • Netflix Ribbon HTTP client:设置ribbon.restclient.enabled=true。但是这个客户端有一些限制,它不支持PATCH方法,但是有内建的重试机制。
    • SimpleHostRoutingFilter:通过Apache HttpClient将请求发送到预定的URL。URL可在RequestContext.getRouteHost()中找到。

1.18.6 Custom Zuul Filter Examples(自定义zuul过滤器案例)

下面大多数的例子都包括在Sample Zuul Filters项目中。这个项目中还包含了一些如果修改请求或者响应的消息体的例子。

本节包括以下示例:

How to Write a Pre Filter(如何编写前置过滤器)

前置过滤器会在RequestContext中设置数据,以便在下游的过滤器中使用。主要用例是设置路由过滤器所需的信息。以下示例显示了Zuul前置过滤器:

public class QueryParamPreFilter extends ZuulFilter {
	@Override
	public int filterOrder() {
		return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
	}

	@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();
		HttpServletRequest request = ctx.getRequest();
		if (request.getParameter("sample") != null) {
		    // put the serviceId in `RequestContext`
    		ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
    	}
        return null;
    }
}

上面的过滤器使用sample请求参数填充SERVICE_ID_KEY。实际上不应该做这种直接映射,Service ID应该从sample的值中查找。

现在,SERVICE_ID_KEY已经被填充,所以PreDecorationFilter将不会执行,RibbonRoutingFilter会执行。

注意:如果想路由到一个完整的URL,调用ctx.setRouteHost(url)。

要修改路由过滤器转发到的路径,请设置REQUEST_URI_KEY。

How to Write a Route Filter(如何编写路由过滤器)

路由过滤器在前置过滤器之后运行,并向其他服务发出请求。它转发请求到其他服务。这里的大部分工作是将请求和响应数据转换到客户机所需的模型。

public class OkHttpRoutingFilter extends ZuulFilter {
	@Autowired
	private ProxyRequestHelper helper;

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

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

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

    @Override
    public Object run() {
		OkHttpClient httpClient = new OkHttpClient.Builder()
				// customize
				.build();

		RequestContext context = RequestContext.getCurrentContext();
		HttpServletRequest request = context.getRequest();

		String method = request.getMethod();

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

		Headers.Builder headers = new Headers.Builder();
		Enumeration<String> headerNames = request.getHeaderNames();
		while (headerNames.hasMoreElements()) {
			String name = headerNames.nextElement();
			Enumeration<String> values = request.getHeaders(name);

			while (values.hasMoreElements()) {
				String value = values.nextElement();
				headers.add(name, value);
			}
		}

		InputStream inputStream = request.getInputStream();

		RequestBody requestBody = null;
		if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
			MediaType mediaType = null;
			if (headers.get("Content-Type") != null) {
				mediaType = MediaType.parse(headers.get("Content-Type"));
			}
			requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
		}

		Request.Builder builder = new Request.Builder()
				.headers(headers.build())
				.url(uri)
				.method(method, requestBody);

		Response response = httpClient.newCall(builder.build()).execute();

		LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();

		for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
			responseHeaders.put(entry.getKey(), entry.getValue());
		}

		this.helper.setResponse(response.code(), response.body().byteStream(),
				responseHeaders);
		context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
		return null;
    }
}

前面的过滤器将Servlet请求信息转换为OkHttp3请求信息,执行HTTP请求,并将OkHttp3响应信息转换为Servlet响应。

How to Write a Post Filter(如何编写后置过滤器) 后置过滤器通常修改响应。下面的例子中在响应头中添加一个X-Sample头部并且设置为UUID。

public class AddResponseHeaderFilter extends ZuulFilter {
	@Override
	public String filterType() {
		return POST_TYPE;
	}

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

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

	@Override
	public Object run() {
		RequestContext context = RequestContext.getCurrentContext();
    	HttpServletResponse servletResponse = context.getResponse();
		servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
		return null;
	}
}

注意:其他操作,比如转换响应体,则要复杂得多,计算量也大得多。

1.18.7 How Zuul Errors Work(zuul的错误是怎么工作的)

Zuul过滤器的生命周期的任何阶段出现异常,error过滤器将会执行。仅当RequestContext.getThrowable()不为null时,才运行SendErrorFilter。然后,它在请求中设置特定的javax.servlet.error.*属性,并将请求转发到Spring Boot错误页面。

1.18.8 Zuul Eager Application Context Loading(Zuul Eager应用程序上下文加载)

Zuul在内部使用Ribbon来调用远程URL。默认情况下,Ribbon客户端在第一次被使用时才被Spring Cloud懒加载。可以通过下面的配置来改变默认行为。它会在启动时初始化Ribbon相关的上下文。

application.yml

zuul:
  ribbon:
    eager-load:
      enabled: true