前提
最近在項目中使用了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)