Spring Cloud入門教程(六):API服務網關(Zuul) 下

上一篇:《Spring Cloud入門教程(五):API服務網關(Zuul) 上》

本人和同事撰寫的《Spring Cloud微服務架構開發實戰》一書也在京東、當當等書店上架,大家可以點擊這里前往購買,多謝大家支持和捧場!


Zuul給我們的第一印象通常是這樣:它包含了對請求的路由和過濾兩個功能,其中路由功能負責將外部請求轉發到具體的微服務實例上,是實現外部訪問統一入口的基礎。過濾器功能則負責對請求的處理過程進行干預,是實現請求校驗、服務聚合等功能的基礎。然而實際上,路由功能在真正運行時,它的路由映射和請求轉發都是由幾個不同的過濾器完成的。其中,路由映射主要是通過PRE類型的過濾器完成,它將請求路徑與配置的路由規則進行匹配,以找到需要轉發的目標地址。而請求轉發的部分則是由Route類型的過濾器來完成,對PRE類型過濾器獲得的路由地址進行轉發。所以,過濾器可以說是Zuul實現API網關功能最重要的核心部件,每一個進入Zuul的請求都會經過一系列的過濾器處理鏈得到請求響應并返回給客戶端。

1. 過濾器簡介

1.1 過濾器特性

Zuul過濾器的關鍵特性有:

  • Type: 定義在請求執行過程中何時被執行;
  • Execution Order: 當存在多個過濾器時,用來指示執行的順序,值越小就會越早執行;
  • Criteria: 執行的條件,即該過濾器何時會被觸發;
  • Action: 具體的動作。

過濾器之間并不會直接進行通信,而是通過RequestContext來共享信息,RequestContext是線程安全的。

對應上面Zuul過濾器的特性,我們在實現一個自定義過濾器時需要實現的方法有:

/**
 * Zuul Pre-Type Filter
 *
 * @author CD826(CD826Dong@gmail.com)
 * @since 1.0.0
 */
public class PreTypeZuulFilter extends ZuulFilter {
    protected Logger logger = LoggerFactory.getLogger(PreTypeZuulFilter.class);

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        this.logger.info("This is pre-type zuul filter.");
        return null;
    }
}

其中:

  • filterType()方法是該過濾器的類型;
  • filterOrder()方法返回的是執行順序;
  • shouldFilter()方法則是判斷是否需要執行該過濾器;
  • run()則是所要執行的具體過濾動作。

1.2 過濾器類型

Zuul中定義了四種標準的過濾器類型,這些過濾器類型對應于請求的典型生命周期。

  • PRE過濾器: 在請求被路由之前調用, 可用來實現身份驗證、在集群中選擇請求的微服務、記錄調試信息等;
  • ROUTING過濾器: 在路由請求時候被調用;
  • POST過濾器: 在路由到微服務以后執行, 可用來為響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等;
  • ERROR過濾器: 在處理請求過程時發生錯誤時被調用。

Zuul過濾器的類型其實也是Zuul過濾器的生命周期,通過下面這張圖來了解它們的執行過程。

Zuul-Filter-010

除了上面給出的四種默認的過濾器類型之外,Zuul還允許我們創建自定義的過濾器類型。例如,我們可以定制一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到后端的微服務。

1.3 自定義過濾器示例代碼

筆者自己沒有單獨構建一個過濾器示例的場景,我們看一下官方給出的幾個示例。

PRE類型示例

public class QueryParamPreFilter extends ZuulFilter { 
    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
    }
    
    @Override
    public String filterType() {
        return PRE_TYPE; 
    }
    
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
            && !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
    }
    
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext(); 
        HttpServletRequest request = ctx.getRequest();
        if (request.getParameter("foo") != null) {
            // put the serviceId in `RequestContext`
            ctx.put(SERVICE_ID_KEY, request.getParameter("foo")); 
        }
        return null; 
    }
}

這個是官方給出的一個示例,從請求的參數foo中獲取需要轉發到的服務Id。當然官方并不建議我們這么做,這里只是方便給出一個示例而已。

ROUTE類型示例

public class OkHttpRoutingFilter extends ZuulFilter {
    @Autowired
    private ProxyRequestHelper helper;

