上文介绍了Zuul的基本使用与路由功能,本文接着介绍Zuul的核心概念 —— Zuul过滤器(filter)。

Zuul的功能基本通过Zuul过滤器来实现(类比于Struts的拦截器,只是Struts拦截器用到责任链模式,Zuul则是通过FilterProcessor来控制执行),在不同的阶段,通过不同类型的过滤器来实现相应的功能。

Zuul过滤器

过滤器类型

zuul的过滤器根据对HTTP请求的不同处理阶段包括如下四种类型

  • pre :在请求转发到后端目标服务之前执行,一般用于请求认证、确定路由地址、日志记录等
  • route :转发请求,使用Apache HttpClient 或 Ribbon来构造对目标服务的请求
  • post :在目标服务返回结果后对结果进行处理,比如添加响应头、收集统计性能数据等
  • error :在请求处理的整个流程中如果出现错误,则会触发error过滤器执行,对错误进行处理

客户端请求经过zuul过滤器处理的流程如下图

zuul-filter

zuul使用RequestContext来在过滤器之间传递数据,数据存于每个request的ThreadLocal,包含请求路由到哪里,错误,HttpServletRequest,HttpServletResponse 等这些数据都存储于RequestContext中。RequestContext 扩展了ConcurrentHashMap,所以我们可以根据需要将信息存于context中进行传递。

@EnableZuulProxy vs @EnableZuulServer

zuul提供了两个注解 @EnableZuulProxy, @EnableZuulServer,来启用不同的过滤器集合。@EnableZuulProxy 启用的过滤器 是@EnableZuulServer 的超集, 它包含了@EnableZuulServer 的所有过滤器,proxy主要多了一些提供路由功能的过滤器(可见@EnableZuulServer 不提供路由功能,作为server模式而不是代理模式运行)

@EnableZuulServer 注解启用的过滤器包括

filter类型 实现类 filter顺序值 功能说明
pre ServletDetectionFilter -3 检测请求是否通过Spring Dispatcher,并在RequestContext 中添加一个key为isDispatcherServletRequest, 值为true(不通过则为false)的属性
pre FormBodyWrapperFilter -1 解析Form data,为请求的下游进行重新编码
pre DebugFilter 1 如果请求参数设置了debug,则会将RequestContext.setDebugRouting() ,RequestContext.setDebugRequest() 设置为ture
route SendForwardFilter 500 使用RequestDispatch servlet来转发请求,转发地址存于RequestContext中key为FilterConstants.FORWARD_TO_KEY的属性中,对于转发到当前应用的接口比较有用
post SendResponseFilter 1000 将代理请求的响应内容写到当前的响应中
error SendErrorFilter 0 如果RequestContext.getThrowable() 不为空,则会转发到/error,可以通过error.path来改变默认的转发路径/error

@EnableZuulProxy 除了上面的过滤器,还包含如下过滤器

filter类型 实现类 filter顺序值 功能说明
pre PreDecorationFilter 5 确定路由到哪里,如何路由,依赖提供的RouteLocator,同时也为下游请求设置多个与proxy相关的header
route RibbonRoutingFilter 10 使用ribbon,hystrix,以及内嵌的http client来发送请求,可在RequestContext中通过FilterConstants.SERVICE_ID_KEY 来找到路由Service的ID
route SimpleHostRoutingFilter 100 使用Apache httpClient来发送请求到一个预先确定的url,可通过RequestContext.getRouteHost()来获取urls

由上可见@EnableZuulServer 注解并不包含往后端服务负载均衡地路由请求的代理功能,@EnableZuulProxy的PreDecorationFilter,RibbonRoutingFilter过滤器才能担当此任。PreDecorationFilter通过提供的DiscoveryClientRouteLocator 从 DiscoveryClient(如Eureka)与属性文件中加载路由定义, 为每个serviceId创建一个route,新服务添加进来,路由也会动态刷新。路由确定了,在RibbonRoutingFilter 中通过ribbon与hystrix结合来向后端目标服务发起请求,并进行负载均衡。过滤器的顺序值表示在同类型过滤器中的执行顺序,值越小越先执行。

自定义Zuul过滤器

自定义的zuul过滤器与框架自带过滤器类似,包括四部分

  1. 过滤器类型,包括pre, route, post
  2. 过滤器顺序,定义在同类型过滤器中的执行顺序,数值越小越先执行
  3. 是否执行过滤,通过一些条件判断来确定是否执行该过滤器
  4. 过滤器执行体,定义具体执行的操作

比如我们需要在Http请求头中设置一个值,供请求链路的下游环节访问,则可以自定义一个过滤器如下,

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
@Component
public class ReqIdPreFilter extends ZuulFilter {

@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1; //在PreDecorationFilter过滤器之前执行
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader("reqId", UUID.randomUUID().toString());
return null;
}
}

在请求的后续环节,比如后端服务的filter或接口中,则可直接从HttpServletRequest 获取该header值,如

1
2
3
4
@GetMapping("hello/reqId")
public String getReqId(HttpServletRequest request) {
return "hello-service返回:" + request.getHeader("reqId");
}

Zuul的错误处理

在zuul过滤器的生命周期中,如果任何一个环节抛出异常,则error过滤器会被执行,SendErrorFilter只有当RequestContext.getThrowable()不为null时才会运行,会设置javax.servlet.error.* 属性到request中,然后将请求转发到spring boot的error page, 默认为BasicErrorController实现的/error接口。 有时候我们需要将返回响应格式进行统一,而默认的/error接口实现可能不满足要求,则可以自定义/error接口。需要实现ErrorController 接口以使默认的BasicErrorController 失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class ZuulErrorController implements ErrorController {

@RequestMapping("/error")
public Map<String, String> error(HttpServletRequest request){
Map<String, String> result = Maps.newHashMap();
result.put("code", request.getAttribute("javax.servlet.error.status_code").toString());
result.put("message", request.getAttribute("javax.servlet.error.message").toString());
result.put("exception", request.getAttribute("javax.servlet.error.exception").toString());
return result;
}

@Override
public String getErrorPath() {
return "/error";
}
}

Zuul的服务降级

当调用服务出现超时或异常时,在zuul侧可提供回调进行服务降级,返回默认响应结果,如

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
51
@Component
public class MyFallbackProvider implements FallbackProvider {

@Override
public String getRoute() {
return null; //指定这个回调针对的route Id,如果对所有route,则返回* 或null
}

@Override
public ClientHttpResponse fallbackResponse(String route, 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 {
Map<String, String> result = Maps.newLinkedHashMap();
result.put("code", "" + status.value());
String msg = HttpStatus.GATEWAY_TIMEOUT == getStatusCode() ? "请求服务超时" : "服务器内部错误";
result.put("message", msg);
return new ByteArrayInputStream(new ObjectMapper().writeValueAsString(result).getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}

则当服务请求失败时,统一返回如下格式的响应

1
2
3
4
{
"code": "500",
"message": "服务器内部错误"
}

总结

本文主要对Zuul过滤器相关内容及自定义使用进行了介绍,同时对过滤器运行过程中异常的处理及服务调用失败的降级回调进行了简单说明。出于篇幅,开发过程中更具体的细节我们后续再继续探讨。



认真生活,快乐分享
欢迎关注微信公众号:空山新雨的技术空间
微信公众号