Skip to content

10同步网关:如何剖析Zuul网关的工作原理?

上一课时中,我们详细介绍了 Spring Cloud 中的 Zuul 网关的使用方式。作为 API 网关的一种实现方式,Spring Cloud Netflix Zuul 为我们提供了丰富的网关控制功能。今天我们将对它的内部原理进行深入剖析。Spring Cloud Netflix Zuul 同样是基于 Netflix 旗下的 Zuul 组件,本课时内容我们也先从 Netflix Zuul 开始讲起。

ZuulFilter 组件架构

Zuul 响应 HTTP 请求的过程是一种典型的过滤器结构,内部提供了 ZuulFilter 组件来实现这一机制。作为切入点,我们先从 ZuulFilter 展开讨论。

ZuulFilter 的定义与 ZuulRegistry

在 Zuul 中,ZuulFilter 是 Zuul 中的关键组件,我们来看一下它在设计上的抽象过程。在 Netflix Zuul 中存在一个 IZuulFilter 接口,定义如下:

java
public interface IZuulFilter {
    boolean shouldFilter();
    Object run() throws ZuulException;
}

很明显,IZuulFilter 接口的 shouldFilter() 方法决定是否需要执行该过滤器,一般情况该方法都会返回 true,但我们可以使用该方法来根据场景动态设置过滤器是否生效。而 run() 方法显然代表着该 Filter 具体需要实现的业务逻辑。

IZuulFilter的直接实现类是 ZuulFilter,ZuulFilter 是一个抽象类,额外提供了如下所示的两个抽象方法:

java
//过滤器类型
abstract public String filterType();
//过滤器顺序	 
abstract public int filterOrder();

其中 filterType() 代表过滤器的类型,内置过滤器分为 PRE、ROUTING、POST 和 ERROR 四种,本课时后续内容会对这些过滤器进行具体展开;而 filterOrder() 方法用于设置过滤器的执行顺序,这个顺序用数字进行表示,数字越小则越先执行。

然后在 ZuulFilter 类中最核心的就是负责执行 Filter 的 runFilter() 方法,该方法的执行流程并不复杂,如下所示:

java
public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        if (!isFilterDisabled()) {
            if (shouldFilter()) {
                Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
                try {
                    Object res = run();
                    zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
                } catch (Throwable e) {
                    t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                    zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                    zr.setException(e);
                } finally {
                    t.stopAndLog();
                }
            } else {
                zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
            }
        }
        return zr;
}

既然有了过滤器,系统中就应该存在一个管理过滤器的组件。有时候,我们称这个组件为过滤器链。而在 Netflix Zuul 中,则使用了一个称为过滤器注册表 FilterRegistry 的组件来保存和维护所有 ZuulFilter。FilterRegistry 类定义如下:

java
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();
    } 
}

FilterRegistry 的实现意外得简单,这里采用了经典的单例模式来创建 FilterRegistry 实例。在 FilterRegistry 内部,可以看到就是直接使用了线程安全的 ConcurrentHashMap 来缓存 ZuulFilter。

有了 ZuulFilter 的定义以及保存它的媒介,接下来我们重点讨论 Netflix Zuul 中加载各种 ZuulFilter 的过程。

ZuulFilter 的加载与 FilterLoader

在 Zuul 中,FilterLoader 负责 ZuulFilter 的加载。FilterLoader 类是 Zuul 的一个核心类,这点从它的注释说明中就能看出:它用来在源码变化时编译、载入和校验过滤器。针对这个核心类,我们将从它的变量入手,对它进行分析。

java
//Filter 文件名与修改时间的映射
private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>();
//Filter 名称与代码的映射
private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>();
//Filter 名称与名称的映射,作用是判断该 Filter 是否存在
private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>();
//Filter 类型与 List<ZuulFilter> 的映射
private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
//前面提到的 FilterRegistry 单例
private FilterRegistry filterRegistry = FilterRegistry.instance();
//动态代码编译器实例,Zuul 提供的默认实现是 GroovyCompiler
static DynamicCodeCompiler COMPILER;
//ZuulFilter 工厂类
static FilterFactory FILTER_FACTORY = new DefaultFilterFactory();

