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) { Filedirectory = 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)