1. 程式人生 > >zuul原始碼分析-探究原生zuul的工作原理

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:

z-s-c-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構建成功後如下:

z-s-c-2

zuul-1.3.1中提供了一個Web應用的Sample專案,我們直接執行zuul-simple-webapp的Gradle配置中的Tomcat外掛即可啟動專案,開始Debug之旅:

z-s-c-3

原始碼分析

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、routerunFilters(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正常啟動後打印出熟悉的日誌如下:

z-s-c-4

接下來,用POSTMAN請求模擬一下請求:

z-s-c-5

小結

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)