这些变量中唯一值得注意的就是 DynamicCodeCompiler。顾名思义,它一种动态代码编译器。DynamicCodeCompiler 接口定义如下:

java
public interface DynamicCodeCompiler {
    Class compile(String sCode, String sName) throws Exception;
 
    Class compile(File file) throws Exception;
}

可以看到 DynamicCodeCompiler 提供了两个接口,一个是从代码编译到类,另一个是从文件编译到类。GroovyCompiler 是该接口的实现类,用于把 Groovy 代码编译为 Java 类。GroovyCompile 的实现如下所示:

java
public class GroovyCompiler implements DynamicCodeCompiler {
 
    GroovyClassLoader getGroovyClassLoader() {
        return new GroovyClassLoader();
    }
 
    @Override
    public Class compile(String sCode, String sName) {
        GroovyClassLoader loader = getGroovyClassLoader();
        Class groovyClass = loader.parseClass(sCode, sName);
        return groovyClass;
    }
 
    @Override
    public Class compile(File file) throws IOException {
        GroovyClassLoader loader = getGroovyClassLoader();
        Class groovyClass = loader.parseClass(file);
        return groovyClass;
	}
}

注意, GroovyCompile 的核心代码同样比较简单。我们通过 groovy.lang.GroovyClassLoader 构建一个类加载器,然后调用它的 parseClass 方法即可将一个 Groovy 文件加载为一个 Java 类。

有了 GroovyCompile 提供的动态加载和编译代码的能力,我们再回到 FilterLoader。FilterLoader 分别提供了 getFilter、putFilter 和 getFiltersByType 这三个工具方法,其中实际涉及加载和存储 Filter 的方法只有 putFilter,该方法如下所示:

java
public boolean putFilter(File file) throws Exception {
        String sName = file.getAbsolutePath() + file.getName();
        if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
            LOG.debug("reloading filter " + sName);
            filterRegistry.remove(sName);
        }
        ZuulFilter filter = filterRegistry.get(sName);
        if (filter == null) {
            Class clazz = COMPILER.compile(file);
            if (!Modifier.isAbstract(clazz.getModifiers())) {
                filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
                List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
                if (list != null) {
                    hashFiltersByType.remove(filter.filterType()); 
                }
                filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
                filterClassLastModified.put(sName, file.lastModified());
                return true;
            }
        }
 
        return false;
}

上述 putFilter 方法通过文件加载 ZuulFilter,整体执行流程也比较简洁明了。首先通过修改时间判断文件是否被修改过,如果被修改过则从 FilterRegistry 移除这个 Filter。然后根据 FilterRegistry 是否缓存了这个 Filter 来决定是否使用前面介绍 DynamicCodeCompiler 动态加载代码,而根据加载的代码再使用 FilterFactory 工厂类来实例化 ZuulFilter。

FilterFactory 的实现类是 DefaultFilterFactory,它的实例创建过程也很直接,就是利用了反射机制,如下所示:

java
@Override
public ZuulFilter newInstance(Class clazz) throws InstantiationException, IllegalAccessException {
        return (ZuulFilter) clazz.newInstance();
}

顺着 FilterLoader 类的 putFilter 方法继续往前走,我们需要明确该方法中传入的 File 对象从何而来。实际上这个 File 对象是由 FilterFileManager 类提供的。顾名思义,这个类的作用是管理这些 File 对象。FilterFileManager 中包含了如下所示的 manageFiles 和 processGroovyFiles 方法,后者调用了 FilterLoader 的 putFilter 方法以进行 Groovy 文件的处理。

java
void manageFiles() throws Exception, IllegalAccessException, InstantiationException {
        List<File> aFiles = getFiles();
        processGroovyFiles(aFiles);
}
	 
void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException {
        for (File file : aFiles) {
            FilterLoader.getInstance().putFilter(file);
        }
}

那么这个 manageFiles 方法是什么时候被调用的呢?注意到在 FilterFileManager 中存在一个每隔 pollingIntervalSeconds 秒就会轮询一次的后台守护线程,该线程就会调用 manageFiles 方法来重新加载 Filter,如下所示:

