18. 路由器和过滤器: Zuul

路由是微服务体系结构的组成部分。例如,/ 可以映射到 web 应用程序,/api/users 映射到用户服务,/api/shop 映射到 shop 服务。Zuul 是 Netflix 基于 JVM 的路由器和服务器端负载均衡器。

Netflix 使用 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 支持。

[Note] Note

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

[Note] Note

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

18.1 如何包含 Zuul

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

18.2 嵌入式 Zuul 反向代理

Spring Cloud 已经创建了一个嵌入的 Zuul 代理来简化一个公共用例的开发,在这个用例中,UI 应用程序希望对一个或多个后端服务进行代理调用。此功能对于代理所需后端服务的用户界面很有用,避免了独立管理所有后端的 CORS 和身份验证问题的需要。

要启用它,请使用 @EnableZuulProxy 注解 Spring Boot 主类。这样做会导致本地调用被转发到相应的服务。按照惯例,ID 为 users 的服务从位于 /users 的代理接收请求(去掉前缀)。代理使用 Ribbon 定位要通过发现转发到的实例。所有请求都是在 hystrix command 中执行的,所以失败会出现在 Hystrix 度量中。一旦断路打开,代理就不会尝试联系服务。

[Note] Note

Zuul 启动程序不包括发现客户端,因此,对于基于 service ID 的路由,你还需要提供类路径上的一种路由(Eureka 是一种选择)。

要跳过自动添加服务,请将 zuul.ignored-services 设置为 service ID 模式列表。如果一个服务匹配一个被忽略但也包含在显式配置的路由映射中的模式,那么它是无标识的,如下例所示:

application.yml. 

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

在前面的示例中,除了 users 之外,所有服务都将被忽略。

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

