zuul原始碼分析-探究原生zuul的工作原理
前提
最近在專案中使用了SpringCloud,基於zuul搭建了一個提供加解密、鑑權等功能的閘道器服務。鑑於之前沒怎麼使用過Zuul,於是順便仔細閱讀了它的原始碼。實際上,zuul原來提供的功能是很單一的:通過一個統一的Servlet入口(ZuulServlet)攔截所有的請求,然後通過內建的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)