zuul源码分析-探究原生zuul的工作原理
前提
最近在项目中使用了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 {
// 饿汉式单例,确保全局只有一个ZuulFilter的缓存
private static final FilterRegistry INSTANCE = new FilterRegistry();
public static final FilterRegistry instance() {
return INSTANCE;
}
//缓存字符串到ZuulFilter实例的映射关系,如果是从文件加载,字符串key的格式是:文件绝对路径 + 文件名,当然也可以自实现
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实例,注意到访问权限是包许可,实际上就是饿汉式单例
final static FilterLoader INSTANCE = new FilterLoader();
private static final Logger LOG = LoggerFactory.getLogger(FilterLoader.class);
//缓存Filter名称(主要是从文件加载,名称为绝对路径 + 文件名的形式)->Filter最后修改时间戳的映射
private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>();
//缓存Filter名字->Filter代码的映射,实际上这个Map只使用到get方法进行存在性判断,一直是一个空的结构
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>>();
//前面提到的ZuulFilter全局缓存的单例
private FilterRegistry filterRegistry = FilterRegistry.instance();
//动态代码编译器实例,Zuul提供的默认实现是GroovyCompiler
static DynamicCodeCompiler COMPILER;
//ZuulFilter的工厂类
static FilterFactory FILTER_FACTORY = new DefaultFilterFactory();
//下面三个方法说明DynamicCodeCompiler、FilterRegistry、FilterFactory可以被覆盖
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;
}
//返回所有缓存的ZuulFilter实例的总数量
public int filterInstanceMapSize() {
return filterRegistry.size();
}
//通过ZuulFilter的类代码和Filter名称获取ZuulFilter实例
public ZuulFilter getFilter(String sCode, String sName) throws Exception {
//检查filterCheck是否存在相同名字的Filter,如果存在说明已经加载过
if (filterCheck.get(sName) == null) {
//filterCheck中放入Filter名称
filterCheck.putIfAbsent(sName, sName);
//filterClassCode中不存在加载过的Filter名称对应的代码
if (!sCode.equals(filterClassCode.get(sName))) {
LOG.info("reloading code " + sName);
//从全局缓存中移除对应的Filter
filterRegistry.remove(sName);
}
}
ZuulFilter filter = filterRegistry.get(sName);
//如果全局缓存中不存在对应的Filter,就使用DynamicCodeCompiler加载代码,使用FilterFactory实例化ZuulFilter
//注意加载的ZuulFilter类不能是抽象的,必须是继承了ZuulFilter的子类
if (filter == null) {
Class clazz = COMPILER.compile(sCode, sName);
if (!Modifier.isAbstract(clazz.getModifiers())) {
filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
}
}
return filter;
}
//通过文件加加载ZuulFilter
public boolean putFilter(File file) throws Exception {
//Filter名称为文件的绝对路径+文件名(这里其实绝对路径已经包含文件名,这里再加文件名的目的不明确)
String sName = file.getAbsolutePath() + file.getName();
//如果文件被修改过则从全局缓存从移除对应的Filter以便重新加载
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());
//这里说明了一旦文件有修改,hashFiltersByType中对应的当前文件加载出来的Filter类型的缓存要移除,原因见下一个方法
if (list != null) {
hashFiltersByType.remove(filter.filterType()); //rebuild this list
}
filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
filterClassLastModified.put(sName, file.lastModified());
return true;
}
}
return false;
}
//通过Filter类型获取同类型的所有ZuulFilter
public List<ZuulFilter> getFiltersByType(String filterType) {
List<ZuulFilter> list = hashFiltersByType.get(filterType);
if (list != null) return list;
list = new ArrayList<ZuulFilter>();
//如果hashFiltersByType缓存被移除,这里从全局缓存中加载所有的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);
}
}
//注意这里会进行排序,是基于filterOrder
Collections.sort(list); // sort by priority
//这里总是putIfAbsent,这就是为什么上个方法可以放心地在修改的情况下移除指定Filter类型中的全部缓存实例的原因
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;
//文件名过滤器,Zuul中的默认实现是GroovyFileFilter,只接受.groovy后缀的文件
static FilenameFilter FILENAME_FILTER;
static FilterFileManager INSTANCE;
private FilterFileManager() {
}
public static void setFilenameFilter(FilenameFilter filter) {
FILENAME_FILTER = filter;
}
//init方法是核心静态方法,它具备了配置,预处理和激活后台轮询线程的功能
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;
}
//启动后台轮询守护线程,每休眠pollingIntervalSeconds秒则进行一次文件扫描尝试更新Filter
void startPoller() {
poller = new Thread("GroovyFilterFileManagerPoller") {
public void run() {
while (bRunning) {
try {
sleep(pollingIntervalSeconds * 1000);
//预处理文件,实际上是ZuulFilter的预加载
manageFiles();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
//设置为守护线程
poller.setDaemon(true);
poller.start();
}
//根据指定目录路径获取目录,主要需要转换为ClassPath
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;
}
//遍历配置目录,获取所有配置目录下的所有满足FilenameFilter过滤条件的文件
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;
}
//遍历指定文件列表,调用FilterLoader单例中的putFilter
void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException {
for (File file : aFiles) {
FilterLoader.getInstance().putFilter(file);
}
}
//获取指定目录下的所有文件,调用processGroovyFiles,个人认为这两个方法没必要做单独封装
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()
进行初始化,我们分析一下它的初始化流程:
//保存RequestContext自身类型
protected static Class<? extends RequestContext> contextClass = RequestContext.class;
//静态对象
private static RequestContext testContext = null;
//静态final修饰的ThreadLocal实例,用于存放所有的RequestContext,每个RequestContext都会绑定在自身请求的处理线程中
//注意这里的ThreadLocal实例的initialValue()方法,当ThreadLocal的get()方法返回null的时候总是会调用initialValue()方法
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;
//当ThreadLocal的get()方法返回null的时候总是会调用initialValue()方法,所以这里是"无则新建RequestContext"的逻辑
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> {
//netflix的配置组件,实际上就是基于配置文件提取的指定key的值
private final AtomicReference<DynamicBooleanProperty> filterDisabledRef = new AtomicReference<>();
//定义Filter的类型
abstract public String filterType();
//定义当前Filter实例执行的顺序
abstract public int filterOrder();
//是否静态的Filter,静态的Filter是无状态的
public boolean isStaticFilter() {
return true;
}
//禁用当前Filter的配置属性的Key名称
//Key=zuul.${全类名}.${filterType}.disable
public String disablePropertyName() {
return "zuul." + this.getClass().getSimpleName() + "." + filterType() + ".disable";
}
//判断当前的Filter是否禁用,通过disablePropertyName方法从配置中读取,默认是不禁用,也就是启用
public boolean isFilterDisabled() {
filterDisabledRef.compareAndSet(null, DynamicPropertyFactory.getInstance().getBooleanProperty(disablePropertyName(), false));
return filterDisabledRef.get().get();
}
//这个是核心方法,执行Filter,如果Filter不是禁用、并且满足执行时机则调用run方法,返回执行结果,记录执行轨迹
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;
}
//实现Comparable,基于filterOrder升序排序,也就是filterOrder越大,执行优先度越低
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实例的核心方法:
//指定Filter类型执行该类型下的所有ZuulFilter
public Object runFilters(String sType) throws Throwable {
//尝试打印Debug日志
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
//获取所有指定类型的ZuulFilter
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);
//如果处理结果是Boolean类型尝试做或操作,其他类型结果忽略
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
//执行ZuulFilter,这个就是ZuulFilter执行逻辑
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();
}
//简单调用ZuulFilter的runFilter方法
ZuulFilterResult result = filter.runFilter();
ExecutionStatus s = result.getStatus();
execTime = System.currentTimeMillis() - ltime;
switch (s) {
case FAILED:
t = result.getException();
//记录调用链中当前Filter的名称,执行结果状态和执行时间
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
break;
case SUCCESS:
o = result.getResult();
//记录调用链中当前Filter的名称,执行结果状态和执行时间
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);
//记录调用链中当前Filter的名称,执行结果状态和执行时间
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 {
//实际上委托到ZuulRunner的init方法
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
//初始化RequestContext实例
RequestContext context = RequestContext.getCurrentContext();
//设置RequestContext中zuulEngineRan=true
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);
}
//这里会先设置RequestContext实例中的throwable属性为执行抛出的Throwable实例
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.8080文件夹'
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());
// FixBug: no global web.xml found
for (LifecycleListener ll : ctx.findLifecycleListeners()) {
if (ll instanceof ContextConfig) {
((ContextConfig) ll).setDefaultWebXml(ctx.getDefaultWebXml());
}
}
//这里添加两个度量父类的空实现
CounterFactory.initialize(new DefaultCounterFactory());
TracerFactory.initialize(new DefaultTracerFactory());
//这里添加自实现的ZuulFilter
FilterRegistry.instance().put("printParameterZuulFilter", new PrintParameterZuulFilter());
FilterRegistry.instance().put("sendResponseZuulFilter", new SendResponseZuulFilter());
//这里添加ZuulServlet
Context context = tomcat.addContext("/zuul", null);
Tomcat.addServlet(context, "zuul", new ZuulServlet());
//设置Servlet的路径
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)
zuul源码分析-探究原生zuul的工作原理的更多相关文章
- Volley源码分析(四)NetWork与ResponseDelivery工作原理
这篇文章主要分析网络请求和结果交付的过程. NetWork工作原理 之前已经说到通过mNetWork.performRequest()方法来得到NetResponse,看一下该方法具体的执行流程,pe ...
- Spring Ioc源码分析系列--@Autowired注解的实现原理
Spring Ioc源码分析系列--@Autowired注解的实现原理 前言 前面系列文章分析了一把Spring Ioc的源码,是不是云里雾里,感觉并没有跟实际开发搭上半毛钱关系?看了一遍下来,对我的 ...
- Vue.js 源码分析(四) 基础篇 响应式原理 data属性
官网对data属性的介绍如下: 意思就是:data保存着Vue实例里用到的数据,Vue会修改data里的每个属性的访问控制器属性,当访问每个属性时会访问对应的get方法,修改属性时会执行对应的set方 ...
- 源码分析 Alibaba sentinel 滑动窗口实现原理(文末附原理图)
要实现限流.熔断等功能,首先要解决的问题是如何实时采集服务(资源)调用信息.例如将某一个接口设置的限流阔值 1W/tps,那首先如何判断当前的 TPS 是多少?Alibaba Sentinel 采用滑 ...
- Struts2 源码分析——Action代理类的工作
章节简言 上一章笔者讲到关于如何加载配置文件里面的package元素节点信息.相信读者到这里心里面对struts2在启动的时候加载相关的信息有了一定的了解和认识.而本章将讲到关于struts2启动成功 ...
- Mybatis源码分析--关联表查询及延迟加载原理(二)
在上一篇博客Mybatis源码分析--关联表查询及延迟加载(一)中我们简单介绍了Mybatis的延迟加载的编程,接下来我们通过分析源码来分析一下Mybatis延迟加载的实现原理. 其实简单来说Myba ...
- 【Canal源码分析】Sink及Store工作过程
一.序列图 二.源码分析 2.1 Sink Sink阶段所做的事情,就是根据一定的规则,对binlog数据进行一定的过滤.我们之前跟踪过parser过程的代码,发现在parser完成后,会把数据放到一 ...
- RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)
在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...
- 源码分析——Action代理类的工作
Action代理类的新建 通过<Struts2 源码分析——调结者(Dispatcher)之执行action>章节我们知道执行action请求,最后会落到Dispatcher类的serv ...
随机推荐
- QT学习笔记2:QT中常用函数
一.QString转number QString number() QString number() QString number() QString number() QString number( ...
- AOP 面向切面 记录请求接口的日志
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是OOP的延续,是软件开发中的一个热点, ...
- bzoj1036 count 树链剖分或LCT
这道题很久以前用树链剖分写的,最近在学LCT ,就用LCT再写了一遍,也有一些收获. 因为这道题点权可以是负数,所以在update时就要注意一下,因为平时我的0节点表示空,它的点权为0,这样可以处理点 ...
- 2015 UESTC 数据结构专题D题 秋实大哥与战争 变化版本的线段树,合并区间,单点查询
D - 秋实大哥与战争 Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://acm.uestc.edu.cn/#/contest/show/59 D ...
- SqlServer Base64解码中文
最近在做一个和拥有TurboCRM相关的项目,其中需要取出客户信息的联系人字段,经过查看,这个字段在存入时用Base64加密过了 这个功能在应用层实现是很方便的,但是由于一些特殊原因,只能放到SqlS ...
- windows安装zookeeper单机版
1.在apache的官方网站提供了好多镜像下载地址,然后找到对应的版本,目前最新的是3.4.6下载地址:http://mirrors.cnnic.cn/apache/zookeeper/zookeep ...
- SQL Server2008无法修改表结构?
之前一直用SQL Server2005的数据库,最近升级到2008之后发现修改不了表结构,提示: 根据提示,取消“阻止保存要求重新创建表的更改”后就可以了. 具体操作:SQL Server Manag ...
- ALAssetsLibrary使用
在iOS中,我们调用摄像头和选择相册中的资源,我们可以使用:UIImagePickerController类来完成. 当然,我们也可以不使用UI的形式来访问iOS设备的相册资源. 那就是使用:AL ...
- Maven编译代码的相关命令
第一.main目录下的主代码编写完毕后,使用Maven进行编译,在项目根目录下运行命令mvn clean compile进 行项目编译. 第二.test目录下的测试用例编写完毕之后就可以调 ...
- PHP session过期机制和配置
问题:使用PHP session时会遇到明明超过了session过期时间,但session依然完好无损的活着,让人头大. 其实仔细看一下php.ini关于PHP session回收机制就一目了然了. ...