    @Override
    public String filterType() {
        return ROUTE_TYPE; 
    }

    @Override
    public int filterOrder() {
        return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1; 
    }

    @Override
    public boolean shouldFilter() {
        return RequestContext.getCurrentContext().getRouteHost() != null &&             RequestContext.getCurrentContext().sendZuulResponse();
    }
    
    @Override
    public Object run() {
        OkHttpClient httpClient = new OkHttpClient.Builder() 
            // customize
            .build();

        RequestContext context = RequestContext.getCurrentContext(); 
        HttpServletRequest request = context.getRequest();
        
        String method = request.getMethod();

        String uri = this.helper.buildZuulRequestURI(request);

        Headers.Builder headers = new Headers.Builder(); 
        Enumeration<String> headerNames = request.getHeaderNames(); 
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement(); 
            Enumeration<String> values = request.getHeaders(name);

            while (values.hasMoreElements()) { 
                String value = values.nextElement(); 
                headers.add(name, value);
            }
        }

        InputStream inputStream = request.getInputStream();

        RequestBody requestBody = null;
        if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
            MediaType mediaType = null;
            if (headers.get("Content-Type") != null) {
                mediaType = MediaType.parse(headers.get("Content-Type")); 
            }
            requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream)); 
        }

        Request.Builder builder = new Request.Builder()
            .headers(headers.build())
            .url(uri)
            .method(method, requestBody);

        Response response = httpClient.newCall(builder.build()).execute();

        LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
        for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) { 
            responseHeaders.put(entry.getKey(), entry.getValue());
        }

        this.helper.setResponse(response.code(), response.body().byteStream(),          responseHeaders);
        context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
        return null; 
    }
}

這個示例是將HTTP請求轉換為使用OkHttp3進行請求,并將服務端的返回轉換成Servlet的響應。

注意: 官方說這僅僅是一個示例,功能不一定正確。

POST類型示例

public class AddResponseHeaderFilter extends ZuulFilter { 
    @Override
    public String filterType() { 
        return POST_TYPE;
    }
    
    @Override
    public int filterOrder() {
        return SEND_RESPONSE_FILTER_ORDER - 1; 
    }

    @Override
    public boolean shouldFilter() {
        return true; 
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext(); 
        HttpServletResponse servletResponse = context.getResponse();        servletResponse.addHeader("X-Foo", UUID.randomUUID().toString()); 
        return null;
    }
}

這個示例很簡單就是返回的頭中增加一個隨機生成X-Foo

1.4 禁用過濾器

只需要在application.properties(或yml)中配置需要禁用的filter,格式為:zuul.[filter-name].[filter-type].disable=true。如:

zuul.FormBodyWrapperFilter.pre.disable=true

1.5 關于Zuul過濾器Error的一點補充

當Zuul在執行過程中拋出一個異常時,error過濾器就會被執行。而SendErrorFilter只有在RequestContext.getThrowable()不為空的時候才會執行。它將錯誤信息設置到請求的javax.servlet.error.*屬性中,并轉發Spring Boot的錯誤頁面。

Zuul過濾器實現的具體類是ZuulServletFilter,其核心代碼如下:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
        try {
            preRouting();
        } catch (ZuulException e) {
            error(e);
            postRouting();
            return;
        }
        
        // Only forward onto to the chain if a zuul response is not being sent
        if (!RequestContext.getCurrentContext().sendZuulResponse()) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        
        try {
            routing();
        } catch (ZuulException e) {
            error(e);
            postRouting();
            return;
        }
        try {
            postRouting();
        } catch (ZuulException e) {
            error(e);
            return;
        }
    } catch (Throwable e) {
        error(new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" + e.getClass().getName()));
    } finally {
        RequestContext.getCurrentContext().unset();
    }
}

從這段代碼中可以看出,error可以在所有階段捕獲異常后執行,但是如果post階段中出現異常被error處理后則不再回到post階段執行,也就是說需要保證在post階段不要有異常,因為一旦有異常后就會造成該過濾器后面其它post過濾器將不再被執行。

一個簡單的全局異常處理的方法是: 添加一個類型為error的過濾器,將錯誤信息寫入RequestContext,這樣SendErrorFilter就可以獲取錯誤信息了。代碼如下:

