Spring Cloud入門教程(六):API服務(wù)網(wǎng)關(guān)(Zuul) 下

上一篇:《Spring Cloud入門教程(五):API服務(wù)網(wǎng)關(guān)(Zuul) 上》

本人和同事撰寫的《Spring Cloud微服務(wù)架構(gòu)開發(fā)實戰(zhàn)》一書也在京東矿辽、當當?shù)葧晟霞芮镉荆蠹铱梢渣c擊這里前往購買,多謝大家支持和捧場恍风!


Zuul給我們的第一印象通常是這樣:它包含了對請求的路由和過濾兩個功能,其中路由功能負責將外部請求轉(zhuǎn)發(fā)到具體的微服務(wù)實例上,是實現(xiàn)外部訪問統(tǒng)一入口的基礎(chǔ)熟菲。過濾器功能則負責對請求的處理過程進行干預(yù),是實現(xiàn)請求校驗朴恳、服務(wù)聚合等功能的基礎(chǔ)抄罕。然而實際上,路由功能在真正運行時于颖,它的路由映射和請求轉(zhuǎn)發(fā)都是由幾個不同的過濾器完成的呆贿。其中,路由映射主要是通過PRE類型的過濾器完成森渐,它將請求路徑與配置的路由規(guī)則進行匹配做入,以找到需要轉(zhuǎn)發(fā)的目標地址。而請求轉(zhuǎn)發(fā)的部分則是由Route類型的過濾器來完成同衣,對PRE類型過濾器獲得的路由地址進行轉(zhuǎn)發(fā)竟块。所以,過濾器可以說是Zuul實現(xiàn)API網(wǎng)關(guān)功能最重要的核心部件耐齐,每一個進入Zuul的請求都會經(jīng)過一系列的過濾器處理鏈得到請求響應(yīng)并返回給客戶端浪秘。

1. 過濾器簡介

1.1 過濾器特性

Zuul過濾器的關(guān)鍵特性有:

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

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

對應(yīng)上面Zuul過濾器的特性,我們在實現(xiàn)一個自定義過濾器時需要實現(xiàn)的方法有:

