19. Router 和 Filter:Zuul

路由在微服务 architecture 的组成部分。例如,/可以映射到 web application,/api/users映射到用户服务,/api/shop映射到商店服务。 Zuul是 Netflix 的基于 JVM 的 router 和服务器端负载均衡器。

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。

19.1 如何包含 Zuul

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

19.2 嵌入式 Zuul 反向代理

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

要启用它,请使用@EnableZuulProxy注释 Spring Boot main class,并将本地 calls 转发到相应的服务。按照惯例,具有 ID“users”的服务将从位于/users的代理接收请求(带有前缀剥离)。代理使用 Ribbon 来定位要转发到实例的实例,并且所有请求都在hystrix 命令中执行,因此失败将显示在 Hystrix metrics 中,一旦电路打开,代理将不会尝试联系该服务。

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

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

application.yml.

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

在此示例中,除了**“users”之外,所有服务都被忽略**。

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

application.yml.

zuul:
  routes:
    users: /myusers/**

这意味着 http calls 到“/myusers”被转发到“users”服务(对于 example“/myusers/101”被转发到“/101”)。

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

application.yml.

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

这意味着 http calls 到“/myusers”会转发到“users_service”服务。 route 必须有一个“路径”,可以指定为 ant-style pattern,因此“/myusers/ *”只匹配一个 level,但“/myusers/ **”按层次匹配。

后端的位置可以指定为“serviceId”(对于发现的服务)或“url”(对于物理位置),e.g.

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

另一种方法是指定 service-route 并为 serviceId 配置 Ribbon client(这需要在 Ribbon 中禁用 Eureka 支持:请参阅以上是了解更多信息),e.g.

application.yml.

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

ribbon:
  eureka:
    enabled: false

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

您可以使用 regexmapper 在 serviceId 和 routes 之间提供约定。它使用名为 groups 的正则表达式从 serviceId 中提取变量,并将它们注入 route pattern。

ApplicationConfiguration.java.

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

这意味着 serviceId“myusers-v1”将映射到 route“/v1/myusers/ ”.接受任何正则表达式,但所有命名组必须同时出现在 servicePattern 和 routePattern 中.如果 servicePattern 不匹配 serviceId,则使用默认行为.在上面的 example 中,serviceId“myusers”将映射到 route“/myusers/”(未检测到 version)此 feature 默认情况下处于禁用状态,仅适用于已发现的服务。

要为所有映射添加前缀,请将zuul.prefix设置为 value,例如/api。在默认情况下转发请求之前,会从请求中删除代理前缀(使用zuul.stripPrefix=false关闭此行为)。您还可以关闭单个 routes e.g 的 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,你会发现它还有一个“可重试的”flag。将 flag 设置为“true”以使 Ribbon client 自动重试失败的请求(如果需要,可以使用 Ribbon client configuration 修改重试操作的参数)。

默认情况下,X-Forwarded-Host标头会添加到转发的请求中。要将其关闭设置zuul.addProxyHeaders = false。默认情况下,前缀路径被剥离,对后端的请求会选择一个标题“X-Forwarded-Prefix”(上例中的“/myusers”)。

如果设置默认的 route(“/”),的 application 可以作为独立的服务器,example zuul.route.home: /会将所有流量(i.e.“/ **”)路由到“home”服务。

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

application.yml.

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

这意味着所有 calls 如“/myusers/101”将被转发到“users”服务上的“/101”。但 calls 包括“/admin/”将无法解决。

如果您需要 routes 保留其 order,则需要使用 YAML 文件,因为 ordering 将使用 properties 文件丢失。例如:

application.yml.

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

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

19.3 Zuul Http Client

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

19.4 Cookies and Sensitive Headers

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

如果您对服务的设计非常小心,对于 example,如果只有一个下游服务 sets cookies,那么您可以让它们从后端一直流到调用者。此外,如果您的代理 sets cookies 和所有后端服务都是同一系统的一部分,那么简单地共享它们就很自然(例如使用 Spring Session 将它们链接到一些共享的 state)。除此之外,任何由下游服务设置的 cookies 对调用者来说可能都不是很有用,因此建议您(至少)将“Set-Cookie”和“Cookie”放入敏感的_header 中,以用于不属于它们的 routes 你的域名。即使对于**属于你的域名的 routes,在允许 cookies 在它们和代理之间流动之前,请仔细考虑它的含义。

敏感的_header 可以按 route e.g 配置为 comma-separated 列表。

application.yml.

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

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

sensitiveHeaders是黑名单,默认不为空,因此要使 Zuul 发送所有 headers(除了“忽略”之外),您必须将其显式设置为空列表。如果您想将 cookie 或授权 headers 传递给后端,则必须执行此操作。 例:

application.yml.

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

也可以通过设置zuul.sensitiveHeaders来全局设置敏感_header。如果在 route 上设置了sensitiveHeaders,则会覆盖 global sensitiveHeaders设置。

19.5 忽略了 Headers

除了 per-route 敏感 headers 之外,您还可以为zuul.ignoredHeaders设置 global value,以便在与下游服务交互期间应丢弃的值(请求和响应)。默认情况下,如果 Spring Security 不在 classpath 上,则它们为空,否则它们将被初始化为@在这种情况下的假设是下游服务也可能添加这些 headers,我们想要代理的值。如果 Spring Security 在 classpath 上,你不能丢弃这些众所周知的安全 headers,你可以将zuul.ignoreSecurityHeaders设置为false。如果您在 Spring Security 中禁用了 HTTP 安全响应 headers 并希望下游服务提供的值,则此功能非常有用

19.6 管理 Endpoints

如果将@EnableZuulProxy与 Spring Boot Actuator 一起使用,则将启用(默认情况下)另外两个 endpoints:

  • Routes

  • 过滤器

19.6.1 Routes Endpoint

/routes的 routes 端点的 GET 将_return 一个映射的 routes 列表:

GET /routes.

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

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

GET /routes? format=details.

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

POST 将强制刷新现有的 routes(e.g. 如果服务目录中有更改)。您可以通过将endpoints.routes.enabled设置为false来禁用此端点。

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

19.6.2 过滤端点

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

19.7 扼杀模式和本地前锋

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

Example 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”应用程序,该应用程序映射到所有不匹配其他模式之一的请求。 /first/**中的 Paths 已被提取到具有外部 URL 的新服务中。 /second/**中的 paths 被转发,因此它们可以在本地处理,e.g. 正常 Spring @RequestMapping/third/**中的 Paths 也被转发,但具有不同的前缀(i.e./third/foo被转发到/3rd/foo)。

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

19.8 通过 Zuul 上传 Files

如果你@EnableZuulProxy你可以使用代理 paths 来上传 files,它应该只用 long 作为 files 很小。对于大 files,有一个替代路径绕过(以避免多个处理)在“/zuul/ *”中。 I.e。如果是zuul.routes.customers=/customers/**则可以将大 files 发送到“/zuul/customers/ *”。 servlet 路径通过zuul.servletPath外部化。如果代理 route 带您通过 Ribbon 负载均衡器 e.g,则非常大的 files 也需要提升超时设置。

application.yml.

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

请注意,要使用流式处理大型 files,您需要在请求中使用分块编码(默认情况下某些浏览器不会这样做)。 E.g。在命令 line 上:

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

19.9 查询 String 编码

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

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

application.yml.

zuul:
  forceOriginalQueryStringEncoding: true

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

19.10 Plain Embedded Zuul

如果使用@EnableZuulServer(而不是@EnableZuulProxy),您也可以在没有代理的情况下运行 Zuul 服务器,或者选择性地切换代理平台的各个部分。您添加到ZuulFilter类型的 application 中的任何 beans 都将自动安装,因为它们与@EnableZuulProxy一起安装,但不会自动添加任何代理过滤器。

在这种情况下,仍然通过配置“zuul.routes.*”来指定进入 Zuul 服务器的 routes,但是没有服务发现和代理,因此忽略“serviceId”和“url”设置。例如:

application.yml.

zuul:
  routes:
    api: /api/**

maps“/api/ **”中的所有_path 到 Zuul 过滤器链。

19.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 set zuul.SendResponseFilter.post.disable=true

19.12 为 Routes 提供 Hystrix 后备

当 Zuul 中给定 route 的电路被触发时,您可以通过创建ZuulFallbackProvider类型的 bean 来提供回退响应。在此 bean 中,您需要指定回退所用的 route ID,并提供ClientHttpResponse到 return 作为回退。这是一个非常简单的ZuulFallbackProvider implementation。

class MyFallbackProvider implements ZuulFallbackProvider {
    @Override
    public String getRoute() {
        return "customers";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        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;
            }
        };
    }
}

这就是 route configuration 的样子。

zuul:
  routes:
    customers: /customers/**

如果您想为所有 routes 提供默认回退,则可以创建ZuulFallbackProvider类型的 bean 并使方法 return *null

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

    @Override
    public ClientHttpResponse fallbackResponse() {
        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;
            }
        };
    }
}

如果您想根据失败原因选择响应,请使用FallbackProvider,它将在将来的版本中替换ZuulFallbackProvder

class MyFallbackProvider implements FallbackProvider {

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

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

    @Override
    public ClientHttpResponse fallbackResponse() {
        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;
            }
        };
    }
}

19.13 Zuul 超时

19.13.1 服务发现配置

如果 Zuul 正在使用服务发现,则需要关注两个超时,Hystrix 超时(因为默认情况下所有 routes 都包含在 Hystrix 命令中)和 Ribbon 超时。 Hystrix 超时需要考虑 Ribbon 读取和连接超时 PLUS 将为该服务发生的重试总次数。默认情况下 Spring Cloud Zuul 会尽力为你计算 Hystrix 超时**除非你明确指定 Hystrix 超时。

Hystrix 超时使用以下公式计算:

(ribbon.ConnectTimeout + ribbon.ReadTimeout) * (ribbon.MaxAutoRetries + 1) * (ribbon.MaxAutoRetriesNextServer + 1)

作为示例,如果在 application properties 中设置以下 properties

application.yml.

ribbon:
  ReadTimeout:100
  ConnectTimeout:500
  MaxAutoRetries:1
  MaxAutoRetriesNextServer:1

然后 Hystrix 超时(对于本例中的所有 routes)将设置为2400ms.

您可以使用service.ribbon.* properties 为各个 routes 配置 Hystrix 超时。

如果您选择不配置上述 properties,则将使用默认值,因此默认的 Hystrix 超时将设置为4000ms

如果设置hystrix.command.commandKey.execution.isolation.thread.timeoutInMilliseconds,其中commandKey是 route id,或设置hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds,则这些值将用于 Hystrix 超时,无论您为ribbon.* properties 设置了什么。如果你设置这些 properties 中的任何一个****你负责确保它考虑 Ribbon 连接和读取超时以及可能发生的任何重试。

19.13.2 URL Configuration

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

19.14 重写位置标题

如果 Zuul 面向 web application,则当 web application 重定向通过 3XX 的 http 状态 code 时,可能需要 re-write 标头,否则浏览器将最终重定向到 web application 的 url 而不是 Zuul url。可以将LocationRewriteFilter Zuul 过滤器配置为 re-write 位置标头到 Zuul 的 URL,它还会添加已剥离的 global 和 route 特定前缀。可以通过 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标头,这可能不适用于所有情况,例如,如果用户重定向到外部 URL。

19.15 Zuul 开发人员指南

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

19.15.1 Zuul Servlet

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

19.15.2 Zuul RequestContext

要在过滤器之间传递信息,Zuul 使用RequestContext。其数据以ThreadLocal特定于每个请求保存。有关 route 请求,错误以及实际HttpServletRequestHttpServletResponse的位置的信息存储在那里。 RequestContext扩展ConcurrentHashMap,因此任何东西都可以存储在 context 中。 FilterConstants包含由 Spring Cloud Netflix 安装的过滤器使用的键(稍后会详细介绍)。

19.15.3 @EnableZuulProxy 与 @EnableZuulServer

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

19.15.4 @EnableZuulServer 过滤器

创建一个从 Spring Boot configuration files 加载 route 定义的SimpleRouteLocator

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

预过滤器:

  • ServletDetectionFilter:检测请求是否通过 Spring Dispatcher。 _Set boolean with key FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY

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

  • DebugFilter:如果设置了debug请求参数,则此过滤器将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)。

19.15.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()中。

19.15.6 自定义 Zuul 过滤器示例

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

19.15.7 如何编写预过滤器

预过滤器用于在RequestContext中设置数据以用于下游过滤器。主要用例是设置 route 过滤器所需的信息。

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

上面的过滤器从foo request 参数填充SERVICE_ID_KEY。实际上,进行这种直接映射并不是一个好的 idea,但是应该从foo的 value 中查找服务 id。

现在SERVICE_ID_KEY已填充,PreDecorationFilter将不会 run 和RibbonRoutingFilter将。如果您想要转发到完整的网址,请改为调用ctx.setRouteHost(url)

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

19.15.8 如何编写 Route 过滤器

Route 过滤器在预过滤器后运行,用于向其他服务发出请求。这里的大部分工作是将请求和响应数据转换为 client 所需的 model。

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 响应。警告:此过滤器可能存在错误,而不是正确的功能。

19.15.9 如何编写后置过滤器

后置过滤器通常会操纵响应。在下面的过滤器中,我们添加一个随机UUID作为X-Foo标头。其他操作,例如转换响应体,要复杂得多,而且要 compute-intensive。

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-Foo", UUID.randomUUID().toString());
		return null;
	}
}

19.15.10 Zuul 错误的工作原理

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

19.15.11 Zuul Eager Application Context Loading

Zuul 内部使用 Ribbon 来调用 remote 网址,Ribbon clients 默认是在第一次调用时由 Spring Cloud 懒洋洋地加载。可以使用以下 configuration 为 Zuul 更改此行为,并将导致 child Ribbon 相关的 Application 上下文在 application startup time 上急切加载。

application.yml.

zuul:
  ribbon:
    eager-load:
      enabled: true