深入理解Zuul之源码解析
Zuul 架构图

在 Zuul 中,整个请求的处理流程如下:
- 请求首先由
ZuulServlet处理。 ZuulServlet中包含一个ZuulRunner对象,该对象初始化了RequestContext。RequestContext用于存储整个请求的相关数据,并被所有的ZuulFilter共享。ZuulRunner中还包含FilterProcessor,它是执行所有ZuulFilter的管理器。FilterProcessor从FilterLoader中获取ZuulFilter。ZuulFilter由FilterFileManager加载,支持 Groovy 热加载(采用轮询方式)。- 加载过滤器后,
ZuulServlet依次执行 pre 类型过滤器、route 类型过滤器,最后执行 post 类型过滤器。 - 若执行过程中发生错误,则会执行 error 类型过滤器。
- 执行完毕后,将请求结果返回给客户端。
Zuul 工作原理源码分析
前文已介绍过 Zuul 的基本用法,其中不可缺少的步骤是在程序启动类上添加 @EnableZuulProxy 注解。该注解的代码如下:
@EnableCircuitBreaker
@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyConfiguration.class)
public @interface EnableZuulProxy {
}其中,@Import 引入了 ZuulProxyConfiguration。跟踪该类可知,它注入了 DiscoveryClient、RibbonCommandFactoryConfiguration 用于负载均衡相关配置,并注入了一系列 Filters,例如 PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter 等。代码如下:
@Bean
public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
return new PreDecorationFilter(routeLocator, this.server.getServletPrefix(), this.zuulProperties,
proxyRequestHelper);
}
// route filters
@Bean
public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
RibbonCommandFactory<?> ribbonCommandFactory) {
RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers);
return filter;
}
@Bean
public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties) {
return new SimpleHostRoutingFilter(helper, zuulProperties);
}其父类 ZuulConfiguration 引用了一些相关配置。在缺失 zuulServlet Bean 的情况下,它会注入 ZuulServlet,该类是 Zuul 的核心类:
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesn't
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}同时也注入了其他过滤器,例如 ServletDetectionFilter、DebugFilter、Servlet30WrapperFilter,这些过滤器都是 pre 类型的:
@Bean
public ServletDetectionFilter servletDetectionFilter() {
return new ServletDetectionFilter();
}
@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
@Bean
public Servlet30WrapperFilter servlet30WrapperFilter() {
return new Servlet30WrapperFilter();
}此外,它还注入了 post 类型的过滤器(如 SendResponseFilter)、error 类型的过滤器(如 SendErrorFilter)以及 route 类型的过滤器(如 SendForwardFilter)。代码如下:
@Bean
public SendResponseFilter sendResponseFilter() {
return new SendResponseFilter();
}
@Bean
public SendErrorFilter sendErrorFilter() {
return new SendErrorFilter();
}
@Bean
public SendForwardFilter sendForwardFilter() {
return new SendForwardFilter();
}系统会初始化 ZuulFilterInitializer 类,将所有的 Filter 向 FilterRegistry 注册:
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map<String, ZuulFilter> filters;
@Bean
public ZuulFilterInitializer zuulFilterInitializer(
CounterFactory counterFactory, TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
}
}FilterRegistry 管理了一个 ConcurrentHashMap 用于存储过滤器,并提供了一些基本的 CRUD 方法。代码如下:
public class FilterRegistry {
private static final FilterRegistry INSTANCE = new FilterRegistry();
public static final FilterRegistry instance() {
return INSTANCE;
}
private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();
private FilterRegistry() {
}
public ZuulFilter remove(String key) {
return this.filters.remove(key);
}
public ZuulFilter get(String key) {
return this.filters.get(key);
}
public void put(String key, ZuulFilter filter) {
this.filters.putIfAbsent(key, filter);
}
public int size() {
return this.filters.size();
}
public Collection<ZuulFilter> getAllFilters() {
return this.filters.values();
}
}FilterLoader 类持有 FilterRegistry,FilterFileManager 类持有 FilterLoader,因此最终是由 FilterFileManager 将 Filter 注入到 FilterRegistry 的 ConcurrentHashMap 中。FilterFileManager 开启了轮询机制,定时加载过滤器。代码如下:
void startPoller() {
poller = new Thread("GroovyFilterFileManagerPoller") {
public void run() {
while (bRunning) {
try {
sleep(pollingIntervalSeconds * 1000);
manageFiles();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
poller.setDaemon(true);
poller.start();
}ZuulServlet 类似于 Spring MVC 中的 DispatcherServlet,起到了前端控制器的作用,所有的请求都由它接管。它的核心代码如下:
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}跟踪 init() 方法可以发现,该方法为每个请求生成了 RequestContext。RequestContext 继承了 ConcurrentHashMap<String, Object>,在请求结束时销毁。RequestContext 的生命周期从 ZuulServlet 开始处理请求起,直到请求结束返回结果。
RequestContext 类存储了很多重要的信息,包括 HttpServletRequest、HttpServletResponse、ResponseDataStream、ResponseStatusCode 等。RequestContext 对象在处理请求的过程中一直存在,因此该对象为所有 Filter 共享。
从 ZuulServlet 的 service() 方法可知,它是先处理 pre 类型的处理器,然后处理 route 类型的处理器,最后再处理 post 类型的处理器。
首先来看 pre() 的处理过程,它会进入到 ZuulRunner。该类的作用是将请求的 HttpServletRequest、HttpServletResponse 放在 RequestContext 类中,并包装了一个 FilterProcessor。代码如下:
public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
RequestContext ctx = RequestContext.getCurrentContext();
if (bufferRequests) {
ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
} else {
ctx.setRequest(servletRequest);
}
ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}
public void preRoute() throws ZuulException {
FilterProcessor.getInstance().preRoute();
}而 FilterProcessor 类负责调用 Filters,例如调用所有 pre 类型的过滤器:
public void preRoute() throws ZuulException {
try {
runFilters("pre");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
}
}跟踪 runFilters() 方法可以发现,它最终调用了 FilterLoader 的 getFiltersByType(sType) 方法来获取同一类型的过滤器,然后用 for 循环遍历所有的 ZuulFilter,执行了 processZuulFilter() 方法。跟踪该方法可以发现,最终是执行了 ZuulFilter 的 run() 方法,并返回该方法的 Object 对象。
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
Object result = processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}route、post 类型的过滤器的执行过程和 pre 执行过程类似。
Zuul 默认过滤器
默认的核心过滤器一览表
Zuul 默认注入的过滤器,它们的执行顺序在 FilterConstants 类中定义。我们可以先定位在这个类,查看过滤器的执行顺序以及相关注释,从而轻松定位到相关的过滤器。也可以直接打开 spring-cloud-netflix-core.jar 的 zuul.filters 包查看一系列 Filter。现将默认注入的 Filter 以表格形式列出:
| 过滤器 | Order | 描述 | 类型 |
|---|---|---|---|
| ServletDetectionFilter | -3 | 检测请求是用 DispatcherServlet 还是 ZuulServlet | pre |
| Servlet30WrapperFilter | -2 | 在 Servlet 3.0 下,包装 requests | pre |
| FormBodyWrapperFilter | -1 | 解析表单数据 | pre |
| SendErrorFilter | 0 | 如果中途出现错误 | error |
| DebugFilter | 1 | 设置请求过程是否开启 debug | pre |
| PreDecorationFilter | 5 | 根据 URI 决定调用哪一个 route 过滤器 | pre |
| RibbonRoutingFilter | 10 | 如果配置中使用 ServiceId 则用这个 route 过滤器,该过滤器可以用 Ribbon 做负载均衡,用 Hystrix 做熔断 | route |
| SimpleHostRoutingFilter | 100 | 如果配置中使用 url 则用这个 route 过滤器 | route |
| SendForwardFilter | 500 | 用 RequestDispatcher 请求转发 | route |
| SendResponseFilter | 1000 | 将响应结果返回给客户端 | post |
过滤器的 order 值越小,就越先执行。并且在执行过滤器的过程中,它们共享了一个 RequestContext 对象,该对象的生命周期贯穿于请求。可以看出优先执行了 pre 类型的过滤器,并将执行后的结果放在 RequestContext 中,供后续的 Filter 使用。
例如,在执行 PreDecorationFilter 的时候,决定使用哪一个 route,它的结果会放在 RequestContext 对象中。后续会执行所有的 route 过滤器,如果不满足条件就不执行该过滤器的 run 方法,最终达到只执行一个 route 过滤器的 run() 方法的效果。
而 error 类型的过滤器,是在程序发生异常的时候执行的。
post 类型的过滤,在默认的情况下,只注入了 SendResponseFilter。该类型的过滤器是将最终的请求结果以流的形式输出给客户端。
SimpleHostRoutingFilter 工作原理
进入到 SimpleHostRoutingFilter 类的 run() 方法,核心代码如下:
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
// 省略代码
String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders();
try {
CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
headers, params, requestEntity);
setResponse(response);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
return null;
}查阅这个类的全部代码可知,该类创建了一个 HttpClient 作为请求类,并重构了 URL,请求到了具体的服务,得到一个 CloseableHttpResponse 对象,并将该对象保存到 RequestContext 对象中。同时调用了 ProxyRequestHelper 的 setResponse 方法,将请求状态码、流等信息保存在 RequestContext 对象中。
private void setResponse(HttpResponse response) throws IOException {
RequestContext.getCurrentContext().set("zuulResponse", response);
this.helper.setResponse(response.getStatusLine().getStatusCode(),
response.getEntity() == null ? null : response.getEntity().getContent(),
revertHeaders(response.getAllHeaders()));
}SendResponseFilter 工作原理
这个过滤器的 order 为 1000,在默认且正常的情况下,是最后一个执行的过滤器。该过滤器负责将最终得到的数据返回给客户端。
在它的 run() 方法里,有两个主要方法:addResponseHeaders() 和 writeResponse(),即添加响应头和写入响应数据流。
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}其中 writeResponse() 方法是通过从 RequestContext 中获取 ResponseBody 或者 ResponseDataStream 来写入到 HttpServletResponse 中的。但是在默认的情况下 ResponseBody 为 null,而 ResponseDataStream 在 route 类型过滤器中已经设置进去了。具体代码如下:
private void writeResponse() throws Exception {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
// 代码省略
OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null;
try {
if (RequestContext.getCurrentContext().getResponseBody() != null) {
String body = RequestContext.getCurrentContext().getResponseBody();
writeResponse(
new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding())),
outStream);
return;
}
// 代码省略
is = context.getResponseDataStream();
InputStream inputStream = is;
// 代码省略
writeResponse(inputStream, outStream);
// 代码省略
}
// 代码省略
}如何在 Zuul 上做日志处理
由于 Zuul 作为 API 网关,所有的请求都经过这里,所以在网关上可以做请求相关的日志处理。
需求示例:需要记录请求的 URL、IP 地址、参数、请求发生的时间、整个请求的耗时、请求的响应状态,甚至请求响应的结果等。
很显然,需要实现这样的一个功能,需要写一个 ZuulFilter。它应该是在请求发送给客户端之前做处理,并且在 route 过滤器路由之后。在默认的情况下,这个过滤器的 order 应该为 500-1000 之间。那么如何获取这些需要的日志信息呢?查找 RequestContext,在请求的生命周期里这个对象存储了整个请求的所有信息。
现在进行编码,在代码的注释中做了详细的说明。代码如下:
@Component
public class LoggerFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod(); // 请求的类型,post get ..
Map<String, String> params = HttpUtils.getParams(request);
String paramsStr = params.toString(); // 请求的参数
long startTime = (long) context.get("startTime"); // 请求的开始时间
Throwable throwable = context.getThrowable(); // 请求的异常,如果有的话
request.getRequestURI(); // 请求的 uri
HttpUtils.getIpAddress(request); // 请求的 IP 地址
context.getResponseStatusCode(); // 请求的状态
long duration = System.currentTimeMillis() - startTime; // 请求耗时
return null;
}
}现在读者也许有疑问,如何得到 startTime,即请求开始的时间?其实这需要另外一个过滤器。在网络请求 route 之前(大部分耗时都在 route 这一步),在过滤器中将时间存储到 RequestContext 即可。另写一个过滤器,代码如下:
@Component
public class AccessFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("startTime", System.currentTimeMillis());
return null;
}
}可能还有这样的需求:需要将响应结果也存储在 log 中。在之前已经分析了,在 route 结束后,将从具体服务获取的响应流存储在 RequestContext 中,在 SendResponseFilter 过滤器写入到 HttpServletResponse 中,最终返回给客户端。那么我只需要在 SendResponseFilter 写入响应流之前,把响应流写入到 log 日志中即可。
这会引发另外一个问题:因为响应流写入到 log 后,RequestContext 就没有响应流了,SendResponseFilter 就没有流输入到 HttpServletResponse 中,导致客户端没有任何返回数据。解决办法如下:
InputStream inputStream = RequestContext.getCurrentContext().getResponseDataStream();
InputStream newInputStream = copy(inputStream);
transferToLog(inputStream);
RequestContext.getCurrentContext().setResponseDataStream(newInputStream);从 RequestContext 获取到流之后,首先将流 copy 一份,将流转化为字符串存在日志中,再 set 到 RequestContext 中。这样 SendResponseFilter 就可以将响应返回给客户端。这样的做法有点影响性能,如果不是字符流,可能需要做更多的处理工作。
说明:本文基于 Spring Cloud Netflix Zuul 1.x 版本源码进行分析。Zuul 1.x 已进入维护模式,新项目建议考虑使用 Spring Cloud Gateway 或 Zuul 2.x。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/shen-ru-li-jie-zuul-zhi-yuan-ma-jie-xi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。