1. 程式人生 > >從零開始搭建spring-cloud(2) ----zuul

從零開始搭建spring-cloud(2) ----zuul

zuul是什麼

zuul 是netflix開源的一個API Gateway 伺服器, 本質上是一個web servlet應用。
zuul 在雲平臺上提供動態路由,監控,彈性,安全等邊緣服務的框架。Zuul 相當於是裝置和 Netflix 流應用的 Web 網站後端所有請求的前門。
zuul的例子可以參考 netflix 在github上的 simple webapp,可以按照netflix 在github wiki 上文件說明來進行使用(https://github.com/Netflix/zuul/wiki)。

zuul的工作原理

過濾器機制

zuul的核心是一系列的filters, 其作用可以類比Servlet框架的Filter,或者AOP。
zuul把Request route到 使用者處理邏輯 的過程中,這些filter參與一些過濾處理,比如Authentication,Load Shedding等。

Zuul提供了一個框架,可以對過濾器進行動態的載入,編譯,執行。

Zuul的過濾器之間沒有直接的相互通訊,他們之間通過一個RequestContext的靜態類來進行資料傳遞的。RequestContext類中有ThreadLocal變數來記錄每個Request所需要傳遞的資料。

Zuul的過濾器是由Groovy寫成,這些過濾器檔案被放在Zuul Server上的特定目錄下面。Zuul會定期輪詢這些目錄,修改過的過濾器會動態的載入到Zuul Server中以便過濾請求使用。

下面有幾種標準的過濾器型別:
Zuul大部分功能都是通過過濾器來實現的。Zuul中定義了四種標準過濾器型別,這些過濾器型別對應於請求的典型生命週期。

  1. PRE:這種過濾器在請求被路由之前呼叫。我們可利用這種過濾器實現身份驗證、在叢集中選擇請求的微服務、記錄除錯資訊等。
  2. ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建傳送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
  3. POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應新增標準的HTTP Header、收集統計資訊和指標、將響應從微服務傳送給客戶端等。
  4. ERROR:在其他階段發生錯誤時執行該過濾器。

 內建的特殊過濾器

zuul還提供了一類特殊的過濾器,分別為:StaticResponseFilter和SurgicalDebugFilter
StaticResponseFilter:StaticResponseFilter允許從Zuul本身生成響應,而不是將請求轉發到源。
SurgicalDebugFilter:SurgicalDebugFilter允許將特定請求路由到分隔的除錯叢集或主機。

自定義的過濾器

除了預設的過濾器型別,Zuul還允許我們建立自定義的過濾器型別。
例如,我們可以定製一種STATIC型別的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。

過濾器的生命週期

Zuul請求的生命週期如圖,該圖詳細描述了各種型別的過濾器的執行順序。

zuul 能做什麼

Zuul可以通過載入動態過濾機制,從而實現以下各項功能:

  • 驗證與安全保障: 識別面向各類資源的驗證要求並拒絕那些與要求不符的請求。
  • 審查與監控: 在邊緣位置追蹤有意義資料及統計結果,從而為我們帶來準確的生產狀態結論。
  • 動態路由: 以動態方式根據需要將請求路由至不同後端叢集處。
  • 壓力測試: 逐漸增加指向叢集的負載流量,從而計算效能水平。
  • 負載分配: 為每一種負載型別分配對應容量,並棄用超出限定值的請求。
  • 靜態響應處理: 在邊緣位置直接建立部分響應,從而避免其流入內部叢集。
  • 多區域彈性: 跨越AWS區域進行請求路由,旨在實現ELB使用多樣化並保證邊緣位置與使用者儘可能接近。

 搭建zuul服務

首先搭建Eureka server和一個Eureka-Provider

在上一節從零開始搭建spring-cloud(1) ----eureka中拷貝spring-cloud-eureka-server到這個專案中。

在上一節從零開始搭建spring-cloud(1) ----eureka中拷貝spring-cloud-eureka-provider-A-1到這個專案中。

搭建zuul服務

新建專案spring-cloud-zuul,pom.xml內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.vincent</groupId>
    <artifactId>spring-cloud-zuul</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.4.RELEASE</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

</project>

我們都知道,在zuul過濾器裡PRE_TYPE型別是在路由前執行的,所以我要給大家演示配置三個PRE_TYPE型別的過濾器,按照順序依次處理不同的業務。以及,三個PRE_TYPE型別過濾器中任意一個出現異常時他的下游業務應該怎麼處理。

我們首先看一下專案的目錄結構:

各個Filter內容如下:

ErrorFilter.java 

package com.vincent.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ERROR_TYPE;

/**
 * @author vincent
 * @time 2019-06-23 16:15
 */
@Component
public class ErrorFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return ERROR_TYPE;
    }

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

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

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        System.out.println("這是ErrorFilter");

        return null;
    }
}

FirstPreFilter.java

package com.vincent.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

/**
 * @author vincent
 * @time 2019-06-23 16:17
 */
@Component
public class FirstPreFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

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

    @Override
    public Object run() throws ZuulException {
        System.out.println("這是第一個自定義Zuul Filter!");
        return null;
    }
}

SecondPreFilter.java

package com.vincent.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

/**
 * @author vincent
 * @time 2019-06-23 16:18
 */