java
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();
}

分析到这里,我们对 ZuulFilter 的加载和管理流程已经非常清晰了。Zuul 通过文件来存储各种 ZuulFilter 的定义和实现逻辑,然后启动一个守护线程定时轮询这些文件,确保变更之后的文件能够动态加载到 FilterRegistry 中。

RequestContext 与上下文

使用过 Zuul 的同学都知道其中的 RequestContext 对象。我们可以通过该对象将业务信息放到请求上下文(Context)中,并使其在各个 ZuulFilter 中进行传递。上下文机制是众多开源软件中所必备的一种需求和实现方案。我们将通过学习 Zuul 中的 RequestContext 来掌握针对 Context 的处理技巧。

为了掌握 Context 处理技巧,我们必须有一种共识,即每个新的请求都是由一个独立的线程进行处理,诸如 Tomcat 之类的服务器会为我们启动了这个线程。也就是说,HTTP 请求的所有参数总是绑定在处理请求的线程中。基于这一点,我们不难想象,对 RequestContext 的访问必须设计成线程安全,Zuul 使用了非常常见和实用的 ThreadLocal,如下所示:

java
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
        @Override
        protected RequestContext initialValue() {
            try {
                return contextClass.newInstance();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }
};

可以看到,这里使用了线程安全的 ThreadLocal 来存放所有的 RequestContext。作为一个实现上的技巧,请注意,这里重写了 ThreadLocal 的 initialValue() 方法。重写目的是确保ThreadLocal 的 get() 方法始终会获取一个 RequestContext 实例。这样做的原因是因为默认情况下 ThreadLocal 的 get() 方法可能会返回 null,而通过重写 initialValue() 方法可以在返回 null 时自动初始化一个 RequestContext 实例。

用于获取 RequestContext 的 getCurrentContext 方法如下所示:

java
public static RequestContext getCurrentContext() {
	    RequestContext context = threadLocal.get();
        return context;
}

我们在开发业务代码时可以先获取这个 RequestContext 对象,进而获取该对象中所存储的各种业务数据。RequestContext 为我们提供了大量的 getter/setter 方法对来完成这一操作。与 Zuul 中的其他组件一样,通过分析代码,我们发现 RequestContext 的实现也是直接对 ConcurrentHashMap 做了一层封装,其中 Key 是 String 类型,而 Value 是 Object 类型,因此我们可以将任何想要传递的数据放到 RequestContext 中。

HTTP 请求与过滤器执行

接下去我们分析如何使用加载好的 ZuulFilter。Zuul 提供了 FilterProcessor 类来执行 Filter,参考如下所示的 runFilters 方法:

java
public Object runFilters(String sType) throws Throwable {

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

注意这个方法传入了 Filter 的类型,然后通过 FilterLoader 获取所属类型的 ZuulFilter 列表。通过遍历列表并调用 processZuulFilter 方法来完成对每个 Filter 的执行。processZuulFilter 方法的结构也比较简单,核心就是通过调用 ZuulFilter 自身的 runFilter 方法,然后获取执行结果状态。

我们同时看到 FilterProcessor 基于 runFilters 方法衍生出 postRoute()、preRoute()、error() 和 route() 这四个方法,分别对应 Zuul 中的四种过滤器类型。其中 PRE 过滤器在请求到达目标服务器之前调用,ROUTING 过滤器把请求发送给目标服务,POST 过滤器在请求从目标服务返回之后执行,而 ERROR 过滤器则在发生错误时执行。同时,这四种过滤器有一定的执行顺序,如下所示:

Zuul 过滤器生命周期(翻译自 Zuul 官网)

在上图中,可以看到每一种过滤器对应了一个 HTTP 请求的执行生命周期。因此,我们可以在不同的过滤器中添加针对该生命周期的特定执行逻辑。例如,在日常开发过程中,常见的做法是在 PRE 过滤器中执行身份验证和记录请求日志等操作;而在 POST 过滤器返回的响应中添加消息头或者各种统计信息等。

基于对 Zuul 请求生命周期的理解,我们最后来看一下 ZuulServlet。在 Spring Cloud 中所有请求都是 HTTP 请求,Zuul 作为一个服务网关同样也需要完成对 HTTP 请求的响应。ZuulServlet 是 Zuul 中对 HttpServlet 接口的一个实现类。我们知道 Servlet 中最核心的就是 service 方法,ZuulServlet 的 service 方法如下所示:

java
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
 
            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();
        }
}

