Spring Cloud(九):API网关 Zuul 其它详细设置
该篇是 Spring Cloud系列(八):路由和过滤器之API网关 Zuul 的延续,主要讲解 Zuul 的相关配置,比如编码、过滤器、熔断回退、超时、跨域等。
Zuul 编码
请求参数编码
处理请求时,可能需要对查询参数进行解码,便于在 Zuul 过滤器中对这些参数进行修改,然后重新编码,在路由过滤器重建后端请求。
例如,使用 Javascript 的 encodeURIComponent() 方法编码的,则接收到的结果可能与原始输入不同。
虽然大多数情况下不会有问题,但有些 Web 服务器对复杂查询字符串的编码需要特别的处理。
要强制使用参数的原始编码,需要设置 zuul.forceOriginalQueryStringEncoding 为 true,以便使用 HttpServletRequest::getQueryString 方法按原样获取请求参数,如下示例:
1 | true = |
该属性激活后,在 SimpleHostRoutingFilter 中构建后端 URI 时强制执行原始查询字符串编码,将使用 HttpServletRequest getQueryString() 方法,而不是 UriTemplate 构建参数字符串。
注意:此方式仅适用于 SimpleHostRoutingFilter,不在 RibbonRoutingFilter 中使用,而是通过 DiscoverClient(如 Eureka) 找到服务。此外,无法使用 RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters) 轻松覆盖请求参数,因为参数字符串现在直接在原始 HttpServletRequest 上获取。
请求 URI 编码
处理传入请求时,在与路由匹配之前对请求 URI 进行解码,然后在路由过滤器中重建后端请求时,将重新编码 URI。如果 URI 中包含编码的 /
字符,则可能导致意外的情况。
要使用原始请求 URI, 将 zuul.decodeUrl 设置为 false,以便使用 HttpServletRequest :: getRequestURI方法获取原始 URI 来使用。
1 | false = |
注意:如果使用 requestURI RequestContext 属性覆盖请求 URI 并且此标志设置为 false,则不会对请求上下文中设置的 URL 进行编码。 您有责任确保 URL 已经编码。
Zuul 更多设置
Zuul 超时设置
如果要配置通过 Zuul 代理的请求的 Socket 超时 和 读超时,在配置中需要添加两个配置项:
- 如果 Zuul 使用服务发现,需要配置
ribbon.ReadTimeout
和ribbon.SocketTimeout
这两个 Ribbon 属性。
如果配置 Zuul 路由指定了 URL(Ribbon 负载均衡失效),则需要使用 zuul.host.connect-timeout-millis
和 zuul.host.socket-timeout-millis
属性。
3xx 状态码重定向
如果 Zuul 面向的是一个 Web 应用,则当 Web 应用程序通过 HTTP 状态码 3XX 重定向时,可能需要重写 Location 头。否则,浏览器会重定向到 Web 应用程序的 URL 而不是 Zuul URL。
可以配置 LocationRewriteFilter Zuul 过滤器以将 Location 标头重新写入 Zuul 的 URL,该过滤器会去除 全局前缀 和 特定路由前缀。如下示例:
1 | import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter; |
注意:使用此过滤器需要小心些,该过滤器作用于所有 3XX 响应代码的重定向,这可能并不适用于所有场景,例如将用户重定向到外部 URL 时。
启用跨域请求
默认情况下,Zuul 将所有跨源请求(cors)路由到服务。如果希望 Zuul 来处理这些请求,可以通过自定义WebMVCConfigurer 类型的 Bean 来实现:
1 |
|
上面的示例,允许来自 http://allowed-origin.com 的 get 和 post 方法向以 path-1 开始的端点发送跨域请求。
可将 CORS 配置应用于指定的路径模式,或者使用 /**
映射整个应用程序允许全局跨域请求。
可以通过此配置自定义属性:allowedOrigins、allowedMethods、allowedHeaders、exposedHeaders、allowCredentials 和 maxAge。
Zuul 预加载 Ribbon
Zuul 内部使用 Ribbon 来调用远程 URL。默认情况下,Ribbon 客户端是懒加载的,只有在被 Spring Cloud 第一次调用时才加载。这样可能会在第一次调用时因需要加载而导致超时异常。
可以开启 Ribbon 饥饿加载(预加载),应用在启动时就加载与 Ribbon 相关的应用上下文:
1 | true = |
普通嵌入式 Zuul
如果使用 @EnableZuulServer(而不是 @EnableZuulProxy),仍可以运行 Zuul 服务器,而无需代理或有选择地切换代理平台的各个部分。
添加到应用程序中 ZuulFilter 类型的任何 bean 都会自动安装(与@EnableZuulProxy 一样),但不会自动添加任何代理过滤器。
在这种情况下,仍然通过配置 zuul.routes 来指定进入 Zuul 服务器的路由,但是没有服务发现和代理。 因此,将忽略 serviceId 和 url 设置。 以下示例将 /api /** 中的所有路径映射到Zuul过滤器链:
1 | /api/** = |
Zuul 重试与熔断回退
当 Zuul 中给定的路由触发了熔断,可以创建 FallbackProvider 类型的 Bean 来提供回退响应。在此 Bean 中,需要指定回退所针对的路由 ID ,并提供 ClientHttpResponse 作为回退返回。
容错重试
在集群环境中,当某个服务不可用时,能够自动切换到基它服务器上去,也就需要重试集制。
Zuul 中开启重试机制需要依赖 spring-retry,导入此包:
1 | <dependency> |
Zuul 集成了 Ribbon 来实现负载均衡,重试机制是在 Ribbon 调用执行的,配置重试:
1 | # 超时时间 |
指定路由回退
以下示例显示了一个简单的 FallbackProvider 实现:
路由设置
1
/customers/** =
熔断回退实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51class MyFallbackProvider implements FallbackProvider {
public String getRoute() {
return "customers";
}
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() {
public HttpStatus getStatusCode() throws IOException {
return status;
}
public int getRawStatusCode() throws IOException {
return status.value();
}
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
public void close() {
}
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
所有路由默认回退
如果要为所有路由提供默认回退,可以创建一个 FallbackProvider 类型的 Bean,并让 getRoute
方法返回 *
或空,如下例所示:
1 | class MyFallbackProvider implements FallbackProvider { |
Zuul Servlet及服务注解
zuul 作为 Servlet 的实现,对于一般情况,Zuul 嵌入到 Spring Dispatch 机制中,这样可以让 SpringMVC 控制路由。
此情况下,Zuul 缓冲请求,如果需要在不缓冲请求的情况下通过 Zuul (例如,对于大型文件上载),Servlet 需要安装在 Spring Dispatcher之外。默认情况下,Servlet 的地址为 /zuul
,可以使用 zuul.servlet-path
属性更改此路径。
Zuul RequestContext
为了在过滤器之间传递数据,Zuul 使用了一个 RequestContext,数据保存每个请求 ThreadLocal 中,存储了路由请求、错误和实际的 HttpServletRequest 和 HttpServletResponse 的信息。详细可查看源码:com.netflix.zuul.context.RequestContext
1 | RequestContext ctx = RequestContext.getCurrentContext(); |
RequestContext 扩展(继承)了 ConcurrentHashMap,因此任何内容都可以存储在上下文中。FilterConstants包含了过滤器需要使用的键(key),这些过滤器由Spring Cloud Netflix 安装。
1 |
|
@EnableZuulProxy 与@EnableZuulServer
Spring Cloud Netflix 安装了许多过滤器,这取决于使用哪个注释来启用 Zuul。
@EnableZuulProxy 注解启用的过滤器是 @EnableZuulServer 的超集,即 @EnableZuulProxy 注解包含了 @EnableZuulServer 注解安装的所有过滤器。
@EnableZuulProxy 注解的代理提供的过滤器启用了路由功能,如果需要使用普通的 Zuul,则应使用 @EnableZuulServer。
Zuul 过滤器
有关 Zuul 工作方式的概述,请参见 Zuul Wiki。
Zuul 过滤器关键特性
- Type:通常用于定义执行过滤器时请求出的阶段。
- Order:执行顺序,定义多个过滤器的执行顺序。
- Criteria:执行过滤器所需的条件。
- Action:满足条件时要执行的操作。
Zuul 提供了一个可以动态读取,编译和运行这些过滤器的框架。 过滤器不直接相互通信,而是通过 RequestContext 共享状态,RequestContext 对每个请求是唯一的。
每个 Filter 的源码被写入到 Zuul 服务器上的一组指定的目录,这些目录会定期轮询更新。从磁盘中读取更新的过滤器、动态编译到运行的服务器,并由 Zuul 为每个后续请求调用。
Zuul 过滤器类型
Zuul 中的过滤器类型主要可分 4 种,并与请求的生命周期相对应:
- PRE:在请求被路由之前调用。适用于身份认证的场景,认证通过后继续执行后面的流程。
- ROUTING: 在路由请求时调用。在这里使用 Apache HttpClient 或 Netflix Ribbon 来构建和发送 HTTP 请求的。主要用作代理、转换,只在使用 @EnableZuulProxy 注解开启代理后有效。
- POST:在将请求路由到目标服务器后执行,即在 route 和 error 过滤器之后被调用。适用于添加响应头,收集统计信息和指标,以及将响应从后端服务传输到客户端。
- ERROR:请求中的任何阶段发生错误时被调用。可以用于统一记录错误信息。
Zuul Request Lifecycle(Zuul 请求生命周期)
HTTP 请求先进入 pre 过滤器,再到 routing 过滤器,最后到 post 过滤器,中间任何一个过滤器有异常都会进入 error 过滤器。
Zuul 过滤器执行顺序源码
在 Zuul 自定配置类中会根据条件注册 ZuulServlet 类型的 Bean ,ZuulServlet 继承自 HttpServlet,过滤器的调用是在 Servlet 的 service() 方法中,在 service() 方法中可以明确的看到过滤器的调用顺序。
1 |
|
1 |
|
Zuul 过滤器拦截请求
当满足某些条件时就拦截请求,不将当前请求转换到后端服务,设置 ctx.setSendZuulResponse(false) 即可。
另 Zuul 的过滤器与 java.servlet.Filter 不同,Filter.doFilter 之前返回,则不会执行后面的过滤器;而 ZuulFilter 不同,设置为 false 后,只是当前过滤器结束,后面还有其它过滤器等着执行。
1 | RequestContext ctx = RequestContext.getCurrentContext(); |
Zuul 禁用过滤器
Zuul 为 Spring Cloud 在代理和服务器模式下默认启用了许多 ZuulFilter 类型的 bean。 有关可以启用的过滤器列表,请参阅 the Zuul filters package。
如果要禁用某一个,请设置 zuul.<SimpleClassName>.<filterType>.disable=true
。按照惯例,过滤器后的包是 Zuul 过滤器类型。
例如,要禁用 org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,需设置zuul.SendResponseFilter.post.disable = true。
@EnableZuulServer Filters
@EnablezuulServer 注解会创建一个 SimpleRouteLocator,它加载在 Spring Boot 配置文件中定义的路由。
该注解安装了以下过滤器(作为普通的 Spring Bean):
- Pre filters:
- ServletDetectionFilter:检查请求是否通过 Spring Dispatcher,设置类型是 Boolean 值的属性FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY。
- FormBodyWrapperFilter:解析表单数据并为下游请求重新编码。
- DebugFilter:如果设置了 debug 请求参数,则将 RequestContext.setDebugRouting() 和 RequestContext.setDebugRequest() 设置为 true。
- SendForwardFilter:使用 Servlet RequestDispatcher 转发请求,转换位置存储在 RequestContext 属性 FilterConstants.FORWARD_TO_KEY中。这对于当前应用转发到端点非常有用。
- Post filters:
- SendResponseFilter:将代理请求的响应写入当前响应。
- Error filters:
- SendErrorFilter:如果 RequestContext.getThrowable() 不为空,则转发到 /error(默认情况下)。可以通过设置 error.path 属性来改变错误的默认转发路径(/error) 。.
@EnableZuulProxy Filters
@EnableZuulServer 注解的有的过滤器,@EnableZuulProxy 注解都有。
- Pre filters:
- PreDecorationFilter:根据提供的 RouteLocator 确定路由的位置和方式。它还为下游请求设置各种与代理相关的头。
- Route filters:
- RibbonRoutingFilter:使用 Ribbon、Hystrix 和 可插拨式 HTT Clients 发送请求,Service ID 是从在 RequestContext 的 FilterConstants.SERVICE_ID_KEY 中查找。此过滤器可使用不同的 HTTP Client。
- Apache HttpClient:默认的客户端。
- Squareup OkHttpClient v3:添加了 com.squareup.okhttp3:okhttp 依赖,并设置了 ribbon.okhttp.enabled=true,即启动该客户端。
- Netflix Ribbon HTTP client:设置 ribbon.restclient.enabled=true 来启用。此客户端有一些限制,包括不支持 PATCH 请求方式,但有内置重试机制。
- SimpleHostRoutingFilter:通过 Apache HttpClient. 将请求发送到预定的URL。URL 在 RequestContext.getRouteHost() 中查找。
- RibbonRoutingFilter:使用 Ribbon、Hystrix 和 可插拨式 HTT Clients 发送请求,Service ID 是从在 RequestContext 的 FilterConstants.SERVICE_ID_KEY 中查找。此过滤器可使用不同的 HTTP Client。
自定义过滤器
Zuul Filters 使用官方示例:spring-cloud-samples/sample-zuul-filters,还有一些在 repository 中对 请求和响应体的操作。
前置过滤器
前置过滤器在 RequestContext 中设置数据,以便以后置的过滤器中使用。主要作用是设置路由过滤器所需的信息。以下示例了 Zuul 前置过滤器:
1 | public class QueryParamPreFilter extends ZuulFilter { |
上面的示例,前置过滤器设置的 SERVICE_ID_KEY 是从请求参数中获取。实际上,不应该使用这种直接映射,而应该从 sample 值中来查询 Service ID。
现在已经设置了 SERVICE_ID_KEY ,那么 PreDecorationFilter 将不运行,运行的是 RibbonRoutingFilter 过滤器。SERVICE_ID_KEY 是过滤器常量 FilterConstants 中的静态变量,可直接调用。
如果想要路由到的一绝对 URL,调用 ctx.setRouteHost(url) 来替换。
要修改路由过滤器转发到的目标路径,可以设置 REQUEST_URI_KEY。
路由过滤器
路由过滤器在前置过滤器后运行,并向其他服务发送请求。路由过滤器的大部分工作是将请求和响应数据转换为客户端所需的的模型。
以下示例显示了Zuul路由过滤器:
1 | public class OkHttpRoutingFilter extends ZuulFilter { |
上面示例的过滤器将 Servlet 请求信息转换为 OkHttp3 请求信息,执行 HTTP 请求,并将 OkHttp3 响应信息转换为 Servlet 响应。
Post 过滤器
Post 过滤器通常用于对响应的操作,以下过滤器将随机 UUID 添加为 X-Sample 头:
1 | public class AddResponseHeaderFilter extends ZuulFilter { |
其他的操作,比如转换响应体,则要复杂的多,计算量也大的多。
Errors 过滤器
Zuul 过滤器(链)生命周期的任何部分发生异常,都会执行 Error 过滤器。SendErrorFilter 过滤器只在 RequestContext.getThrowable() 非空时运行,它会在请求中设置javax.servlet.error.*
属性,并将请求转发到 Spring Boot 的错误页面。
1 |
|
报错会重定向到 /error
路径,默认返回的是 Spring Boot 的 Withelable Error Page 页面,可以自定义 /error 的 Controller,实现 ErrorController,改为返回 JSON 数据。如下示例:
1 | /** |
Zuul 高可用
通常的做法是用额外的负载均衡器来实现 Zuul 的高可用,比如常用的 Nginx 等。
部署多个 Zuul 实例,在 Zuul 前面加个负载均衡层进行反向代理,在配置文件中手动设置多个 Zuul 节点的地址,重载配置。
若想要动态扩展,可以在管理运维后端定时从 Eureka 注册中心获取服务列表,比对 Zuul 节点是否有变更,若有变更则修改 Nginx 配置,或通过脚本来修改,然后重置配置来达到网关的动态扩容。
Spring Cloud(九):API网关 Zuul 其它详细设置
http://blog.gxitsky.com/2019/04/05/SpringCloud-09-route-filter-Zuul-config/