public class GlobalErrorFilter extends ZuulFilter { 
    @Override
    public String filterType() { 
        return ERROR_TYPE;
    }
    
    @Override
    public int filterOrder() {
        return 10; 
    }

    @Override
    public boolean shouldFilter() {
        return true; 
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = context.getThrowable();
        this.logger.error("[ErrorFilter] error message: {}", throwable.getCause().getMessage());
        context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.exception", throwable.getCause());
        return null;
    }
}

2. @EnableZuulServer VS. @EnableZuulProxy

Zuul為我們提供了兩個主應用注解: @EnableZuulServer@EnableZuulProxy,其中@EnableZuulProxy包含@EnableZuulServer的功能,而且還加入了@EnableCircuitBreaker@EnableDiscoveryClient。當我們需要運行一個沒有代理功能的Zuul服務,或者有選擇的開關部分代理功能時,那么需要使用 @EnableZuulServer 替代 @EnableZuulProxy。 這時候我們可以添加任何 ZuulFilter類型實體類都會被自動加載,這和上一篇使用@EnableZuulProxy是一樣,但不會自動加載任何代理過濾器。

2.1 @EnableZuulServer默認過濾器

當我們使用@EnableZuulServer時,默認所加載的過濾器有:

2.1.1 PRE類型過濾器

  • ServletDetectionFilter

該過濾器是最先被執行的。其主要用來檢查當前請求是通過SpringDispatcherServlet處理運行的,還是通過ZuulServlet來處理運行的。判斷結果會保存在isDispatcherServletRequest中,值類型為布爾型。

  • FormBodyWrapperFilter

該過濾器的目的是將符合要求的請求體包裝成FormBodyRequestWrapper對象,以供后續處理使用。

  • DebugFilter

PRE類型過濾器。當請求參數中設置了debug參數時,該過濾器會將當前請求上下文中的RequestContext.setDebugRouting()RequestContext.setDebugRequest()設置為true,這樣后續的過濾器可以根據這兩個參數信息定義一些debug信息,當生產環境出現問題時,我們就可以通過增加該參數讓后臺打印出debug信息,以幫助我們進行問題分析。對于請求中的debug參數的名稱,我們可以通過zuul.debug.parameter進行自定義。

2.1.2 ROUTE類型過濾器

  • SendForwardFilter

該過濾器只對請求上下文中存在forward.to(FilterConstants.FORWARD_TO_KEY)參數的請求進行處理。即處理之前我們路由規則中forward的本地跳轉。

2.1.3 POST類型過濾器

  • SendResponseFilter

該過濾器就是對代理請求所返回的響應進行封裝,然后作為本次請求的相應發送回給請求者。

2.1.4 Error類型過濾器

  • SendErrorFilter

該過濾器就是判斷當前請求上下文中是否有異常信息(RequestContext.getThrowable()不為空),如果有則默認轉發到/error頁面,我們也可以通過設置error.path來自定義錯誤頁面。

2.2 @EnableZuulProxy默認過濾器

@EnableZuulProxy則在上面的基礎上增加以下過濾器:

2.2.1 PRE類型過濾器

  • PreDecorationFilter

該過濾器根據提供的RouteLocator確定路由到的地址,以及怎樣去路由。該路由器也可為后端請求設置各種代理相關的header。

2.2.2 ROUTE類型過濾器

  • RibbonRoutingFilter

該過濾器會針對上下文中存在serviceId(可以通過RequestContext.getCurrentContext().get(“serviceId”)獲取)的請求進行處理,使用Ribbon、Hystrix和可插拔的HTTP客戶端發送請求,并將服務實例的請求結果返回。也就是之前所說的只有當我們使用serviceId配置路由規則時Ribbon和Hystrix方才生效。

  • SimpleHostRoutingFilter

該過濾器檢測到routeHost參數(可通過RequestContext.getRouteHost()獲取)設置時,就會通過Apache HttpClient向指定的URL發送請求。此時,請求不會使用Hystrix命令進行包裝,所以這類請求也就沒有線程隔離和斷路器保護。

你可以到這里下載本篇的代碼。

下一篇:《Spring Cloud入門教程(七):分布式鏈路跟蹤(Sleuth)》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容