18. Router 和 Filter:Zuul

路由是微服务 architecture 不可或缺的一部分。例如,/可以映射到 web application,/api/users映射到用户服务,/api/shop映射到商店服务。 Zuul是来自 Netflix 的 JVM-based router 和 server-side 负载均衡器。

Netflix 使用 Zuul用于以下内容:

  • 认证

  • 洞察

  • 压力测试

  • 金丝雀测试

  • 动态路由

  • 服务迁移

  • 负载脱落

  • 安全

  • 静态响应处理

  • Active/Active 交通管理

Zuul 的规则引擎允许规则和过滤器基本上以任何 JVM 语言编写,并支持 Java 和 Groovy。

configuration property zuul.max.host.connections已被两个新的 properties 替换,zuul.host.maxTotalConnectionszuul.host.maxPerRouteConnections,默认分别为 200 和 20。

所有 routes 的默认 Hystrix 隔离 pattern(ExecutionIsolationStrategy)是SEMAPHORE。如果首选隔离 pattern,zuul.ribbonIsolationStrategy可以更改为THREAD

18.1 如何包含 Zuul

要在项目中包含 Zuul,请使用 group ID 为org.springframework.cloud且 artifact ID 为spring-cloud-starter-netflix-zuul的 starter。有关使用当前 Spring Cloud Release Train 设置 build 系统的详细信息,请参阅Spring Cloud 项目页面

18.2 嵌入式 Zuul 反向代理

Spring Cloud 创建了一个嵌入式 Zuul 代理,以便于开发一个 common 用例,其中 UI application 想要对一个或多个后端服务进行代理 calls。此 feature 对于用户界面代理其所需的后端服务非常有用,从而无需为所有后端_End 独立管理 CORS 和身份验证问题。

要启用它,请使用@EnableZuulProxy注释 Spring Boot main class。这样做会导致本地 calls 转发到适当的服务。按照惯例,ID 为users的服务从位于/users的代理接收请求(前缀已剥离)。代理使用 Ribbon 来定位要通过发现转发的实例。所有请求都在hystrix 命令中执行,因此失败出现在 Hystrix metrics 中。电路打开后,代理不会尝试联系该服务。

Zuul starter 不包含发现 client,因此,对于基于服务 ID 的 routes,您还需要在 classpath 上提供其中一个(Eureka 是一种选择)。

要跳过自动添加服务,请将zuul.ignored-services设置为服务 ID 模式列表。如果服务与被忽略但又包含在显式配置的 routes map 中的 pattern 匹配,则它是不带号的,如下面的 example 所示:

application.yml.

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

在前面的 example 中,忽略了所有服务,除了users

要扩充或更改代理 routes,可以添加外部 configuration,如下所示:

application.yml.

