Zuul 2是如何動態加載Filter的?

Zuul 2沿用了Zuul 1的責任鏈模式的設計,其網關核心功能還是通過Filter鏈來實現的。要熟練使用和擴展Zuul 2的功能,必須要瞭解其Filter的加載和執行機制。另外,Zuul 2使用Guice作爲依賴注入工具,因此在開始分析之前,我們需要大致瞭解Guice的基本原理和用法,傳送門:Guide to Google Guice

爲了瞭解Zuul 2的filter加載機制,我們從入口開始看起。在官方提供的zuul-sample項目中,啓動類是Bootstrap,在其start方法中,可以看到這麼幾行:

ConfigurationManager.loadCascadedPropertiesFromResources("application");
Injector injector = InjectorBuilder.fromModule(new ZuulSampleModule()).createInjector();
BaseServerStartup serverStartup = injector.getInstance(BaseServerStartup.class);
server = serverStartup.server();

在上述代碼中使用了ConfigurationManager去加載application.properties配置文件,然後通過Guice創建ZuulSampleModule,接着創建BaseServerStartup和Server實例。
在ZuulSampleModule的configure方法中,跟filter加載相關的是如下兩行:

install(new ZuulFiltersModule());
bind(FilterFileManager.class).asEagerSingleton();

在ZuulFiltersModule的configure方法中,主要做的事情是指定了GroovyCompiler、GuiceFilterFactory和BasicFilterUsageNotifier的Guice注入綁定。而provideFilterFileManagerConfig方法,則是根據配置文件中zuul.filters.locations屬性按照路徑加載filterLocations,以及根據zuul.filters.packageszuul.filters.classes屬性按照類名加載filterClassNames,然後據此創建FilterFileManagerConfig對象並返回。

FilterFileManagerConfig對象的屬性如下:

private String[] directories; // filter文件夾路徑
private String[] classNames; // filter類名
private int pollingIntervalSeconds; // 掃描時間間隔,秒數
private FilenameFilter filenameFilter; // 文件名過濾器

FilterFileManager會根據FilterFileManagerConfig指定的配置,使用FilterLoader執行filter文件加載,同樣地這兩個實例也是通過Guice注入到FilterFileManager的構造器中。
FilterLoader的類圖如下:
在這裏插入圖片描述

FilterLoader其主要功能是按照類名、文件名、腳本內容等方式使用DynamicCodeCompiler去加載爲ZuulFilter實例,爲了只加載新增或者變更了的filter文件,其內部使用了一些ConcurrentMap記住已經加載過的filter及其上次修改時間戳。FilterRegistry是filter註冊表,其類圖如下:
在這裏插入圖片描述

在FilterFileManager的構造器中,除了設置相關屬性以外,還啓動了名爲processFilesService的固定大小線程池,主要是爲了能夠異步地使用filterLoader去動態加載filter文件。

在FilterFileManager的@PostConstruct方法init中,主要做了三件事情:

  • 加載config中classNames屬性直接指定的filter;
  • 加載config中directories屬性指定的路徑下的filter;
  • 啓動一個名爲poller的Thread去定時檢測config中directories屬性指定的路徑下的filter文件是否有更新;
    其代碼如下:
    /**
     * Initialized the GroovyFileManager.
     *
     * @throws Exception
     */
    @PostConstruct
    public void init() throws Exception
    {
        long startTime = System.currentTimeMillis();
        
        filterLoader.putFiltersForClasses(config.getClassNames());
        manageFiles();
        startPoller();
        
        LOG.warn("Finished loading all zuul filters. Duration = " + (System.currentTimeMillis() - startTime) + " ms.");
    }

在FilterFileManager的@PreDestroy方法shutdown中,主要是負責關閉poller Thread。

FilterFileManager啓動關閉的流程如下圖示:
在這裏插入圖片描述

根據類名加載filter的代碼在FilterLoader.putFiltersForClasses()方法中:

    /**
     * Load and cache filters by className
     *
     * @param classNames The class names to load
     * @return List of the loaded filters
     * @throws Exception If any specified filter fails to load, this will abort. This is a safety mechanism so we can
     * prevent running in a partially loaded state.
     */
    public List<ZuulFilter> putFiltersForClasses(String[] classNames) throws Exception
    {
        List<ZuulFilter> newFilters = new ArrayList<>();
        for (String className : classNames)
        {
            newFilters.add(putFilterForClassName(className));
        }
        return newFilters;
    }

    public ZuulFilter putFilterForClassName(String className) throws Exception
    {
        Class clazz = Class.forName(className);
        if (! ZuulFilter.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Specified filter class does not implement ZuulFilter interface!");
        }
        else {
            ZuulFilter filter = filterFactory.newInstance(clazz);
            putFilter(className, filter, System.currentTimeMillis());
            return filter;
        }
    }

實現動態加載變化的filter功能是在filterLoader.putFilter(file)中實現的,其代碼如下:

/**
 * From a file this will read the ZuulFilter source code, compile it, and add it to the list of current filters
 * a true response means that it was successful.
 *
 * @param file
 * @return true if the filter in file successfully read, compiled, verified and added to Zuul
 * @throws IllegalAccessException
 * @throws InstantiationException
 * @throws IOException
 */
public boolean putFilter(File file) throws Exception
{
    try {
        String sName = file.getAbsolutePath();
        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 = filterFactory.newInstance(clazz);
                putFilter(sName, filter, file.lastModified());
                return true;
            }
        }
    }
    catch (Exception e) {
        LOG.error("Error loading filter! Continuing. file=" + String.valueOf(file), e);
        return false;
    }

    return false;
}

這段代碼主要是通過比較傳入的File的lastModified時間戳和map緩存中同名文件的時間戳來判定文件是否有變更,使用compiler去編譯filter文件爲Class對象,然後使用filterFactory去創建類實例。

至此,Zuul 2動態加載filter的機制已經介紹完畢,後續如果有時間會繼續補充Zuul 2其他相關技術主題的分析文章。以兩張UML圖作爲本篇文章的結語吧:
在這裏插入圖片描述

ZuulFilter相關的類後面的專題文章會具體展開介紹。

在這裏插入圖片描述

縱向的箭頭表示F6,即step over;橫向的箭頭表示F5,即step in。需要注意的是,由於Guice容器託管的關係,有些箭頭不是嚴格的表示調用的先後關係,對此大家意會就好了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章