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

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

本人和同事撰寫(xiě)的《Spring Cloud微服務(wù)架構(gòu)開(kāi)發(fā)實(shí)戰(zhàn)》一書(shū)也在京東、當(dāng)當(dāng)?shù)葧?shū)店上架,大家可以點(diǎn)擊這里前往購(gòu)買(mǎi),多謝大家支持和捧場(chǎng)!


Zuul給我們的第一印象通常是這樣:它包含了對(duì)請(qǐng)求的路由和過(guò)濾兩個(gè)功能,其中路由功能負(fù)責(zé)將外部請(qǐng)求轉(zhuǎn)發(fā)到具體的微服務(wù)實(shí)例上,是實(shí)現(xiàn)外部訪問(wèn)統(tǒng)一入口的基礎(chǔ)。過(guò)濾器功能則負(fù)責(zé)對(duì)請(qǐng)求的處理過(guò)程進(jìn)行干預(yù),是實(shí)現(xiàn)請(qǐng)求校驗(yàn)、服務(wù)聚合等功能的基礎(chǔ)。然而實(shí)際上,路由功能在真正運(yùn)行時(shí),它的路由映射和請(qǐng)求轉(zhuǎn)發(fā)都是由幾個(gè)不同的過(guò)濾器完成的。其中,路由映射主要是通過(guò)PRE類(lèi)型的過(guò)濾器完成,它將請(qǐng)求路徑與配置的路由規(guī)則進(jìn)行匹配,以找到需要轉(zhuǎn)發(fā)的目標(biāo)地址。而請(qǐng)求轉(zhuǎn)發(fā)的部分則是由Route類(lèi)型的過(guò)濾器來(lái)完成,對(duì)PRE類(lèi)型過(guò)濾器獲得的路由地址進(jìn)行轉(zhuǎn)發(fā)。所以,過(guò)濾器可以說(shuō)是Zuul實(shí)現(xiàn)API網(wǎng)關(guān)功能最重要的核心部件,每一個(gè)進(jìn)入Zuul的請(qǐng)求都會(huì)經(jīng)過(guò)一系列的過(guò)濾器處理鏈得到請(qǐng)求響應(yīng)并返回給客戶(hù)端。

1. 過(guò)濾器簡(jiǎn)介

1.1 過(guò)濾器特性

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

  • Type: 定義在請(qǐng)求執(zhí)行過(guò)程中何時(shí)被執(zhí)行;
  • Execution Order: 當(dāng)存在多個(gè)過(guò)濾器時(shí),用來(lái)指示執(zhí)行的順序,值越小就會(huì)越早執(zhí)行;
  • Criteria: 執(zhí)行的條件,即該過(guò)濾器何時(shí)會(huì)被觸發(fā);
  • Action: 具體的動(dòng)作。

過(guò)濾器之間并不會(huì)直接進(jìn)行通信,而是通過(guò)RequestContext來(lái)共享信息,RequestContext是線程安全的。

對(duì)應(yīng)上面Zuul過(guò)濾器的特性,我們?cè)趯?shí)現(xiàn)一個(gè)自定義過(guò)濾器時(shí)需要實(shí)現(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()方法是該過(guò)濾器的類(lèi)型;
  • filterOrder()方法返回的是執(zhí)行順序;
  • shouldFilter()方法則是判斷是否需要執(zhí)行該過(guò)濾器;
  • run()則是所要執(zhí)行的具體過(guò)濾動(dòng)作。

1.2 過(guò)濾器類(lèi)型

Zuul中定義了四種標(biāo)準(zhǔn)的過(guò)濾器類(lèi)型,這些過(guò)濾器類(lèi)型對(duì)應(yīng)于請(qǐng)求的典型生命周期。

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

Zuul過(guò)濾器的類(lèi)型其實(shí)也是Zuul過(guò)濾器的生命周期,通過(guò)下面這張圖來(lái)了解它們的執(zhí)行過(guò)程。

Zuul-Filter-010

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

1.3 自定義過(guò)濾器示例代碼

筆者自己沒(méi)有單獨(dú)構(gòu)建一個(gè)過(guò)濾器示例的場(chǎng)景,我們看一下官方給出的幾個(gè)示例。

PRE類(lèi)型示例

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

這個(gè)是官方給出的一個(gè)示例,從請(qǐng)求的參數(shù)foo中獲取需要轉(zhuǎn)發(fā)到的服務(wù)Id。當(dāng)然官方并不建議我們這么做,這里只是方便給出一個(gè)示例而已。

ROUTE類(lèi)型示例

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

這個(gè)示例是將HTTP請(qǐng)求轉(zhuǎn)換為使用OkHttp3進(jìn)行請(qǐng)求,并將服務(wù)端的返回轉(zhuǎn)換成Servlet的響應(yīng)。

注意: 官方說(shuō)這僅僅是一個(gè)示例,功能不一定正確。

POST類(lèi)型示例

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

這個(gè)示例很簡(jiǎn)單就是返回的頭中增加一個(gè)隨機(jī)生成X-Foo。

1.4 禁用過(guò)濾器

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

zuul.FormBodyWrapperFilter.pre.disable=true

1.5 關(guān)于Zuul過(guò)濾器Error的一點(diǎn)補(bǔ)充

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

Zuul過(guò)濾器實(shí)現(xiàn)的具體類(lèi)是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í)行,也就是說(shuō)需要保證在post階段不要有異常,因?yàn)橐坏┯挟惓:缶蜁?huì)造成該過(guò)濾器后面其它post過(guò)濾器將不再被執(zhí)行。