/**
 * 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()方法返回的是執(zhí)行順序;
  • shouldFilter()方法則是判斷是否需要執(zhí)行該過濾器;
  • run()則是所要執(zhí)行的具體過濾動作沟沙。

1.2 過濾器類型

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

  • PRE過濾器: 在請求被路由之前調(diào)用, 可用來實現(xiàn)身份驗證尝胆、在集群中選擇請求的微服務(wù)丧裁、記錄調(diào)試信息等;
  • ROUTING過濾器: 在路由請求時候被調(diào)用;
  • POST過濾器: 在路由到微服務(wù)以后執(zhí)行, 可用來為響應(yīng)添加標準的HTTP Header、收集統(tǒng)計信息和指標含衔、將響應(yīng)從微服務(wù)發(fā)送給客戶端等;
  • ERROR過濾器: 在處理請求過程時發(fā)生錯誤時被調(diào)用煎娇。

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

Zuul-Filter-010

除了上面給出的四種默認的過濾器類型之外缓呛,Zuul還允許我們創(chuàng)建自定義的過濾器類型。例如杭隙,我們可以定制一種STATIC類型的過濾器哟绊,直接在Zuul中生成響應(yīng),而不將請求轉(zhuǎn)發(fā)到后端的微服務(wù)痰憎。

1.3 自定義過濾器示例代碼

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

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; 
    }
}

這個是官方給出的一個示例铣耘,從請求的參數(shù)foo中獲取需要轉(zhuǎn)發(fā)到的服務(wù)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請求轉(zhuǎn)換為使用OkHttp3進行請求裆操,并將服務(wù)端的返回轉(zhuǎn)換成Servlet的響應(yīng)。

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

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 關(guān)于Zuul過濾器Error的一點補充

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

Zuul過濾器實現(xiàn)的具體類是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可以在所有階段捕獲異常后執(zhí)行奕扣,但是如果post階段中出現(xiàn)異常被error處理后則不再回到post階段執(zhí)行薪鹦,也就是說需要保證在post階段不要有異常,因為一旦有異常后就會造成該過濾器后面其它post過濾器將不再被執(zhí)行惯豆。

一個簡單的全局異常處理的方法是: 添加一個類型為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為我們提供了兩個主應(yīng)用注解: @EnableZuulServer@EnableZuulProxy地熄,其中@EnableZuulProxy包含@EnableZuulServer的功能,而且還加入了@EnableCircuitBreaker@EnableDiscoveryClient芯杀。當我們需要運行一個沒有代理功能的Zuul服務(wù)端考,或者有選擇的開關(guān)部分代理功能時,那么需要使用 @EnableZuulServer 替代 @EnableZuulProxy揭厚。 這時候我們可以添加任何 ZuulFilter類型實體類都會被自動加載却特,這和上一篇使用@EnableZuulProxy是一樣,但不會自動加載任何代理過濾器筛圆。

2.1 @EnableZuulServer默認過濾器

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

2.1.1 PRE類型過濾器

  • ServletDetectionFilter

該過濾器是最先被執(zhí)行的。其主要用來檢查當前請求是通過SpringDispatcherServlet處理運行的太援,還是通過ZuulServlet來處理運行的闽晦。判斷結(jié)果會保存在isDispatcherServletRequest中,值類型為布爾型提岔。

  • FormBodyWrapperFilter

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

  • DebugFilter

PRE類型過濾器唧垦。當請求參數(shù)中設(shè)置了debug參數(shù)時捅儒,該過濾器會將當前請求上下文中的RequestContext.setDebugRouting()RequestContext.setDebugRequest()設(shè)置為true,這樣后續(xù)的過濾器可以根據(jù)這兩個參數(shù)信息定義一些debug信息振亮,當生產(chǎn)環(huán)境出現(xiàn)問題時巧还,我們就可以通過增加該參數(shù)讓后臺打印出debug信息,以幫助我們進行問題分析坊秸。對于請求中的debug參數(shù)的名稱麸祷,我們可以通過zuul.debug.parameter進行自定義。

2.1.2 ROUTE類型過濾器

  • SendForwardFilter

該過濾器只對請求上下文中存在forward.to(FilterConstants.FORWARD_TO_KEY)參數(shù)的請求進行處理褒搔。即處理之前我們路由規(guī)則中forward的本地跳轉(zhuǎn)阶牍。

2.1.3 POST類型過濾器

  • SendResponseFilter

該過濾器就是對代理請求所返回的響應(yīng)進行封裝喷面,然后作為本次請求的相應(yīng)發(fā)送回給請求者。

2.1.4 Error類型過濾器

  • SendErrorFilter

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

2.2 @EnableZuulProxy默認過濾器

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

2.2.1 PRE類型過濾器

  • PreDecorationFilter

該過濾器根據(jù)提供的RouteLocator確定路由到的地址,以及怎樣去路由。該路由器也可為后端請求設(shè)置各種代理相關(guān)的header乖杠。

2.2.2 ROUTE類型過濾器

  • RibbonRoutingFilter

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

  • SimpleHostRoutingFilter

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末创淡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子南吮,更是在濱河造成了極大的恐慌琳彩,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件部凑,死亡現(xiàn)場離奇詭異露乏,居然都是意外死亡,警方通過查閱死者的電腦和手機涂邀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門瘟仿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人比勉,你說我怎么就攤上這事劳较。” “怎么了浩聋?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵观蜗,是天一觀的道長。 經(jīng)常有香客問我衣洁,道長墓捻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任坊夫,我火速辦了婚禮砖第,結(jié)果婚禮上撤卢,老公的妹妹穿的比我還像新娘。我一直安慰自己梧兼,他們只是感情好放吩,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袱院,像睡著了一般屎慢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忽洛,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音环肘,去河邊找鬼欲虚。 笑死,一個胖子當著我的面吹牛悔雹,可吹牛的內(nèi)容都是我干的复哆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼腌零,長吁一口氣:“原來是場噩夢啊……” “哼梯找!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起益涧,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤锈锤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后闲询,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體久免,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年扭弧,在試婚紗的時候發(fā)現(xiàn)自己被綠了阎姥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡鸽捻,死狀恐怖呼巴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情御蒲,我是刑警寧澤衣赶,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站删咱,受9級特大地震影響屑埋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痰滋,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一摘能、第九天 我趴在偏房一處隱蔽的房頂上張望续崖。 院中可真熱鬧,春花似錦团搞、人聲如沸严望。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽像吻。三九已至,卻和暖如春复隆,著一層夾襖步出監(jiān)牢的瞬間拨匆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工挽拂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惭每,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓亏栈,卻偏偏與公主長得像台腥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绒北,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容