18. Router 和filter:Zuul
路由是微服务架构不可或缺的一部分。例如,/
可能被 Map 到您的 Web 应用程序,/api/users
被 Map 到用户服务,而/api/shop
被 Map 到 Store 服务。 Zuul是 Netflix 的基于 JVM 的 Router 和服务器端负载平衡器。
-
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.maxTotalConnections
和zuul.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 和身份验证问题的需求。
要启用它,请使用@EnableZuulProxy
Comments 一个 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
您可以使用regexmapper
在serviceId
和路由之间提供约定。它使用正则表达式命名组从serviceId
中提取变量,并将其注入到路由模式中,如以下示例所示:
ApplicationConfiguration.java.
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
前面的示例意味着myusers-v1
的serviceId
Map 到 Route/v1/myusers/**
。可以接受任何正则表达式,但是所有已命名的组都必须同时存在servicePattern
和routePattern
中。如果servicePattern
与serviceId
不匹配,则使用默认行为。在前面的示例中,_9 的serviceId
Map 到“/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-Host
Headers 被添加到转发的请求中。要关闭它,请设置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
)支持。要使用RestClient
或okhttp3.OkHttpClient
,请分别设置ribbon.restclient.enabled=true
或ribbon.okhttp.enabled=true
。如果要定制 Apache HTTPClient 端或 OK HTTPClient 端,请提供ClosableHttpClient
或OkHttpClient
类型的 Bean。
18.4 Cookie 和敏感标题
您可以在同一系统中的服务之间共享 Headers,但是您可能不希望敏感 Headers 泄漏到下游到外部服务器中。您可以在路由配置中指定忽略的 Headers 列表。 Cookies 发挥着特殊的作用,因为它们在浏览器中具有定义明确的语义,并且始终将其视为敏感内容。如果代理的使用者是浏览器,那么下游服务的 cookie 也会给用户带来麻烦,因为它们都混杂在一起(所有下游服务看起来都来自同一位置)。
如果您对服务的设计很谨慎(例如,如果只有一个下游服务设置 cookie),则可以让它们从后端一直流到调用者。另外,如果您的代理设置 cookie,并且所有后端服务都在同一系统中,则很自然地简单地共享它们(例如,使用 Spring Session 将它们链接到某些共享状态)。除此之外,由下游服务设置的任何 cookie 可能对调用者都不有用,因此建议您将(至少)Set-Cookie
和Cookie
设置为不属于您域的路由的敏感 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.ReadTimeout
和ribbon.SocketTimeout
功能区属性配置这些超时。
如果通过指定 URL 配置了 Zuul 路由,则需要使用zuul.host.connect-timeout-millis
和zuul.host.socket-timeout-millis
。
18.15 重写 LocationHeaders
如果 Zuul 在 Web 应用程序的前面,则当 Web 应用程序通过 HTTP 状态代码3XX
重定向时,您可能需要重新编写Location
Headers。否则,浏览器将重定向到 Web 应用程序的 URL,而不是 Zuul URL。您可以配置LocationRewriteFilter
Zuul filter以将Location
Headers 重写为 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
个响应代码的Location
Headers,这可能不适用于所有情况,例如,将用户重定向到外部 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
中的GET
和POST
方法将跨域请求发送到以path-1
开头的端点。您可以使用/**
Map 将 CORS 配置应用于特定的路径模式或整个应用程序的全局路径。您可以通过此配置来自定义属性:allowedOrigins
,allowedMethods
,allowedHeaders
,exposedHeaders
,allowCredentials
和maxAge
。
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
中。有关将请求路由到何处,错误以及实际的HttpServletRequest
和HttpServletResponse
的信息存储在此处。 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
:使用 ServletRequestDispatcher
转发请求。转发位置存储在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_KEY
,PreDecorationFilter
将不运行,而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-Sample
Headers:
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