@Component
public class SecondPreFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 2;
    }

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

    @Override
    public Object run() throws ZuulException {
        System.out.println("這是SecondPreFilter!");
        //從RequestContext獲取上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //從上下文獲取HttpServletRequest
        HttpServletRequest request = ctx.getRequest();
        //從request嘗試獲取a引數值
        String a = request.getParameter("a");
        //如果a引數值為空則進入此邏輯
        if (null == a) {
            //對該請求禁止路由,也就是禁止訪問下游服務
            ctx.setSendZuulResponse(false);
            //設定responseBody供PostFilter使用
            ctx.setResponseBody("{\"status\":500,\"message\":\"a param is null\"}");
            //logic-is-success保存於上下文,作為同類型下游Filter的執行開關
            ctx.set("logic-is-success", false);
            //到這裡此Filter邏輯結束
            return null;
        }
        //設定避免報空
        ctx.set("logic-is-success", true);
        return null;
    }
}

ThirdPreFilter.java

package com.vincent.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

/**
 * @author vincent
 * @time 2019-06-23 16:19
 */
@Component
public class ThirdPreFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 3;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        //從上下文獲取logic-is-success值,用於判斷此Filter是否執行
        return (boolean)ctx.get("logic-is-success");
    }

    @Override
    public Object run() throws ZuulException {
        System.out.println("這是ThirdPreFilter!");
        //從RequestContext獲取上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //從上下文獲取HttpServletRequest
        HttpServletRequest request = ctx.getRequest();
        //從request嘗試獲取b引數值
        String b = request.getParameter("b");
        //如果b引數值為空則進入此邏輯
        if (null == b) {
            //對該請求禁止路由,也就是禁止訪問下游服務
            ctx.setSendZuulResponse(false);
            //設定responseBody供PostFilter使用
            ctx.setResponseBody("{\"status\":500,\"message\":\"b param is null\"}");
            //logic-is-success保存於上下文,作為同類型下游Filter的執行開關,假定後續還有自定義Filter當設定此值
            ctx.set("logic-is-success", false);
            //到這裡此Filter邏輯結束
            return null;
        }
        return null;
    }
}

PostFilter.java

package com.vincent.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;

/**
 * @author vincent
 * @time 2019-06-23 16:20
 */
public class PostFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

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

    @Override
    public Object run() throws ZuulException {
        System.out.println("這是PostFilter!");
        //從RequestContext獲取上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //處理返回中文亂碼
        ctx.getResponse().setCharacterEncoding("GBK");
        //獲取上下文中儲存的responseBody
        String responseBody = ctx.getResponseBody();
        //如果responseBody不為空,則說明流程有異常發生
        if (null != responseBody) {
            //設定返回狀態碼
            ctx.setResponseStatusCode(500);
            //替換響應報文
            ctx.setResponseBody(responseBody);
        }
        return null;
    }
}

新建App.java,內容如下:

@SpringBootApplication
@EnableZuulProxy
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

我們開始配置application.properties,內容如下:

eureka.client.service-url.defaultZone=http://127.0.0.1:8080/eureka/
spring.application.name=service-zuul

zuul.routes.users.url=http://localhost:8081/
zuul.routes.users.path=/**
zuul.ignored-headers=Access-Controller-Allow-Credentials, Access-Control-Allow-Origin
zuul.host.connect-timeout-millis=10000000
zuul.host.socket-timeout-millis=10000000

server.port=8082

這裡zuul的匹配規則是通過url進行匹配。

先新增引數訪問微服務

控制檯輸出結果如下:

這是第一個自定義Zuul Filter!
這是SecondPreFilter!
這是PostFilter!

新增引數a訪問,

控制檯輸出結果如下:

這是第一個自定義Zuul Filter!
這是SecondPreFilter!
這是ThirdPreFilter!
這是PostFilter!

新增引數a和引數b訪問

控制檯輸出結果如下:

2019-06-23 17:29:44.348  INFO 3363 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver      : Resolving eureka endpoints via configuration
這是第一個自定義Zuul Filter!
這是SecondPreFilter!
這是ThirdPreFilter!
這是PostFilter!

zuul轉發有兩種配置

根據 eureka server 的serviceId 轉發

zuul.routes.serviceName.path=/exampleService/**
zuul.routes.serviceName.serviceId=serviceId

注:
zuul.routes 是固定的
serviceName 是可以隨便寫的,但最好根據要路由的服務取
serviceId 是 eureka 服務註冊時的名稱
exampleService 是前端請求某個微服務的一個公共的路徑名,如/users
而微服務在 Controller層的 RequestMapping 註解中可以不包含/users

例如本專案中的配置如下:

zuul.routes.myservice.path=/**
zuul.routes.myservice.service-id=service-provider-A

根據具體 URL 轉發:

zuul.routes.serviceName.path=/exampleService/**
zuul.routes.serviceName.url=http://127.0.0.1:8080/

如果專案尚未使用eureka,可以採用了第二種轉發規則。這種轉發有很多好處,最大的好處就是可以很好地過渡到Spring Cloud,使用Zuul可以直接HTTP呼叫,方便很多。

例如本專案中的URL規則轉發如下:

zuul.routes.users.url=http://localhost:8081/
zuul.routes.users.path=/**