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中,主要做了三件事情:
/**
* 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。
根据类名加载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容器托管的关系,有些箭头不是严格的表示调用的先后关系,对此大家意会就好了。