zuul:
  routes:
    users: /myusers/**

前面的 example 意味着 HTTP calls 到/myusers被转发到users服务(对于 example /myusers/101被转发到/101)。

要获得对 route 的更多 fine-grained 控制,您可以单独指定路径和 serviceId,如下所示:

application.yml.

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

前面的 example 表示 HTTP calls 到/myusers转发到users_service服务。 route 必须具有可以指定为 ant-style pattern 的path,因此/myusers/*仅匹配一个 level,但/myusers/**与层次结构匹配。

后端的位置可以指定为serviceId(对于发现中的服务)或url(对于物理位置),如以下 example 所示:

application.yml.

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

这些简单的 url-routes 不是作为HystrixCommand执行的,也不是 load-balance 多个带有 Ribbon 的 URL。要实现这些目标,您可以使用静态服务器列表指定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: http://example1.com,http://example2.com
    ConnectTimeout: 1000
    ReadTimeout: 3000
    MaxTotalHttpConnections: 500
    MaxConnectionsPerHost: 100

另一种方法是指定 service-route 并为serviceId配置 Ribbon client(这样做需要在 Ribbon 中禁用 Eureka 支持 - 请参阅以上是了解更多信息),如下面的示例所示:

application.yml.

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

ribbon:
  eureka:
    enabled: false

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

您可以使用regexmapperserviceId和 routes 之间提供约定。它使用 regular-expression 命名组从serviceId中提取变量并将它们注入 route pattern,如下面的 example 所示:

ApplicationConfiguration.java.

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

前面的 example 表示myusers-v1myusers-v1映射到 route /v1/myusers/**。接受任何正则表达式,但所有命名组必须同时出现在servicePatternroutePattern中。如果servicePattern不匹配serviceId,则使用默认行为。在前面的 example 中,serviceIdmyusers被映射到“/myusers/ **”route(没有检测到 version)。默认情况下禁用此 feature,仅适用于已发现的服务。

要为所有映射添加前缀,请将zuul.prefix设置为 value,例如/api。默认情况下,在转发请求之前,会从请求中删除代理前缀(您可以使用zuul.stripPrefix=false关闭此行为)。您还可以关闭单个 routes 中的 service-specific 前缀的剥离,如下面的示例所示:

application.yml.

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

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

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

zuul.routes条目实际上绑定到ZuulProperties类型的 object。如果查看该对象的 properties,可以看到它也有一个retryable flag。将 flag 设置为true以使 Ribbon client 自动重试失败的请求。当您需要修改使用 Ribbon client configuration 的重试操作的参数时,也可以将 flag 设置为true

默认情况下,X-Forwarded-Host标头会添加到转发的请求中。要将其关闭,请设置zuul.addProxyHeaders = false。默认情况下,前缀路径被剥离,对后端的请求会获取X-Forwarded-Prefix标头(前面显示的示例中为/myusers)。

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

如果需要更多 fine-grained 忽略,则可以指定要忽略的特定模式。这些模式在 route location process 的开头进行评估,这意味着前缀应该包含在 pattern 中以保证 match。忽略模式 span 所有服务并取代任何其他 route 规范。以下 example 显示了如何创建忽略的模式:

application.yml.

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

前面的 example 表示所有 calls(例如/myusers/101)都转发到users服务上的/101。但是,包括/admin/在内的 calls 无法解析。

如果您需要 routes 保留其 order,则需要使用 YAML 文件,因为使用 properties 文件时 ordering 会丢失。以下 example 显示了这样一个 YAML 文件:

application.yml.

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

如果您要使用 properties 文件,legacy路径可能会在users路径前面结束,从而导致users路径无法访问。

18.3 Zuul Http Client

Zuul 使用的默认 HTTP client 现在由 Apache HTTP Client 支持,而不是弃用的 Ribbon RestClient。要使用RestClientokhttp3.OkHttpClient,请分别设置ribbon.restclient.enabled=trueribbon.okhttp.enabled=true。如果要自定义 Apache HTTP client 或 OK HTTP client,请提供或OkHttpClient类型的 bean。

18.4 Cookies and Sensitive Headers

您可以在同一系统中的服务之间共享 headers,但您可能不希望敏感的 headers 在下游泄漏到外部服务器。您可以在 route configuration 中指定忽略的 headers 列表。 Cookies 扮演着一个特殊的角色,因为它们在浏览器中具有良好定义的语义,并且它们总是被视为敏感的。如果您的代理的 consumer 是浏览器,那么下游服务的 cookies 也会给用户带来问题,因为它们都混杂在一起(所有下游服务看起来都来自同一个地方)。

如果您对服务的设计非常谨慎,(例如,如果只有一个下游服务 sets cookies),您可以让它们从后端一直流到调用者。此外,如果您的代理 sets cookies 和所有 back-end 服务都是同一系统的一部分,则可以很自然地简单地共享它们(例如,使用 Spring Session 将它们链接到一些共享的 state)。除此之外,由下游服务设置的任何 cookies 可能对调用者没有用处,因此建议您(至少)和Cookie进入敏感的_header,以用于不属于您的域的 routes。即使对于属于您的域的 routes,在让 cookies 在它们和代理之间流动之前,请仔细考虑它的含义。

敏感的 headers 可以按 route 配置为 comma-separated 列表,如下面的示例所示:

application.yml.

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

这是sensitiveHeaders的默认 value,因此除非您希望它不同,否则无需进行设置。这是 Spring Cloud Netflix 1.1 中的新功能(在 1.0 中,用户无法控制 headers,并且所有 cookies 都向两个方向流动)。

sensitiveHeaders是黑名单,默认值不为空。因此,要使 Zuul 发送所有 headers(除了ignored之外),您必须将其显式设置为空列表。如果您想将 cookie 或授权 headers 传递给您的后端,则必须这样做。以下 example 显示了如何使用sensitiveHeaders

application.yml.

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

您还可以通过设置zuul.sensitiveHeaders来设置敏感的 headers。如果在 route 上设置了sensitiveHeaders,它将覆盖 global sensitiveHeaders设置。

18.5 忽略了 Headers

除了 route-sensitive headers 之外,您还可以为与下游服务交互期间应丢弃的值(请求和响应)设置名为zuul.ignoredHeaders的 global value。默认情况下,如果 Spring Security 不在 classpath 上,则这些是空的。否则,它们被初始化为一组众所周知的“安全”headers(对于 example,涉及缓存),由 Spring Security 指定。在这种情况下的假设是下游服务也可能添加这些 headers,但我们需要来自代理的值。要在 Spring Security 位于 classpath 时不丢弃这些众所周知的安全性_header,可以将zuul.ignoreSecurityHeaders设置为false。如果您在 Spring Security 中禁用了 HTTP 安全响应 headers 并希望下游服务提供的值,那么这样做会非常有用。

18.6 管理 Endpoints

默认情况下,如果对 Spring Boot Actuator 使用@EnableZuulProxy,则启用另外两个 endpoints:

  • Routes

  • 过滤器

18.6.1 Routes Endpoint

/routes的 routes 端点的 GET 返回映射的 routes 的列表:

GET /routes.

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

可以通过将?format=details query string 添加到/routes来请求其他 route 详细信息。这样做会产生以下输出:

GET /routes/details.

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

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

routes 应自动响应服务目录中的更改,但POST/routes是强制更改立即发生的一种方法。

18.6.2 过滤端点

/filters处的过滤器端点的GET按类型返回 Zuul 过滤器的 map。对于 map 中的每种过滤器类型,您将获得该类型的所有过滤器及其详细信息的列表。

18.7 扼杀模式和本地前锋

迁移现有的 application 或 API 时,common pattern 是“扼杀”旧的 endpoints,慢慢用不同的 implementations 替换它们。 Zuul 代理是一个有用的工具,因为您可以使用它来处理来自旧 endpoints 的 clients 的所有流量,但将一些请求重定向到新的。

以下 example 显示了“strangle”场景的 configuration 详细信息:

application.yml.

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

在前面的例子中,我们扼杀了“legacy”application,它被映射到所有不匹配其他模式之一的请求。 /first/**中的 Paths 已被提取到具有外部 URL 的新服务中。转发/second/**中的 Paths 以便可以在本地处理它们(对于 example,使用正常的 Spring @RequestMapping)。 /third/**中的 Paths 也被转发但具有不同的前缀(/third/foo被转发到/3rd/foo)。

忽略的模式不会被完全忽略,它们只是不由代理处理(因此它们也可以在本地有效转发)。

18.8 通过 Zuul 上传 Files

如果你使用@EnableZuulProxy,你可以使用代理 paths 上传 files 它应该工作,所以 long 因为 files 很小。对于大型 files,在“/zuul/ *”中有一个绕过 Spring DispatcherServlet(以避免 Multipart 处理)的替代路径。换句话说,如果你有zuul.routes.customers=/customers/**,那么你可以POST大 files 到/zuul/customers/*。 servlet 路径通过zuul.servletPath外部化。如果代理 route 引导您完成 Ribbon 负载均衡器,则非常大的 files 也需要提升超时设置,如下面的示例所示:

application.yml.

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

请注意,要使用大型 files 进行流式处理,您需要在请求中使用分块编码(默认情况下某些浏览器不会这样做),如下面的示例所示:

$ curl -v -H "Transfer-Encoding: chunked" \
    -F "[emailprotected]" localhost:9999/zuul/simple/file

18.9 查询 String 编码

处理传入请求时,将对查询参数进行解码,以便它们可用于 Zuul 过滤器中的可能修改。然后它们 re-encoded 后端请求在 route 过滤器中重建。如果(对于 example)它使用 Javascript 的encodeURIComponent()方法编码,结果可能与原始输入不同。虽然这在大多数情况下不会引起任何问题,但是一些 web 服务器可能会因复杂查询 string 的编码而变得挑剔。

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

application.yml.

zuul:
  forceOriginalQueryStringEncoding: true

这个特殊的 flag 只适用于SimpleHostRoutingFilter。此外,您无法使用RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)轻松覆盖查询参数,因为查询 string 现在直接在原始HttpServletRequest上获取。

18.10 Plain Embedded Zuul

如果使用@EnableZuulServer(而不是@EnableZuulProxy),您还可以运行 Zuul 服务器而无需代理或有选择地切换代理平台的某些部分。您添加到ZuulFilter类型的 application 的任何 beans 都会自动安装(与@EnableZuulProxy一样),但不会自动添加任何代理过滤器。

在这种情况下,仍然通过配置“zuul.routes.*”来指定进入 Zuul 服务器的 routes,但是没有服务发现和代理。因此,将忽略“serviceId”和“url”设置。以下 example maps 将“/api/ **”中的所有_path 映射到 Zuul 过滤器链:

application.yml.

zuul:
  routes:
    api: /api/**

18.11 禁用 Zuul 过滤器

Zuul for Spring Cloud 在代理和服务器模式下默认启用了许多ZuulFilter beans。有关可以启用的过滤器列表,请参见Zuul 过滤器包。如果要禁用其中一个,请设置zuul.<SimpleClassName>.<filterType>.disable=true。按照惯例,filters之后的包是 Zuul 过滤器类型。要 example 禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,请设置zuul.SendResponseFilter.post.disable=true

18.12 为 Routes 提供 Hystrix 后备

当 Zuul 中给定 route 的电路跳闸时,您可以通过创建FallbackProvider类型的 bean 来提供回退响应。在此 bean 中,您需要指定回退所用的 route ID,并提供ClientHttpResponse到 return 作为回退。以下 example 显示了一个相对简单的FallbackProvider implementation:

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;
            }
        };
    }
}

以下 example 显示了如何显示前一个 example 的 route configuration:

zuul:
  routes:
    customers: /customers/**

如果要为所有 routes 提供默认回退,可以创建类型的 bean 并使方法 return *null,如下面的 example 所示:

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;
            }
        };
    }
}

18.13 Zuul 超时

如果要为通过 Zuul 代理的请求配置 socket 超时和读取超时,您有两个选项,基于您的 configuration:

  • 如果 Zuul 使用服务发现,则需要使用ribbon.ReadTimeoutribbon.SocketTimeout Ribbon properties 配置这些超时。

如果您通过指定 URL 配置了 Zuul routes,则需要使用zuul.host.connect-timeout-milliszuul.host.socket-timeout-millis

18.14 重写 Location 标头

如果 Zuul 面向 web application,当 web application 通过3XX的 HTTP 状态 code 重定向时,您可能需要 re-write Location标头。否则,浏览器会重定向到 web application 的 URL 而不是 Zuul URL。您可以将LocationRewriteFilter Zuul 过滤器配置为 re-write Location标头到 Zuul 的 URL。它还会添加剥离的 global 和 route-specific 前缀。以下 example 使用 Spring Configuration 文件添加过滤器:

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

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

仔细使用此过滤器。过滤器作用于 ALL 3XX响应代码的Location标头,这可能不适用于所有情况,例如将用户重定向到外部 URL 时。

18.15 Metrics

对于路由请求时可能发生的任何故障,Zuul 将在 Actuator metrics 端点下提供 metrics。可以通过点击/actuator/metrics来查看这些 metrics。 metrics 将具有ZUUL::EXCEPTION:errorCause:statusCode格式的 name。

18.16 Zuul 开发人员指南

有关 Zuul 如何工作的一般概述,请参阅Zuul Wiki

18.16.1 Zuul Servlet

Zuul 实现为 Servlet。对于一般情况,Zuul 嵌入到 Spring Dispatch 机制中。这让 Spring MVC 可以控制路由。在这种情况下,Zuul 缓冲请求。如果需要在没有缓冲请求的情况下通过 Zuul(例如,对于大型文件上载),Servlet 也安装在 Spring Dispatcher 之外。默认情况下,servlet 的地址为/zuul。可以使用zuul.servlet-path property 更改此路径。

18.16.2 Zuul RequestContext

要在过滤器之间传递信息,Zuul 使用RequestContext。其数据以ThreadLocal特定于每个请求保存。有关在哪里 route 请求,错误以及实际的HttpServletRequestHttpServletResponse的信息存储在那里。 RequestContext扩展ConcurrentHashMap,因此任何东西都可以存储在 context 中。 FilterConstants包含 Spring Cloud Netflix 安装的过滤器使用的密钥(更多关于这些后来)。

18.16.3 @EnableZuulProxy 与 @EnableZuulServer

Spring Cloud Netflix 安装了许多过滤器,具体取决于使用哪个 annotation 来启用 Zuul。 @EnableZuulProxy@EnableZuulServer的超集。换句话说,@EnableZuulProxy包含@EnableZuulServer安装的所有过滤器。 “代理”中的其他过滤器启用路由功能。如果你想要一个“空白”Zuul,你应该使用@EnableZuulServer

18.16.4 @EnableZuulServer 过滤器

@EnableZuulServer创建一个SimpleRouteLocator,从 Spring Boot configuration files 加载 route 定义。

安装了以下过滤器(正常 Spring Beans):

  • 预过滤器:

  • ServletDetectionFilter:检测请求是否通过 Spring Dispatcher。 使用 key 的 key 设置 boolean。

  • FormBodyWrapperFilter:解析表单数据,并为后续请求解析 re-encodes。

  • DebugFilter:如果设置了debug请求参数,sets RequestContext.setDebugRouting()RequestContext.setDebugRequest()true。 * Route 过滤器:

  • SendForwardFilter:使用 Servlet RequestDispatcher转发请求。转发位置存储在RequestContext属性FilterConstants.FORWARD_TO_KEY中。这对于转发到当前 application 中的 endpoints 非常有用。

  • 过滤后:

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

  • 错误过滤器:

  • SendErrorFilter:如果RequestContext.getThrowable()不为空,则转发到/error(默认情况下)。您可以通过设置error.path property 来更改默认转发路径(/error)。

18.16.5 @EnableZuulProxy 过滤器

创建DiscoveryClientRouteLocator,从DiscoveryClient(例如 Eureka)以及 properties 加载 route 定义。从DiscoveryClient为每个serviceId创建一个 route。添加新服务后,将刷新 routes。

除了前面描述的过滤器之外,还安装了以下过滤器(正常 Spring Beans):

  • 预过滤器:

  • PreDecorationFilter:根据提供的RouteLocator确定 route 的位置和方式。它还为下游请求设置了各种 proxy-related headers。

  • Route 过滤器:

  • RibbonRoutingFilter:使用 Ribbon,Hystrix 和可插入的 HTTP clients 发送请求。服务 ID 位于RequestContext属性FilterConstants.SERVICE_ID_KEY中。此过滤器可以使用不同的 HTTP clients:

  • Apache HttpClient:默认的 client。

  • Squareup OkHttpClient v3:通过在 classpath 上设置com.squareup.okhttp3:okhttp library 并设置ribbon.okhttp.enabled=true来启用。

  • Netflix Ribbon HTTP client:通过设置ribbon.restclient.enabled=true启用。这个 client 有局限性,包括它不支持 PATCH 方法,但它也有 built-in 重试。

  • SimpleHostRoutingFilter:通过 Apache HttpClient 向预定 URL 发送请求。 URL 位于RequestContext.getRouteHost()中。

18.16.6 自定义 Zuul 过滤器示例

下面的大多数“如何写”示例都包含在Sample Zuul 过滤器项目中。还有在 repository 中操作请求或响应主体的示例。

本节包括以下示例:

如何编写预过滤器

预过滤器在RequestContext中设置数据以用于下游过滤器。主要用例是设置 route 过滤器所需的信息。以下 example 显示了一个 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 request 参数填充SERVICE_ID_KEY。在实践中,您不应该进行这种直接映射。相反,应该从sample的 value 中查找服务 ID。

现在SERVICE_ID_KEY已填充,PreDecorationFilter不运行RibbonRoutingFilter并且运行RibbonRoutingFilter

如果要 route 到完整 URL,请改为调用ctx.setRouteHost(url)

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

如何编写 Route 过滤器

Route 在预过滤后过滤 run 并向其他服务发出请求。这里的大部分工作是将请求和响应数据转换为 client 所需的 model。以下 example 显示了一个 Zuul route 过滤器:

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 响应。

如何编写后置过滤器

后置过滤器通常会操纵响应。以下过滤器添加随机UUID作为X-Sample标头:

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;
	}
}

其他操作(例如转换响应体)要复杂得多且计算量大。

18.16.7 Zuul 错误的工作原理

如果在 Zuul 过滤器生命周期的任何部分期间抛出 exception,则执行错误过滤器。如果RequestContext.getThrowable()不是null,则SendErrorFilter仅为 run。然后,它会在请求中设置特定的javax.servlet.error.*属性,并将请求转发到 Spring Boot 错误页面。

18.16.8 Zuul Eager Application Context Loading

Zuul 内部使用 Ribbon 来调用 remote URL。默认情况下,Ribbon clients 在第一次调用时由 Spring Cloud 延迟加载。通过使用以下 configuration 可以为 Zuul 更改此行为,这会导致 application startup time 上的子 Ribbon 相关 Application 上下文的 eager loading。以下 example 显示了如何启用 eager loading:

application.yml.

zuul:
  ribbon:
    eager-load:
      enabled: true