Zuul 架构图

在这里插入图片描述

在 Zuul 中,整个请求的处理流程如下:

  1. 请求首先由 ZuulServlet 处理。
  2. ZuulServlet 中包含一个 ZuulRunner 对象,该对象初始化了 RequestContextRequestContext 用于存储整个请求的相关数据,并被所有的 ZuulFilter 共享。
  3. ZuulRunner 中还包含 FilterProcessor,它是执行所有 ZuulFilter 的管理器。
  4. FilterProcessorFilterLoader 中获取 ZuulFilterZuulFilterFilterFileManager 加载,支持 Groovy 热加载(采用轮询方式)。
  5. 加载过滤器后,ZuulServlet 依次执行 pre 类型过滤器、route 类型过滤器,最后执行 post 类型过滤器。
  6. 若执行过程中发生错误,则会执行 error 类型过滤器。
  7. 执行完毕后,将请求结果返回给客户端。

Zuul 工作原理源码分析

前文已介绍过 Zuul 的基本用法,其中不可缺少的步骤是在程序启动类上添加 @EnableZuulProxy 注解。该注解的代码如下:

@EnableCircuitBreaker
@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyConfiguration.class)
public @interface EnableZuulProxy {
}

其中,@Import 引入了 ZuulProxyConfiguration。跟踪该类可知,它注入了 DiscoveryClientRibbonCommandFactoryConfiguration 用于负载均衡相关配置,并注入了一系列 Filters,例如 PreDecorationFilterRibbonRoutingFilterSimpleHostRoutingFilter 等。代码如下:

@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;
}

同时也注入了其他过滤器,例如 ServletDetectionFilterDebugFilterServlet30WrapperFilter,这些过滤器都是 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 类持有 FilterRegistryFilterFileManager 类持有 FilterLoader,因此最终是由 FilterFileManager 将 Filter 注入到 FilterRegistryConcurrentHashMap 中。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() 方法可以发现,该方法为每个请求生成了 RequestContextRequestContext 继承了 ConcurrentHashMap<String, Object>,在请求结束时销毁。RequestContext 的生命周期从 ZuulServlet 开始处理请求起,直到请求结束返回结果。

RequestContext 类存储了很多重要的信息,包括 HttpServletRequestHttpServletResponseResponseDataStreamResponseStatusCode 等。RequestContext 对象在处理请求的过程中一直存在,因此该对象为所有 Filter 共享。

ZuulServletservice() 方法可知,它是先处理 pre 类型的处理器,然后处理 route 类型的处理器,最后再处理 post 类型的处理器。

首先来看 pre() 的处理过程,它会进入到 ZuulRunner。该类的作用是将请求的 HttpServletRequestHttpServletResponse 放在 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() 方法可以发现,它最终调用了 FilterLoadergetFiltersByType(sType) 方法来获取同一类型的过滤器,然后用 for 循环遍历所有的 ZuulFilter,执行了 processZuulFilter() 方法。跟踪该方法可以发现,最终是执行了 ZuulFilterrun() 方法,并返回该方法的 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;
}

routepost 类型的过滤器的执行过程和 pre 执行过程类似。

Zuul 默认过滤器

默认的核心过滤器一览表

Zuul 默认注入的过滤器,它们的执行顺序在 FilterConstants 类中定义。我们可以先定位在这个类,查看过滤器的执行顺序以及相关注释,从而轻松定位到相关的过滤器。也可以直接打开 spring-cloud-netflix-core.jarzuul.filters 包查看一系列 Filter。现将默认注入的 Filter 以表格形式列出:

过滤器Order描述类型
ServletDetectionFilter-3检测请求是用 DispatcherServlet 还是 ZuulServletpre
Servlet30WrapperFilter-2在 Servlet 3.0 下,包装 requestspre
FormBodyWrapperFilter-1解析表单数据pre
SendErrorFilter0如果中途出现错误error
DebugFilter1设置请求过程是否开启 debugpre
PreDecorationFilter5根据 URI 决定调用哪一个 route 过滤器pre
RibbonRoutingFilter10如果配置中使用 ServiceId 则用这个 route 过滤器,该过滤器可以用 Ribbon 做负载均衡,用 Hystrix 做熔断route
SimpleHostRoutingFilter100如果配置中使用 url 则用这个 route 过滤器route
SendForwardFilter500用 RequestDispatcher 请求转发route
SendResponseFilter1000将响应结果返回给客户端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 对象中。同时调用了 ProxyRequestHelpersetResponse 方法,将请求状态码、流等信息保存在 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,而 ResponseDataStreamroute 类型过滤器中已经设置进去了。具体代码如下:

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。