前提
最近在项目中使用了SpringCloud,基于Zuul搭建了一个提供加解密、鉴权等功能的网关服务。鉴于之前没怎么使用过Zuul,于是顺便仔细阅读了它的源码。实际上,Zuul原来提供的功能是很单一的:通过一个统一的Servlet入口(ZuulServlet
,或者Filter
入口,使用ZuulServletFilter
)拦截所有的请求,然后通过内建的com.netflix.zuul.IZuulFilter
链对请求做拦截和过滤处理。ZuulFilter
和javax.servlet.Filter
的原理相似,但是它们本质并不相同。javax.servlet.Filter
在Web应用中是独立的组件,ZuulFilter
是ZuulServlet
处理请求时候调用的,后面会详细分析。
源码环境准备
Zuul的项目地址是https://github.com/Netflix/zuul,它是著名的”开源框架提供商”Netflix的作品,项目的目的是:Zuul是一个网关服务,提供动态路由、监视、弹性、安全性等。在SpringCloud中引入了zuul,配合Netflix的另一个负载均衡框架Ribbon和Netflix的另一个提供服务发现与注册框架Eureka,可以实现服务的动态路由。值得注意的是,zuul在2.x甚至3.x的分支中已经引入了netty,框架的复杂性大大提高。但是当前的SpringCloud体系并没有升级zuul的版本,目前使用的是zuul1.x的最高版本1.3.1。
因此我们需要阅读它的源码的时候可以选择这个发布版本。值得注意的是,由于这些版本的发布时间已经比较久,有部分插件或者依赖包可能找不到,笔者在构建zuul1.3.1的源码的时候发现这几个问题:
- 1、
nebula.netflixoss
插件的旧版本已经不再支持,所有build.gradle文件中的nebula.netflixoss
插件的版本修改为5.2.0。
- 2、2017年的时候Gradle支持的版本是2.x,笔者这里选择了gradle-2.14,选择高版本的Gradle有可能在构建项目的时候出现
jetty
插件不支持。
- 3、Jdk最好使用1.8,Gradle构建文件中的sourceCompatibility、targetCompatibility、languageLevel等配置全改为1.8。
另外,如果使用IDEA进行构建,注意配置项目的Jdk和Java环境,所有配置改为Jdk1.8,Gradle构建成功后如下:
zuul-1.3.1中提供了一个Web应用的Sample项目,我们直接运行zuul-simple-webapp的Gradle配置中的Tomcat插件即可启动项目,开始Debug之旅:
源码分析
ZuulFilter的加载
从Zuul的源码来看,ZuulFilter
的加载模式可能跟我们想象的大有不同,Zuul设计的初衷是ZuulFilter
是存放在Groovy文件中,可以实现基于最后修改时间进行热加载。我们先看看Zuul核心类之一com.netflix.zuul.filters.FilterRegistry
(Filter的注册中心,实际上是ZuulFilter的全局缓存):
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(); }
}
|
实际上Zuul使用了简单粗暴的方式(直接使用ConcurrentHashMap
)缓存了ZuulFilter
,这些缓存除非主动调用remove
方法,否则不会自动清理。Zuul提供默认的动态代码编译器,接口是DynamicCodeCompiler
,目的是把代码编译为Java的类,默认实现是GroovyCompiler
,功能就是把Groovy代码编译为Java类。还有一个比较重要的工厂类接口是FilterFactory
,它定义了ZuulFilter
类生成ZuulFilter
实例的逻辑,默认实现是DefaultFilterFactory
,实际上就是利用Class#newInstance()
反射生成ZuulFilter
实例。接着,我们可以进行分析FilterLoader
的源码,这个类的作用就是加载文件中的ZuulFilter
实例:
public class FilterLoader { final static FilterLoader INSTANCE = new FilterLoader();
private static final Logger LOG = LoggerFactory.getLogger(FilterLoader.class);
private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>(); private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>(); private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>(); private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
private FilterRegistry filterRegistry = FilterRegistry.instance(); static DynamicCodeCompiler COMPILER; static FilterFactory FILTER_FACTORY = new DefaultFilterFactory(); public void setCompiler(DynamicCodeCompiler compiler) { COMPILER = compiler; }
public void setFilterRegistry(FilterRegistry r) { this.filterRegistry = r; }
public void setFilterFactory(FilterFactory factory) { FILTER_FACTORY = factory; } public static FilterLoader getInstance() { return INSTANCE; } public int filterInstanceMapSize() { return filterRegistry.size(); } public ZuulFilter getFilter(String sCode, String sName) throws Exception { if (filterCheck.get(sName) == null) { filterCheck.putIfAbsent(sName, sName); if (!sCode.equals(filterClassCode.get(sName))) { LOG.info("reloading code " + sName); filterRegistry.remove(sName); } } ZuulFilter filter = filterRegistry.get(sName); if (filter == null) { Class clazz = COMPILER.compile(sCode, sName); if (!Modifier.isAbstract(clazz.getModifiers())) { filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz); } } return filter; }
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; } public List<ZuulFilter> getFiltersByType(String filterType) { List<ZuulFilter> list = hashFiltersByType.get(filterType); if (list != null) return list; list = new ArrayList<ZuulFilter>(); Collection<ZuulFilter> filters = filterRegistry.getAllFilters(); for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) { ZuulFilter filter = iterator.next(); if (filter.filterType().equals(filterType)) { list.add(filter); } } Collections.sort(list); hashFiltersByType.putIfAbsent(filterType, list); return list; } }
|
上面的几个方法和缓存容器都比较简单,这里实际上有加载和存放动作的方法只有putFilter
,这个方法正是Filter
文件管理器FilterFileManager
依赖的,接着看FilterFileManager
的源码:
public class FilterFileManager {
private static final Logger LOG = LoggerFactory.getLogger(FilterFileManager.class);
String[] aDirectories; int pollingIntervalSeconds; Thread poller; boolean bRunning = true; static FilenameFilter FILENAME_FILTER;
static FilterFileManager INSTANCE;
private FilterFileManager() { }
public static void setFilenameFilter(FilenameFilter filter) { FILENAME_FILTER = filter; } public static void init(int pollingIntervalSeconds, String... directories) throws Exception, IllegalAccessException, InstantiationException{ if (INSTANCE == null) INSTANCE = new FilterFileManager(); INSTANCE.aDirectories = directories; INSTANCE.pollingIntervalSeconds = pollingIntervalSeconds; INSTANCE.manageFiles(); INSTANCE.startPoller(); }
public static FilterFileManager getInstance() { return INSTANCE; }
public static void shutdown() { INSTANCE.stopPoller(); }
void stopPoller() { bRunning = false; } 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(); } public File getDirectory(String sPath) { File directory = new File(sPath); if (!directory.isDirectory()) { URL resource = FilterFileManager.class.getClassLoader().getResource(sPath); try { directory = new File(resource.toURI()); } catch (Exception e) { LOG.error("Error accessing directory in classloader. path=" + sPath, e); } if (!directory.isDirectory()) { throw new RuntimeException(directory.getAbsolutePath() + " is not a valid directory"); } } return directory; } List<File> getFiles() { List<File> list = new ArrayList<File>(); for (String sDirectory : aDirectories) { if (sDirectory != null) { File directory = getDirectory(sDirectory); File[] aFiles = directory.listFiles(FILENAME_FILTER); if (aFiles != null) { list.addAll(Arrays.asList(aFiles)); } } } return list; } void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException { for (File file : aFiles) { FilterLoader.getInstance().putFilter(file); } } void manageFiles() throws Exception, IllegalAccessException, InstantiationException { List<File> aFiles = getFiles(); processGroovyFiles(aFiles); }
|
分析完FilterFileManager
源码之后,Zuul中基于文件加载ZuulFilter的逻辑已经十分清晰:后台启动一个守护线程,定时轮询指定文件夹里面的文件,如果文件存在变更,则尝试更新指定的ZuulFilter缓存,FilterFileManager的init
方法调用的时候在启动后台线程之前会进行一次预加载。
RequestContext
在分析ZuulFilter
的使用之前,有必要先了解Zuul中的请求上下文对象RequestContext
。首先要有一个共识:每一个新的请求都是由一个独立的线程处理(这个线程是Tomcat里面起的线程),换言之,请求的所有参数(Http报文信息解析出来的内容,如请求头、请求体等等)总是绑定在处理请求的线程中。RequestContext
的设计就是简单直接有效,它继承于ConcurrentHashMap<String, Object>
,所以参数可以直接设置在RequestContext
中,Zuul没有设计一个类似于枚举的类控制RequestContext
的可选参数,因此里面的设置值和提取值的方法都是硬编码的,例如:
public HttpServletRequest getRequest() { return (HttpServletRequest) get("request"); }
public void setRequest(HttpServletRequest request) { put("request", request); }
public HttpServletResponse getResponse() { return (HttpServletResponse) get("response"); }
public void setResponse(HttpServletResponse response) { set("response", response); } ...
|
看起来很暴力并且不怎么优雅,但是实际上是高效的。RequestContext一般使用静态方法RequestContext#getCurrentContext()
进行初始化,我们分析一下它的初始化流程:
protected static Class<? extends RequestContext> contextClass = RequestContext.class;
private static RequestContext testContext = null;
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); } } };
public RequestContext() { super(); }
public static RequestContext getCurrentContext() { if (testContext != null) return testContext; RequestContext context = threadLocal.get(); return context; }
|
注意上面的ThreadLocal
覆盖了初始化方法initialValue()
,ThreadLocal
的初始化方法总是在ThreadLocal#get()
方法返回null的时候调用,实际上静态方法RequestContext#getCurrentContext()
的作用就是:如果ThreadLocal
中已经绑定了RequestContext
静态实例就直接获取绑定在线程中的RequestContext
实例,否则新建一个RequestContext
实例存放在ThreadLocal
(绑定到当前的请求线程中)。了解这一点后面分析ZuulServletFilter和ZuulServlet
的时候就很简单了。
ZuulFilter
抽象类com.netflix.zuul.ZuulFilter
是Zuul里面的核心组件,它是用户扩展Zuul行为的组件,用户可以实现不同类型的ZuulFilter
、定义它们的执行顺序、实现它们的执行方法达到定制化的目的,SpringCloud的netflix-zuul
就是一个很好的实现包。ZuulFilter
实现了IZuulFilter
接口,我们先看这个接口的定义:
public interface IZuulFilter { boolean shouldFilter();
Object run() throws ZuulException; }
|
很简单,shouldFilter()
方法决定是否需要执行(也就是执行时机由使用者扩展,甚至可以禁用),而run()
方法决定执行的逻辑。接着看ZuulFilter
的源码:
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> { private final AtomicReference<DynamicBooleanProperty> filterDisabledRef = new AtomicReference<>(); abstract public String filterType();
abstract public int filterOrder(); public boolean isStaticFilter() { return true; }
public String disablePropertyName() { return "zuul." + this.getClass().getSimpleName() + "." + filterType() + ".disable"; }
public boolean isFilterDisabled() { filterDisabledRef.compareAndSet(null, DynamicPropertyFactory.getInstance().getBooleanProperty(disablePropertyName(), false)); return filterDisabledRef.get().get(); }
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; } public int compareTo(ZuulFilter filter) { return Integer.compare(this.filterOrder(), filter.filterOrder()); } }
|
这里注意几个地方,第一个是filterOrder()
方法和compareTo(ZuulFilter filter)
方法,子类实现ZuulFilter
时候,filterOrder()
方法返回值越大,或者说Filter的顺序系数越大,ZuulFilter
执行的优先度越低。第二个地方是可以通过zuul.${全类名}.${filterType}.disable=false通过类名和Filter
类型禁用对应的Filter
。第三个值得注意的地方是Zuul中定义了四种类型的ZuulFilter
,后面分析ZuulRunner
的时候再详细展开。ZuulFilter
实际上就是使用者扩展的核心组件,通过实现ZuulFilter
的方法可以在一个请求处理链中的特定位置执行特定的定制化逻辑。第四个值得注意的地方是runFilter()
方法执行不会抛出异常,如果出现异常,Throwable
实例会保存在ZuulFilterResult
对象中返回到外层方法,如果正常执行,则直接返回runFilter()
方法的结果。
FilterProcessor
前面花大量功夫分析完ZuulFilter
基于Groovy文件的加载机制(在SpringCloud体系中并没有使用此策略,因此,我们持了解的态度即可)以及RequestContext
的设计,接着我们分析FilterProcessor
去了解如何使用加载好的缓存中的ZuulFilter
。我们先看FilterProcessor
的基本属性:
public class FilterProcessor {
static FilterProcessor INSTANCE = new FilterProcessor(); protected static final Logger logger = LoggerFactory.getLogger(FilterProcessor.class);
private FilterUsageNotifier usageNotifier;
public FilterProcessor() { usageNotifier = new BasicFilterUsageNotifier(); }
public static FilterProcessor getInstance() { return INSTANCE; }
public static void setProcessor(FilterProcessor processor) { INSTANCE = processor; }
public void setFilterUsageNotifier(FilterUsageNotifier notifier) { this.usageNotifier = notifier; } ... }
|
像之前分析的几个类一样,FilterProcessor
设计为单例,提供可以覆盖单例实例的方法。需要注意的一点是属性usageNotifier是FilterUsageNotifier
类型,FilterUsageNotifier
接口的默认实现是BasicFilterUsageNotifier
(FilterProcessor
的一个静态内部类),BasicFilterUsageNotifier
依赖于Netflix的一个工具包servo-core
,提供基于内存态的计数器统计每种ZuulFilter的每一次调用的状态ExecutionStatus
。枚举ExecutionStatus的可选值如下:
- 1、SUCCESS,代表该Filter处理成功,值为1。
- 2、SKIPPED,代表该Filter跳过处理,值为-1。
- 3、DISABLED,代表该Filter禁用,值为-2。
- 4、SUCCESS,代表该FAILED处理出现异常,值为-3。
当然,使用者也可以覆盖usageNotifier属性。接着我们看FilterProcessor
中真正调用ZuulFilter
实例的核心方法:
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; }
public Object processZuulFilter(ZuulFilter filter) throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); boolean bDebug = ctx.debugRouting(); final String metricPrefix = "zuul.filter-"; long execTime = 0; String filterName = ""; try { long ltime = System.currentTimeMillis(); filterName = filter.getClass().getSimpleName(); RequestContext copy = null; Object o = null; Throwable t = null; if (bDebug) { Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName); copy = ctx.copy(); } ZuulFilterResult result = filter.runFilter(); ExecutionStatus s = result.getStatus(); execTime = System.currentTimeMillis() - ltime; switch (s) { case FAILED: t = result.getException(); ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); break; case SUCCESS: o = result.getResult(); ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime); if (bDebug) { Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms"); Debug.compareContextState(filterName, copy); } break; default: break; } if (t != null) throw t; usageNotifier.notify(filter, s); return o;
} catch (Throwable e) { if (bDebug) { Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage()); } usageNotifier.notify(filter, ExecutionStatus.FAILED); if (e instanceof ZuulException) { throw (ZuulException) e; } else { ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName); ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); throw ex; } } }
|
上面介绍了FilterProcessor
中的processZuulFilter(ZuulFilter filter)
方法主要提供ZuulFilter
执行的一些度量相关记录(例如Filter
执行耗时摘要,会形成一个链,记录在一个字符串中)和ZuulFilter
的执行方法,ZuulFilter执行结果可能是成功或者异常,前面提到过,如果抛出异常Throwable
实例会保存在ZuulFilterResult
中,在processZuulFilter(ZuulFilter filter)
发现ZuulFilterResult
中的Throwable
实例不为null则直接抛出,否则返回ZuulFilter
正常执行的结果。另外,FilterProcessor
中通过指定Filter
类型执行所有对应类型的ZuulFilter
的runFilters(String sType)
方法,我们知道了runFilters(String sType)
方法如果处理结果是Boolean类型尝试做或操作,其他类型结果忽略,可以理解为此方法的返回值是没有很大意义的。参考SpringCloud里面对ZuulFilter
的返回值处理一般是直接塞进去当前线程绑定的RequestContext
中,选择特定的ZuulFilter
子类对前面的ZuulFilter
产生的结果进行处理。FilterProcessor
基于runFilters(String sType)
方法提供了其他指定filterType的方法:
public void postRoute() throws ZuulException { try { runFilters("post"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName()); } }
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()); } }
public void error() { try { runFilters("error"); } catch (Throwable e) { logger.error(e.getMessage(), e); } }
public void route() throws ZuulException { try { runFilters("route"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName()); } }
|
上面提供的方法很简单,无法是指定参数为post、pre、error、route对runFilters(String sType)
方法进行调用,至于这些FilterType的执行位置见下一个小节的分析。
ZuulServletFilter和ZuulServlet
Zuul本来就是设计为Servlet
规范组件的一个类库,ZuulServlet
就是javax.servlet.http.HttpServlet
的实现类,而ZuulServletFilter
是javax.servlet.Filter
的实现类。这两个类都依赖到ZuulRunner
完成ZuulFilter
的调用,它们的实现逻辑是完全一致的,我们只需要看其中一个类的实现,这里挑选ZuulServlet
:
public class ZuulServlet extends HttpServlet {
private static final long serialVersionUID = -3374242278843351500L; private ZuulRunner zuulRunner;
@Override public void init(ServletConfig config) throws ServletException { super.init(config); String bufferReqsStr = config.getInitParameter("buffer-requests"); boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false; zuulRunner = new ZuulRunner(bufferReqs); }
@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(); } }
void postRoute() throws ZuulException { zuulRunner.postRoute(); }
void route() throws ZuulException { zuulRunner.route(); }
void preRoute() throws ZuulException { zuulRunner.preRoute(); }
void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { zuulRunner.init(servletRequest, servletResponse); } void error(ZuulException e) { RequestContext.getCurrentContext().setThrowable(e); zuulRunner.error(); } }
|
ZuulServletFilter
和ZuulServlet
不相同的地方仅仅是初始化和处理方法的方法签名(参数列表和方法名),其他逻辑甚至是代码是一模一样,使用过程中我们需要了解javax.servlet.http.HttpServlet
和javax.servlet.Filter
的作用去选择到底使用ZuulServletFilter
还是ZuulServlet
。上面的代码可以看到,ZuulServlet
初始化的时候可以配置初始化布尔值参数buffer-requests,这个参数默认为false,它是ZuulRunner
实例化的必须参数。ZuulServlet
中的调用ZuulFilter
的方法都委托到ZuulRunner
实例去完成,但是我们可以从service(servletRequest, servletResponse)
方法看出四种FilterType(pre、route、post、error)的ZuulFilter
的执行顺序,总结如下:
- 1、pre、route、post都不抛出异常,顺序是:pre->route->post,error不执行。
- 2、pre抛出异常,顺序是:pre->error->post。
- 3、route抛出异常,顺序是:pre->route->error->post。
- 4、post抛出异常,顺序是:pre->route->post->error。
注意,一旦出现了异常,会把抛出的Throwable实例设置到绑定到当前请求线程的RequestContext
实例中的throwable属性。还需要注意在service(servletRequest, servletResponse)
的finally块中调用了RequestContext.getCurrentContext().unset();
,实际上是从RequestContext
的ThreadLocal
实例中移除当前的RequestContext
实例,这样做可以避免ThreadLocal
使用不当导致内存泄漏。
接着看ZuulRunner
的源码:
public class ZuulRunner {
private boolean bufferRequests;
public ZuulRunner() { this.bufferRequests = true; }
public ZuulRunner(boolean bufferRequests) { this.bufferRequests = bufferRequests; }
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 postRoute() throws ZuulException { FilterProcessor.getInstance().postRoute(); }
public void route() throws ZuulException { FilterProcessor.getInstance().route(); }
public void preRoute() throws ZuulException { FilterProcessor.getInstance().preRoute(); }
public void error() { FilterProcessor.getInstance().error(); } }
|
postRoute()
、route()
、preRoute()
、error()
都是直接委托到FilterProcessor
中完成的,实际上就是执行对应类型的所有ZuulFilter
实例。这里需要注意的是,初始化ZuulRunner
时候,HttpServletResponse
会被包装为com.netflix.zuul.http.HttpServletResponseWrapper
实例,它是Zuul实现的javax.servlet.http.HttpServletResponseWrapper
的子类,主要是添加了一个属性status用来记录Http状态码。如果初始化参数bufferRequests为true,HttpServletRequest
会被包装为com.netflix.zuul.http.HttpServletRequestWrapper
,它是Zuul实现的javax.servlet.http.HttpServletRequestWrapper
的子类,这个包装类主要是把请求的表单参数和请求体都缓存在实例属性中,这样在一些特定场景中可以提高性能。如果没有特殊需要,这个参数bufferRequests一般设置为false。
Zuul简单的使用例子
我们做一个很简单的例子,场景是:对于每个POST请求,使用pre类型的ZuulFilter
打印它的请求体,然后使用post类型的ZuulFilter
,响应结果硬编码为字符串”Hello World!”。我们先为CounterFactory
、`TracerFactory添加两个空的子类,因为Zuul处理逻辑中依赖到这两个组件实现数据度量:
public class DefaultTracerFactory extends TracerFactory {
@Override public Tracer startMicroTracer(String name) { return null; } }
public class DefaultCounterFactory extends CounterFactory {
@Override public void increment(String name) {
} }
|
接着我们分别继承ZuulFilter,实现一个pre类型的用于打印请求参数的Filter,命名为PrintParameterZuulFilter
,实现一个post类型的用于返回字符串”Hello World!”的Filter,命名为SendResponseZuulFilter
:
public class PrintParameterZuulFilter extends ZuulFilter {
@Override public String filterType() { return "pre"; }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); return "POST".equalsIgnoreCase(request.getMethod()); }
@Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); if (null != request.getContentType()) { if (request.getContentType().contains("application/json")) { try { ServletInputStream inputStream = request.getInputStream(); String result = StreamUtils.copyToString(inputStream, Charset.forName("UTF-8")); System.out.println(String.format("请求URI为:%s,请求参数为:%s", request.getRequestURI(), result)); } catch (IOException e) { throw new ZuulException(e, 500, "从输入流中读取请求参数异常"); } } else if (request.getContentType().contains("application/x-www-form-urlencoded")) { StringBuilder params = new StringBuilder(); Enumeration<String> parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { String name = parameterNames.nextElement(); params.append(name).append("=").append(request.getParameter(name)).append("&"); } String result = params.toString(); System.out.println(String.format("请求URI为:%s,请求参数为:%s", request.getRequestURI(), result.substring(0, result.lastIndexOf("&")))); } } return null; } }
public class SendResponseZuulFilter extends ZuulFilter {
@Override public String filterType() { return "post"; }
@Override public int filterOrder() { return 0; }
@Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); return "POST".equalsIgnoreCase(request.getMethod()); }
@Override public Object run() throws ZuulException { RequestContext context = RequestContext.getCurrentContext(); String output = "Hello World!"; try { context.getResponse().getWriter().write(output); } catch (IOException e) { throw new ZuulException(e, 500, e.getMessage()); } return true; } }
|
接着,我们引入嵌入式Tomcat,简单地创建一个Servlet容器,Maven依赖为:
<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>8.5.34</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>8.5.34</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>8.5.34</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper-el</artifactId> <version>8.5.34</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jsp-api</artifactId> <version>8.5.34</version> </dependency>
|
添加带main方法的类把上面的组件和Tomcat的组件组装起来:
public class ZuulMain {
private static final String WEBAPP_DIRECTORY = "src/main/webapp/"; private static final String ROOT_CONTEXT = "";
public static void main(String[] args) throws Exception { Tomcat tomcat = new Tomcat(); File tempDir = File.createTempFile("tomcat" + ".", ".8080"); tempDir.delete(); tempDir.mkdir(); tempDir.deleteOnExit(); tomcat.setBaseDir(tempDir.getAbsolutePath()); tomcat.setPort(8080); StandardContext ctx = (StandardContext) tomcat.addWebapp(ROOT_CONTEXT, new File(WEBAPP_DIRECTORY).getAbsolutePath()); WebResourceRoot resources = new StandardRoot(ctx); resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/")); ctx.setResources(resources); ctx.setDefaultWebXml(new File("src/main/webapp/WEB-INF/web.xml").getAbsolutePath()); for (LifecycleListener ll : ctx.findLifecycleListeners()) { if (ll instanceof ContextConfig) { ((ContextConfig) ll).setDefaultWebXml(ctx.getDefaultWebXml()); } } CounterFactory.initialize(new DefaultCounterFactory()); TracerFactory.initialize(new DefaultTracerFactory()); FilterRegistry.instance().put("printParameterZuulFilter", new PrintParameterZuulFilter()); FilterRegistry.instance().put("sendResponseZuulFilter", new SendResponseZuulFilter()); Context context = tomcat.addContext("/zuul", null); Tomcat.addServlet(context, "zuul", new ZuulServlet()); context.addServletMappingDecoded("/*", "zuul"); tomcat.start(); tomcat.getServer().await(); } }
|
执行main方法,Tomcat正常启动后打印出熟悉的日志如下:
接下来,用POSTMAN请求模拟一下请求:
小结
Zuul虽然在它的Github仓库中的简介中说它是一个提供动态路由、监视、弹性、安全性等的网关框架,但是实际上它原生并没有提供这些功能,这些功能是需要使用者扩展ZuulFilter实现的,例如基于负载均衡的动态路由需要配置Netflix自己家的Ribbon实现。Zuul在设计上的扩展性什么良好,ZuulFilter就像插件一个可以通过类型、排序系数构建一个调用链,通过Filter或者Servlet做入口,嵌入到Servlet(Web)应用中。不过,在Zuul后续的版本如2.x和3.x中,引入了Netty,基于TCP做底层的扩展,但是编码和使用的复杂度大大提高。也许这就是SpringCloud在netflix-zuul
组件中选用了zuul1.x的最后一个发布版本1.3.1的原因吧。springcloud-netflix
中使用到Netflix的zuul(动态路由)、robbin(负载均衡)、eureka(服务注册与发现)、hystrix(熔断)等核心组件,这里立个flag先逐个组件分析其源码,逐个击破后再对springcloud-netflix
做一次完整的源码分析。
(本文完 c-5-d r-a-20190310 最近996,不能经常更新,顺便祝自己生日快乐…)