路由器和过滤器:Zuul【译】

原文链接:http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.0.1.RELEASE/single/spring-cloud-netflix.html#_router_and_filter_zuul

8. 路由器和过滤器:Zuul

路由是微服务架构不可或缺的一部分。 例如,/可以映射到您的Web应用程序,/api /users映射到用户服务,/api/shop映射到商店服务。 Zuul是Netflix的基于JVM的路由器和服务器端负载均衡器。

Netflix使用Zuul进行以下操作:

  • 认证
  • Insights
  • 压力测试
  • Canary测试
  • 动态路由
  • 服务迁移
  • 负载脱落
  • 安全
  • 静态响应处理
  • 主动/主动流量管理

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

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

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

8.1 如何引入Zuul

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

8.2 嵌入式Zuul反向代理

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

要启用它,请使用@EnableZuulProxy注释Spring Boot主类。 这样做会导致本地呼叫转发到适当的服务。 按照惯例,具有用户ID的服务从位于/users的代理接收请求(带有前缀剥离)。 代理使用功能区来定位要通过发现转发的实例。 所有请求都在hystrix命令中执行,因此Hystrix指标中会出现故障。 电路打开后,代理不会尝试联系该服务。

Zuul启动器不包含发现客户端,因此,对于基于服务ID的路由,您还需要在类路径中提供其中一个(Eureka是一种选择)。

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

application.yml.

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

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

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

application.yml.

zuul:
  routes:
    users: /myusers/**

前面的示例意味着对/myusers的HTTP调用被转发到用户服务(例如/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: 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配置Ribbon客户端(这样做需要在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-v1serviceId映射到路由/v1/myusers/**。 接受任何正则表达式,但所有命名组必须同时出现在servicePatternroutePattern中。 如果servicePatternserviceId不匹配,则使用默认行为。 在前面的示例中,myusersserviceId映射到/myusers/**路由(未检测到版本)。 默认情况下禁用此功能,仅适用于已发现的服务。

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

application.yml.

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

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

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

zuul.routes条目实际上绑定到ZuulProperties类型的对象。 如果查看该对象的属性,可以看到它还具有retryable的标志。 将该标志设置为true以使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)都转发到用户服务上的/101。 但是,包括/admin/的呼叫无法解决。

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

application.yml.

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

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

8.3 Zuul Http客户端

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

8.4 Cookie和敏感标题

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

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

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

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

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

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

application.yml.

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

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

8.5 忽略标题

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

8.6 管理端点

默认情况下,如果将@EnableZuulProxy与Spring Boot Actuator一起使用,则可以启用另外两个端点:

  • 路由
  • 过滤

8.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
  }
}

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

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

8.6.2 过滤结点

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

8.7 扼杀模式和本地前锋

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

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

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

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

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

8.8 通过Zuul上传文件

如果您使用@EnableZuulProxy,您可以使用代理路径上传文件,只要文件很小,它就可以工作。 对于大型文件,有一个替代路径绕过/zuul/*中的Spring DispatcherServlet(以避免多部分处理)。 换句话说,如果你有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 "file=@mylarge.iso" localhost:9999/zuul/simple/file

8.9 查询字符串编码

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

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

application.yml.

zuul:
  forceOriginalQueryStringEncoding: true

此特殊标志仅适用于SimpleHostRoutingFilter。 此外,您无法使RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)轻松覆盖查询参数,因为查询字符串现在直接在原始HttpServletRequest上获取。

8.10 普通嵌入式Zuul

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

application.yml.

zuul:
  routes:
    api: /api/**

8.11 禁用Zuul过滤器

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

8.12 为路由提供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;
            }
        };
    }
}

8.13 Zuul超时

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

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

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

8.14 重写Location标头

如果Zuul面向Web应用程序,则当Web应用程序通过HTTP状态代码3XX重定向时,您可能需要重新写入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();
    }
}

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

8.15 Metrics

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

8.16 Zuul开发人员指南

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

8.16.1 Zuul Servlet

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

8.16.2 Zuul RequestContext

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

8.16.3 @EnableZuulProxy vs. @EnableZuulServer

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

8.16.4 @EnableZuulServer过滤

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

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

  • 预过滤器:
    • ServletDetectionFilter: 检测请求是否通过Spring Dispatcher。 使用FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的键设置布尔值。
    • FormBodyWrapperFilter: 解析表单数据并为下游请求重新编码。
    • DebugFilter: 如果设置了debug 请求参数,则将RequestContext.setDebugRouting()RequestContext.setDebugRequest()设置为true
  • 路由过滤器:
    • SendForwardFilter: 使用Servlet RequestDispatcher转发请求。 转发位置存储在RequestContext属性FilterConstants.FORWARD_TO_KEY中。 这对于转发到当前应用程序中的端点非常有用。
  • 后置过滤器:
    • SendResponseFilter: 将代理请求的响应写入当前响应。
  • 错误过滤器:
    • SendErrorFilter: 如果RequestContext.getThrowable()不为null,则转发到/error(默认情况下)。 您可以通过设置error.path属性来更改默认转发路径(/error)。

8.16.5 @EnableZuulProxy过滤

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

除了前面描述的过滤器之外,还安装了以下过滤器(与普通的Spring Bean一样):

  • 预过滤器:
    • PreDecorationFilter: 确定路由的位置和方式,具体取决于提供的RouteLocator。 它还为下游请求设置各种与代理相关的标头。
  • 路由过滤器:
    • RibbonRoutingFilter: 使用Ribbon,Hystrix和可插入HTTP客户端发送请求。 服务ID位于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:

8.16.6 自定义Zuul过滤器示例

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

本节包括以下示例:

如何编写预过滤器

预过滤器在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的值中查找服务ID。

现在已填充SERVICE_ID_KEYPreDecorationFilter不会运行并且RibbonRoutingFilter会运行。

如果要路由到完整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响应。

如何编写后置过滤器

后置过滤器通常会操纵响应。 以下过滤器添加随机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;
    }
}

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

8.16.7 Zuul错误如何工作

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

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

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

application.yml.

zuul:
  ribbon:
    eager-load:
      enabled: true
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容