18. Router 和filter:Zuul

路由是微服务架构不可或缺的一部分。例如,/可能被 Map 到您的 Web 应用程序,/api/users被 Map 到用户服务,而/api/shop被 Map 到 Store 服务。 Zuul是 Netflix 的基于 JVM 的 Router 和服务器端负载平衡器。

Netflix 使用 Zuul

  • Authentication

  • Insights

  • Stress Testing

  • Canary Testing

  • Dynamic Routing

  • Service Migration

  • Load Shedding

  • Security

  • 静态响应处理

  • 主动/主动流量 Management

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

Note

配置属性zuul.max.host.connections已被两个新属性zuul.host.maxTotalConnectionszuul.host.maxPerRouteConnections代替,它们分别默认为 200 和 20.

Note

所有路由的默认 Hystrix 隔离模式(ExecutionIsolationStrategy)是SEMAPHORE。如果首选该隔离模式,则可以将zuul.ribbonIsolationStrategy更改为THREAD

18.1 如何包括 Zuul

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

18.2 嵌入式 Zuul 反向代理

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

要启用它,请使用@EnableZuulProxyComments 一个 Spring Boot 主类。这样做会导致将本地调用转发到适当的服务。按照惯例,ID 为users的服务从位于/users的代理接收请求(前缀已去除)。代理使用功能区来定位要通过发现转发到的实例。所有请求都在hystrix command中执行,因此失败会显示在 HystrixMetrics 中。一旦电路断开,代理就不会尝试与服务联系。

Note

Zuul 启动程序不包括发现 Client 端,因此,对于基于服务 ID 的路由,您还需要在 Classpath 上提供其中之一(Eureka 是一种选择)。