application.yml. 

 zuul:
  routes:
    users: /myusers/**

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

要对路由进行更细粒度的控制,可以独立地指定 path 和 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 路由不会作为 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 配置 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 和路由之间提供约定。它使用名为 groups 的正则表达式从 serviceId 中提取变量并将其注入路由模式,如下例所示:

ApplicationConfiguration.java. 

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

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

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

application.yml. 

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

[Note] Note

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/ 在内的调用无法解决。

[Warning] Warning

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

application.yml. 

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

如果要使用属性文件,则 legacy 路径可能会出现在 users 路径之前,从而导致无法访问 users 路径。

18.3 Zuul Http 客户端

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

18.4 Cookies 和敏感 Headers

你可以在同一个系统中的服务之间共享头,但可能不希望敏感头向下游泄漏到外部服务器。可以将忽略的头列表指定为路由配置的一部分。Cookies 起着特殊的作用,因为它们在浏览器中有定义良好的语义,并且总是被视为敏感的。如果代理的使用者是浏览器,那么下游服务的 Cookies 也会给用户带来问题,因为它们都混在一起(所有下游服务看起来都来自同一个地方)。

如果你对服务的设计很小心(例如,如果只有一个下游服务设置 cookies),那么你可能能够让它们从后端一直流到调用者。此外,如果代理设置了 cookies,并且所有后端服务都是同一系统的一部分,那么只需共享它们(例如,使用 Spring 会话将它们链接到某个共享状态)是很自然的。除此之外,由下游服务设置的任何 cookies 可能对调用方无效,因此建议你(至少)将 Set-Cookie 和 Cookie 置为不属于你的域的路由的敏感头段。即使对于属于你的域的路由,在允许 cookies 在它们和代理之间流动之前,也要仔细考虑它的含义。

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

application.yml. 

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

[Note] Note

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

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

application.yml. 

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

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

18.5 忽略 Headers

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

18.6 管理端点

默认情况下,如果将 @EnableZuulProxy 与 Spring Boot 执行器一起使用,则会启用另外两个端点:

  • Routes
  • Filters

18.6.1 路由端点

一个通过 /routes 到路由端点的 GET,将返回路由映射的列表。

GET /routes. 

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

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

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 来禁用此端点。

[Note] Note

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

18.6.2 过滤器端点

一个通过 /filters 到过滤器端点的 GET,将返回 Zuul 过滤器类型的 map。对于 map 中的每种过滤器类型,你将获得该类型的所有过滤器的列表,以及它们的详细信息。

18.7 窒息模式和本地跳转

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

以下示例展示 “strangle” 方案的配置详细信息:

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” 应用程序,它被映射到所有不匹配其他模式之一的请求。/first/** 中的路径已提取到具有外部 URL 的新服务中。/second/** 中的路径被转发,以便在本地处理它们(例如,使用普通的 Spring @RequestMapping)。/third/** 中的路径也被转发,但前缀不同(/third/foo 被转发到 /3rd/foo)。

[Note] Note

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

18.8 通过 Zuul 上传文件

如果使用 @EnableZuulProxy,则可以使用代理路径上载文件,只要文件很小,它就可以工作。对于大型文件,有一个替代路径可以绕过 "/zuul/*" 中的 Spring DispatcherServlet(以避免多部分处理)。换句话说,如果你有 zuul.routes.customers=/customers/**,那么可以将大文件 POST 到 /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

18.9 查询字符串编码

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

为了强制对查询字符串进行原始编码,可以向 ZuulProperties 传递一个特殊的标志,以便使用 HttpServletRequest::getQueryString 方法将查询字符串视为原样,如下例所示:

application.yml. 

 zuul:
  forceOriginalQueryStringEncoding: true

[Note] Note

此特殊标志仅适用于 SimpleHostRoutingFilter。此外,由于查询字符串现在直接在原始 HttpServletRequest 上获取,因此你失去了使用 RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters))轻松重写查询参数的能力。

18.10 请求 URI 编码

在处理传入请求时,请求 URI 在与路由匹配之前被解码。当在路由过滤器中重建后端请求时,请求 URI 将被重新编码。如果你的URI包含编码的 "/" 字符,这可能会导致一些意外的行为。

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

application.yml. 

 zuul:
  decodeUrl: false

[Note] Note

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

18.11 完全嵌入 Zuul

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

在这种情况下,到 Zuul 服务器的路由仍然通过配置 "zuul.routes.*" 来指定,但不存在服务发现和代理。因此,忽略 "serviceId" 和 "url" 设置。以下示例将 "/api/**" 中的所有路径映射到 Zuul 过滤器链:

application.yml. 

 zuul:
  routes:
    api: /api/**

18.12 禁用 Zuul 过滤器

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

18.13 为路由提供 Hystrix 回退

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

18.14 Zuul 超时

如果要配置通过 Zuul 代理的请求的 socket 超时和读取超时,根据你的配置,有两个选项:

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

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

18.15 重新 Location 头

如果 Zuul 面对的是一个 web 应用程序,那么当 web 应用程序通过 3XX 的 HTTP 状态代码重定向时,可能需要重新编写 Location 头。否则,浏览器将重定向到 web 应用程序的 URL 而不是 Zuul URL。你可以配置 LocationRewriteFilter Zuul 过滤器以将 Location 头重新写入 Zuul 的 URL。它还添加了剥离的全局前缀和路由特定前缀。以下示例通过使用 Spring 配置文件添加过滤器:

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

@Configuration
@EnableZuulProxy
public class ZuulConfig {
    @Bean
    public LocationRewriteFilter locationRewriteFilter() {
        return new LocationRewriteFilter();
    }
}
[Caution] 提醒

小心使用这个过滤器。过滤器作用于所有 3XX 响应代码的 Location 头,这可能不适用于所有场景,例如将用户重定向到外部 URL 时。

18.16 启用跨域请求

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

@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 的 GET 和 POST 方法向以 path-1 开始的端点发送跨域请求。你可以使用 /** 映射,将 CORS 配置应用于特定的路径模式,或者全局应用于整个应用程序。你可以通过折线配置自定义属性:allowedOrigins、allowedMethods、allowedHeaders、exposedHeaders、allowCredentials 和 maxAge。

18.17 度量

Zuul 将在执行器度量端点下为路由请求时可能发生的任何故障提供度量。点击 /actuator/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 请求上下文

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

18.18.3 @EnableZuulProxy 与 @EnableZuulServer

Spring Cloud Netflix 安装了许多过滤器,这取决于使用哪个注解来启用 Zuul。@EnableZuulProxy 是 @EnableZuulServer 的超集。换句话说,@EnableZuulProxy 包含 @EnableZuulServer 安装的所有过滤器。“proxy” 中的附加过滤器启用路由功能。如果需要 “blank” Zuul,则应使用 @EnableZuulServer。

18.18.4 @EnableZuulServer 过滤器

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

安装了以下过滤器(与普 Spring Bean 一样):

  • 前置过滤器:

    • ServletDetectionFilter:检测请求是否通过 Spring Dispatcher。设置一个 FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY 键 的 boolean。
    • FormBodyWrapperFilter:解析表单数据并为下游请求重新编码。
    • DebugFilter:如果设置了 debug 请求参数,则将 RequestContext.setDebugRouting() 和 RequestContext.setDebugRequest() 设置为 true
  • 路由过滤器:

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

    • SendResponseFilter:将代理请求的响应写入当前响应。
  • 错误过滤器

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

18.18.5 @EnableZuulProxy 过滤器

创建一个 DiscoveryClientRouteLocator,它从 DiscoveryClient(如 Eureka)和属性加载路由定义。从 DiscoveryClient 为每个 serviceId 创建一个路由。添加新服务时,将刷新路由。

除了前面描述的过滤器外,还安装了以下过滤器(作为普 Spring Bean):

  • 前置过滤器:

    • 根据提供的 RouteLocator 确定路由的位置和方式。它还为下游请求设置各种与代理相关的头。
  • 路由过滤器:

    • RibbonRoutingFilter:使用Ribbon、Hystrix 和可插入 HTTP 客户端发送请求。在 RequestContext 属性 FilterConstants.SERVICE_ID_KEY 中找到 Service ID。此过滤器可以使用不同的 HTTP 客户端:

      • Apache HttpClient:默认客户端。
      • Squareup OkHttpClient v3:通过在类路径上设置 com.squareup.okhttp3:okhttp 库并设置 ribbon.okhttp.enabled=true 启用。
      • Netflix Ribbon HTTP client:通过设置 ribbon.restclient.enabled=true 启用。这个客户端有一些限制,包括它不支持补丁方法,但是它也有内置的重试。
    • SimpleHostRoutingFilter:通过 Apache HTTPClient 向预定的 URL 发送请求。在 RequestContext.getRouteHost() 中找到 URL。

18.18.6 自定义 Zuul 过滤器示例

下面的大多数“如何编写”示例包含在 Sample Zuul Filters 中。还有一些操作该存储库中请求或响应主体的示例。

本节包括以下示例:

  • “如何编写前置过滤器” 部分
  • “如何编写路由过滤器” 部分
  • “如何编写 Post 过滤器” 部分

如何编写前置过滤器

前置过滤器在 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。实际上,你不应该进行这种直接映射。相反,应该从 sample 的值中查找 service ID。

既然已经填充了 SERVICE_ID_KEY,那么 PreDecorationFilter 将不运行,而 RibbonRoutingFilter 将运行。

[Tip] Tip

如果要路由到完整的 URL,请改为调用 ctx.setRouteHost(url)。

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

如何编写路由过滤器

路由过滤器在前置过滤器之后运行,并向其他服务发出请求。这里的大部分工作是将请求和响应数据转换为客户端所需的模型,并从中转换出来。以下示例展示 Zuul 路由过滤器:

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

如何编写 Post 过滤器

Post 过滤器通常操作响应。以下过滤器将随机 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;
	}
}
[Note] Note

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

18.18.7 Zuul 错误的工作原理

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

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

Zuul 内部使用 Ribbon 调用远程 URL。默认情况下,Ribbon 客户端在第一次调用时被 Spring Cloud 延迟加载。通过使用以下配置,可以更改 Zuul 的此行为,这将导致在应用程序启动时预先加载与子 Ribbon 相关的应用程序上下文。下面的示例展示如何启用预加载:

application.yml. 

zuul:
  ribbon:
    eager-load:
      enabled: true