一個(gè)簡(jiǎn)單的全局異常處理的方法是: 添加一個(gè)類(lèi)型為error的過(guò)濾器,將錯(cuò)誤信息寫(xiě)入RequestContext,這樣SendErrorFilter就可以獲取錯(cuò)誤信息了。代碼如下:

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為我們提供了兩個(gè)主應(yīng)用注解: @EnableZuulServer@EnableZuulProxy,其中@EnableZuulProxy包含@EnableZuulServer的功能,而且還加入了@EnableCircuitBreaker@EnableDiscoveryClient。當(dāng)我們需要運(yùn)行一個(gè)沒(méi)有代理功能的Zuul服務(wù),或者有選擇的開(kāi)關(guān)部分代理功能時(shí),那么需要使用 @EnableZuulServer 替代 @EnableZuulProxy。 這時(shí)候我們可以添加任何 ZuulFilter類(lèi)型實(shí)體類(lèi)都會(huì)被自動(dòng)加載,這和上一篇使用@EnableZuulProxy是一樣,但不會(huì)自動(dòng)加載任何代理過(guò)濾器。

2.1 @EnableZuulServer默認(rèn)過(guò)濾器

當(dāng)我們使用@EnableZuulServer時(shí),默認(rèn)所加載的過(guò)濾器有:

2.1.1 PRE類(lèi)型過(guò)濾器

  • ServletDetectionFilter

該過(guò)濾器是最先被執(zhí)行的。其主要用來(lái)檢查當(dāng)前請(qǐng)求是通過(guò)SpringDispatcherServlet處理運(yùn)行的,還是通過(guò)ZuulServlet來(lái)處理運(yùn)行的。判斷結(jié)果會(huì)保存在isDispatcherServletRequest中,值類(lèi)型為布爾型。

  • FormBodyWrapperFilter

該過(guò)濾器的目的是將符合要求的請(qǐng)求體包裝成FormBodyRequestWrapper對(duì)象,以供后續(xù)處理使用。

  • DebugFilter

PRE類(lèi)型過(guò)濾器。當(dāng)請(qǐng)求參數(shù)中設(shè)置了debug參數(shù)時(shí),該過(guò)濾器會(huì)將當(dāng)前請(qǐng)求上下文中的RequestContext.setDebugRouting()RequestContext.setDebugRequest()設(shè)置為true,這樣后續(xù)的過(guò)濾器可以根據(jù)這兩個(gè)參數(shù)信息定義一些debug信息,當(dāng)生產(chǎn)環(huán)境出現(xiàn)問(wèn)題時(shí),我們就可以通過(guò)增加該參數(shù)讓后臺(tái)打印出debug信息,以幫助我們進(jìn)行問(wèn)題分析。對(duì)于請(qǐng)求中的debug參數(shù)的名稱(chēng),我們可以通過(guò)zuul.debug.parameter進(jìn)行自定義。

2.1.2 ROUTE類(lèi)型過(guò)濾器

  • SendForwardFilter

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

2.1.3 POST類(lèi)型過(guò)濾器

  • SendResponseFilter

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

2.1.4 Error類(lèi)型過(guò)濾器

  • SendErrorFilter

該過(guò)濾器就是判斷當(dāng)前請(qǐng)求上下文中是否有異常信息(RequestContext.getThrowable()不為空),如果有則默認(rèn)轉(zhuǎn)發(fā)到/error頁(yè)面,我們也可以通過(guò)設(shè)置error.path來(lái)自定義錯(cuò)誤頁(yè)面。

2.2 @EnableZuulProxy默認(rèn)過(guò)濾器

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

2.2.1 PRE類(lèi)型過(guò)濾器

  • PreDecorationFilter

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

2.2.2 ROUTE類(lèi)型過(guò)濾器

  • RibbonRoutingFilter

該過(guò)濾器會(huì)針對(duì)上下文中存在serviceId(可以通過(guò)RequestContext.getCurrentContext().get(“serviceId”)獲取)的請(qǐng)求進(jìn)行處理,使用Ribbon、Hystrix和可插拔的HTTP客戶(hù)端發(fā)送請(qǐng)求,并將服務(wù)實(shí)例的請(qǐng)求結(jié)果返回。也就是之前所說(shuō)的只有當(dāng)我們使用serviceId配置路由規(guī)則時(shí)Ribbon和Hystrix方才生效。

  • SimpleHostRoutingFilter

該過(guò)濾器檢測(cè)到routeHost參數(shù)(可通過(guò)RequestContext.getRouteHost()獲取)設(shè)置時(shí),就會(huì)通過(guò)Apache HttpClient向指定的URL發(fā)送請(qǐng)求。此時(shí),請(qǐng)求不會(huì)使用Hystrix命令進(jìn)行包裝,所以這類(lèi)請(qǐng)求也就沒(méi)有線程隔離和斷路器保護(hù)。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容