要跳过自动添加服务的步骤,请将zuul.ignored-services设置为服务 ID 模式的列表。如果服务与被忽略但仍包含在显式配置的路由 Map 中的模式匹配,则将其忽略,如以下示例所示:

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 样式模式的path,因此/myusers/*仅匹配一个级别,但/myusers/**则分层匹配。

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

application.yml.

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

这些简单的 url-routes 不会以HystrixCommand的身份执行,也不会使用 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

另一种方法是指定一条服务路由,并为serviceId配置 RibbonClient 程序(这样做需要在 Ribbon 中禁用 Eureka 支持,请参见上面的更多信息),如以下示例所示:

application.yml.

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

ribbon:
  eureka:
    enabled: false

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

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

ApplicationConfiguration.java.

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

前面的示例意味着myusers-v1serviceIdMap 到 Route/v1/myusers/**。可以接受任何正则表达式,但是所有已命名的组都必须同时存在servicePatternroutePattern中。如果servicePatternserviceId不匹配,则使用默认行为。在前面的示例中,_9 的serviceIdMap 到“/myusers/**”路由(未检测到版本)。默认情况下,此功能是禁用的,仅适用于发现的服务。

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

application.yml.

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

Note

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

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

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

默认情况下,X-Forwarded-HostHeaders 被添加到转发的请求中。要关闭它,请设置zuul.addProxyHeaders = false。默认情况下,前缀路径被剥离,并且到后端的请求选择一个X-Forwarded-Prefix头(在前面显示的示例中为/myusers)。

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

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

application.yml.

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

前面的示例意味着所有调用(例如/myusers/101)都被转发到users服务上的/101。但是,包含/admin/的调用无法解决。

Warning

如果您需要保留 Route 的 Sequences,则需要使用 YAML 文件,因为使用属性文件时 Sequences 会丢失。以下示例显示了这样的 YAML 文件:

application.yml.

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

如果要使用属性文件,则legacy路径可能最终位于users路径的前面,从而导致users路径不可访问。

18.3 Zuul HttpClient 端

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

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

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

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

application.yml.

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

Note

这是sensitiveHeaders的默认值,因此除非您希望它与众不同,否则无需进行设置。这是 Spring Cloud Netflix 1.1 中的新增功能(在 1.0 中,用户无法控制标题,并且所有 cookie 都双向流动)。

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

application.yml.

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

您还可以通过设置zuul.sensitiveHeaders来设置敏感标题。如果在 Route 上设置了sensitiveHeaders,它将覆盖全局sensitiveHeaders设置。

18.5 忽略的标题

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

18.6Management 端点

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

  • Routes

  • Filters

18.6.1 路由端点

/routes处的路由端点的 GET 返回已 Map 路由的列表:

GET /routes.

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

可以通过将?format=details查询字符串添加到/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强制刷新现有路由(例如,当服务目录中发生更改时)。您可以通过将endpoints.routes.enabled设置为false来禁用此端点。

Note

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

18.6.2 过滤端点

GET/filters处的filter端点将按类型返回 Zuul filter的 Map。对于 Map 中的每种filter类型,您将获得该类型的所有filter的列表以及它们的详细信息。

18.7 扼杀模式和本地转发

迁移现有应用程序或 API 时,常见的模式是“勒死”旧的端点,并用不同的实现缓慢地替换它们。 Zuul 代理是一个有用的工具,因为您可以使用它来处理来自旧端点 Client 端的所有流量,但可以将某些请求重定向到新请求。

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

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

在前面的示例中,我们扼杀了“旧版”应用程序,该应用程序 Map 到与其他模式之一不匹配的所有请求。 /first/**中的路径已使用外部 URL 提取到新服务中。转发/second/**中的路径,以便可以在本地处理它们(例如,使用普通的 Spring @RequestMapping)。 /third/**中的路径也被转发,但是具有不同的前缀(/third/foo被转发到/3rd/foo)。

Note

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

18.8 通过 Zuul 上传文件

如果使用@EnableZuulProxy,则可以使用代理路径上载文件,只要文件很小,它就可以正常工作。对于大文件,“/zuul/*”中有一个替代路径绕过 Spring DispatcherServlet(以避免进行 Multipart 处理)。换句话说,如果您拥有zuul.routes.customers=/customers/**,则可以将POST大文件/zuul/customers/*。 servlet 路径通过zuul.servletPath外部化。如果代理路由带您通过功能区负载平衡器,则极大的文件也需要提高超时设置,如以下示例所示:

application.yml.

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

请注意,要使流技术处理大文件,您需要在请求中使用分块编码(某些浏览器默认不这样做),如以下示例所示:

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

18.9 查询字符串编码

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

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

application.yml.

zuul:
  forceOriginalQueryStringEncoding: true

Note

该特殊标志仅适用于SimpleHostRoutingFilter。另外,您还失去了使用RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)轻松覆盖查询参数的功能,因为现在直接在原始HttpServletRequest上获取查询字符串。

18.10 请求 URI 编码

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

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

application.yml.

zuul:
  decodeUrl: false

Note

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

18.11 纯嵌入式 Zuul

如果您使用@EnableZuulServer(而不是@EnableZuulProxy),则也可以运行 Zuul 服务器而无需代理或有选择地打开代理平台的某些部分。您添加到ZuulFilter类型的应用程序中的所有 bean 都会自动安装(与@EnableZuulProxy一样),但是不会自动添加任何代理filter。

在这种情况下,仍然可以通过配置“ zuul.routes.*”来指定进入 Zuul 服务器的路由,但是没有服务发现也没有代理。因此,“ serviceId”和“ url”设置将被忽略。以下示例将“/api/**”中的所有路径 Map 到 Zuul filter链:

application.yml.

zuul:
  routes:
    api: /api/**

18.12 禁用 Zuul filter

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

18.13 提供 Route 的 Hystrix 后备

当 Zuul 中给定 Route 的电路跳闸时,可以通过创建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;
            }
        };
    }
}

18.14 Zuul 超时

如果要为通过 Zuul 代理的请求配置套接字超时和读取超时,则根据您的配置,有两种选择:

  • 如果 Zuul 使用服务发现,则需要使用ribbon.ReadTimeoutribbon.SocketTimeout功能区属性配置这些超时。

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

18.15 重写 LocationHeaders

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

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

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

Warning

小心使用此filter。该filter作用于所有3XX个响应代码的LocationHeaders,这可能不适用于所有情况,例如,将用户重定向到外部 URL。

18.16 启用跨源请求

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

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

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

18.17 Metrics

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

18.18 Zuul 开发人员指南

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

18.18.1 Zuul Servlet

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

18.18.2 Zuul RequestContext

要在filter之间传递信息,Zuul 使用RequestContext。其数据保存在每个请求专用的ThreadLocal中。有关将请求路由到何处,错误以及实际的HttpServletRequestHttpServletResponse的信息存储在此处。 RequestContext扩展了ConcurrentHashMap,因此任何内容都可以存储在上下文中。 FilterConstants包含由 Spring Cloud Netflix 安装的filter使用的密钥(有关later的更多信息)。

18.18.3 @EnableZuulProxy 与@EnableZuulServer

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

18.18.4 @EnableZuulServer filter

@EnableZuulServer创建一个SimpleRouteLocator,该加载会从 Spring Boot 配置文件中加载路由定义。

已安装以下filter(作为普通的 Spring Bean):

  • Pre filters:

  • ServletDetectionFilter:检测请求是否通过 Spring Dispatcher。设置键为FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的布尔值。

    • FormBodyWrapperFilter:解析表单数据并为下游请求重新编码。

    • DebugFilter:如果设置了debug request 参数,请将RequestContext.setDebugRouting()RequestContext.setDebugRequest()设置为true。 *路由filter:

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

  • Post filters:

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

  • Error filters:

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

18.18.5 @EnableZuulProxy filter

创建一个DiscoveryClientRouteLocator,该DiscoveryClientRouteLocator将从DiscoveryClient(例如 Eureka)以及从属性中加载 Route 定义。为DiscoveryClient中的每个serviceId创建一条路由。添加新服务后,将刷新路由。

除了前面描述的filter之外,还安装了以下filter(作为普通的 Spring Bean):

  • Pre filters:

  • PreDecorationFilter:根据提供的RouteLocator确定 Route 和 Route。它还为下游请求设置了各种与代理相关的 Headers。

  • Route filters:

  • RibbonRoutingFilter:使用 Ribbon,Hystrix 和可插拔 HTTPClient 端发送请求。服务 ID 位于RequestContext属性FilterConstants.SERVICE_ID_KEY中。此filter可以使用不同的 HTTPClient 端:

  • Apache HttpClient:默认 Client 端。

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

    • Netflix Ribbon HTTPClient 端:通过设置ribbon.restclient.enabled=true启用。该 Client 端具有局限性,包括不支持 PATCH 方法,但是还具有内置的重试功能。

    • SimpleHostRoutingFilter:通过 Apache HttpClient 将请求发送到 sched 的 URL。网址位于RequestContext.getRouteHost()

18.18.6 自定义 Zuul filter示例

以下大多数“如何编写”示例都包含在Zuul filter samples项目中。在该存储库中也有一些处理请求或响应正文的示例。

本节包括以下示例:

如何编写前置filter

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

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

前面的filter从sample请求参数中填充SERVICE_ID_KEY。实际上,您不应该执行这种直接 Map。而是应从sample的值查找服务 ID。

现在已填充SERVICE_ID_KEYPreDecorationFilter将不运行,而RibbonRoutingFilter将运行。

Tip

如果要路由到完整 URL,请致电ctx.setRouteHost(url)

要修改路由filter转发到的路径,请设置REQUEST_URI_KEY

如何编写路由filter

路由filter在Pre filter之后运行,并向其他服务发出请求。此处的许多工作是在 Client 端所需的模型之间来回转换请求和响应数据。以下示例显示了 Zuul 路由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;
    }
}

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

如何编写postfilter

后置filter通常操纵响应。以下filter添加了一个随机的UUID作为X-SampleHeaders:

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

Note

其他操作,例如转换响应主体,则更加复杂且计算量大。

18.18.7 Zuul 错误的工作方式

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

18.18.8 Zuul Eager 应用程序上下文加载

Zuul 在内部使用 Ribbon 来调用远程 URL。默认情况下,丝带云 Client 端在第一次调用时由 Spring Cloud 延迟加载。可以使用以下配置为 Zuul 更改此行为,这将导致在应用程序启动时急于加载与子 Ribbon 相关的应用程序上下文。以下示例显示了如何启用即时加载:

application.yml.

zuul:
  ribbon:
    eager-load:
      enabled: true