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.packages
和zuul.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容器託管的關係,有些箭頭不是嚴格的表示調用的先後關係,對此大家意會就好了。