注意这里的 postRoute()、preRoute()、error() 和 route() 这四个方法同样只是对 ZuulRunner 中同名方法的直接封装。你可以尝试通过这段代码梳理 Zuul 中四种过滤器的执行顺序。

Spring Cloud 集成 ZuulFilter

针对 Zuul 的整体架构,在设计上的初衷是把所有 ZuulFilter 通过 Groovy 文件的形式进行管理和加载。但是,很遗憾,在 Spring Cloud 体系中并没有使用此策略来加载 ZuulFilter。那么 Spring Cloud 的 ZuulFilter是如何实现加载的呢?

spring-cloud-netflix-zuul 同样是一个 Spring Boot 应用程序,所以我们还是直接进入 ZuulServerAutoConfiguration 类,并找到了如下所示的 ZuulFilterConfiguration 类:

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

这里我们看到了熟悉的 FilterLoader 和 FilterRegistry,通过 ZuulFilterInitializer 的构造函数引入了过滤器加载的流程。同时,我们发现 filters 变量是一个 Key 为 String、Value 为 ZuulFilter 的 Map,而该变量上添加了 @Autowired 注解。这就意味着 Spring 会把 ZuulFilter 的 bean 自动装载到 Map 对象中。

我们在 ZuulServerAutoConfiguration 以及它的子类 ZuulProxyAutoConfiguration 中发现了很多添加了 @Bean 注解并以"Filter"结尾的类,其形式如下所示:

java
@Bean
public ServletDetectionFilter servletDetectionFilter() {
        return new ServletDetectionFilter();
}

这个 ServletDetectionFilter 类就是 ZuulFilter 的子类,在 Spring 容器中会被自动注入ZuulFilterConfiguration 的 filters 对象中。

获取了容器中自动注入的 ZuulFilter 之后,我们继续跟进 ZuulFilterInitializer,发现了如下所示的 contextInitialized 方法:

java
@PostConstruct
public void contextInitialized() {
        log.info("Starting filter initializer");
 
        TracerFactory.initialize(tracerFactory);
        CounterFactory.initialize(counterFactory);
 
        for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
            filterRegistry.put(entry.getKey(), entry.getValue());
        }
}

注意到该方法开启了 @PostConstruct 注解,因此在 Spring 容器中,一旦 ZuulFilterInitializer 的构造函数执行完成,就会执行这个 contextInitialized 方法。而在该方法中,我们通过 FilterRegistry 的 put 方法把从容器中自动注入的各个 Filter 添加到 FilterRegistry 中。同样,在 ZuulFilterInitializer 也存在一个被添加了 @PostConstruct 注解的 contextDestroyed 方法,该方法中调用了 filterRegistry 的 remove 方法删除 Filter。

至此,Spring Cloud 通过 Netflix Zuul 提供的 FilterLoader 和 FilterRegistry 等工具类完成了两者之间的集成。

小结与预告

在上一课时的基础上,本课时详细解析了 Zuul 网关的内部构造和实现原理。在 Zuul 中,最核心的组件就是过滤器组件 ZuulFilter,我们围绕 ZuulFilter 的定义、加载、存储和管理给出源码级的分析过程。同时,我们也基于一个 HTTP 请求的处理过程,描绘了 Zuul 中的过滤器类型以及处理机制。

这里给你留一道思考题:在 Zuul 中,处理请求上下文时使用了哪种实现方式类确保线程安全性?

在介绍完 Zuul 之后,下一课时我们将介绍另一款主流的 API 网关,这就是 Spring Cloud 中自研的 Spring Cloud Gateway。