天天看點

SpringCloud微服務基礎二(Hystrix、Feign和Zuul)寫在前面一、服務熔斷Hystrix二、聲明式服務消費Feign三、API網關Zuul

文章目錄

  • 寫在前面
  • 一、服務熔斷Hystrix
    • 1. 什麼是Hystrix
    • 2. Hystrix 搭建與配置
    • 3. @HystrixCommand注解常用參數
    • 4. Hystrix 的服務降級
    • 5. Hystrix 的異常處理
    • 6. 自定義Hystrix請求的服務異常熔斷處理
    • 7. Hystrix 儀表盤監控
      • (1)概述
      • (2)Hystrix Dashboard搭建與配置
      • (3)對單體應用進行監控
      • (4)Hystrix儀表盤監控資料解讀
  • 二、聲明式服務消費Feign
    • 1. 什麼是Feign
    • 2. 使用Feign代替RestTemplate通路服務提供者
    • 3. 負載均衡
    • 4. 服務熔斷
    • 5. 擷取熔斷的異常資訊
    • 6. Feign調用丢失請求頭問題
  • 三、API網關Zuul
    • 1. 概述
    • 2. 使用Zuul搭建API網關
    • 3. 路由規則
      • (1)配置路由規則
      • (2)忽略某些路徑
      • (3)路由增加統一字首
      • (4)路由規則通配符
      • (5)請求到達`API`網關後,再轉發給自己本身
    • 4. 請求過濾
    • 5. 異常處理
      • (1)自定義Error過濾器
      • (2)自定義全局error錯誤頁面

上一篇:Spring Cloud微服務基礎一 (Eureka和Ribbon)

寫在前面

本文使用的開發環境:

  • SpringBoot版本:2.2.4.RELEASE
  • Spring Cloud版本:Hoxton SR1
  • JDK版本:1.8.0_102
  • IntelliJ IDEA

一、服務熔斷Hystrix

1. 什麼是Hystrix

在微服務架構中,每個服務單元都是單獨部署,單獨運作,服務之間通過遠端調用實作資訊互動,那麼當某個服務的響應太慢或者故障,又或者因為網絡波動或故障,則會造成調用者延遲或調用失敗,當大量請求到達,則會造成請求的堆積,導緻調用者的線程挂起,進而引發調用者也無法響應,調用者也發生故障。

是以在微服務架構中,很容易造成服務故障的蔓延,引發整個微服務系統癱瘓不可用。為了解決此問題,微服務架構中引入了一種叫熔斷器的服務保護機制。

熔斷器也叫斷路器,就是當被調用方沒有響應,調用方直接傳回一個錯誤響應即可,而不是長時間的等待,這樣避免調用時因為等待而線程一直得不到釋放,避免故障在分布式系統間蔓延。

Spring Cloud Hystrix

實作了熔斷器、線程隔離等一系列服務保護功能。該功能也是基于Netflix的開源架構Hystrix實作的,該架構的目标在于通過控制那些通路遠端系統、服務和第三方庫的節點,進而對延遲和故障提供更強大的容錯能力。Hystrix具備服務降級、服務熔斷、線程和信号隔離、請求緩存、請求合并以及服務監控等強大功能。

2. Hystrix 搭建與配置

SpringCloud

的服務消費者中使用熔斷器

Hystrix

是非常簡單和友善的,隻需要簡單三步即可:

  1. 在建立服務消費者時,勾選

    hystrix

    依賴:

    Spring Cloud Circuit Breaker --> Hystrix [Maintenance]

    也可以手動往pom.xml檔案中添加

    Hystrix

    的依賴:
    <dependency>
     	<groupId>org.springframework.cloud</groupId>
     	<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
               
    如果手動添加依賴,必須添加Maven的依賴管理器
    <properties>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>
    
    <dependencyManagement>
     	<dependencies>
          	<dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
     		</dependency>
     	</dependencies>
    </dependencyManagement>
               
    注意:如果通過SpringBoot的開發工具建立Web工程那麼這個依賴以及依賴管理是自動添加的
  2. 在入口類中使用

    @EnableCircuitBreaker

    注解開啟斷路器功能,也可以使用一個

    @SpringCloudApplication

    注解代替主類上的三個注解。
    注解

    @SpringCloudApplication

    是三個注解的集合,包括:

    @SpringBootApplication

    @EnableDiscoveryClient

    @EnableCircuitBreaker

    ,分别是SpringBoot注解、注冊服務中心Eureka注解、斷路器注解。對于SpringCloud來說,這是每一微服務必須應有的三個注解,是以才推出了@SpringCloudApplication這一注解集合。
  3. 在調用遠端服務的方法上添加注解:

    @HystrixCommand

3. @HystrixCommand注解常用參數

@HystrixCommand

注解用于标記目前方法使用了

Hystrix

的服務熔斷機制,當服務的提供者出現異常或逾時都會進行熔斷。

  • fallbackMethod

    fallbackMethod

    參數用于指定當服務熔斷之後,使用哪個方法的傳回值來替代服務提供者的傳回資訊,取值為目前類中的某個方法名,該方法又叫異常熔斷方法。定義的異常熔斷方法中,可以通過指定形參

    Throwable

    來擷取程式抛出的異常,該異常可能是服務提供者抛出的,也可能是服務消費者抛出的。
  • ignoreExceptions

    :指定一個數組,該數組中是需要忽略的異常類型,當服務的提供者抛出了某個異常以後,如果這個異常我們不需要進行服務的降級,那麼我們就可以指定這個參數來忽略這個異常,異常将會抛給頁面。取值為

    Throwable

    類的子類

    class

    ,例如

    NullPointerException.class

    RuntimeException.class

    注意:通常情況下我們不能将程式的異常直接抛到使用者面前,是以通常情況我們不需要指定

    ignoreExceptions

    參數
  • commandProperties

    :指定

    Hystrix

    的一些屬性,取值為一個數組,其中

    @HystrixProperty

    注解用于指定

    Hystrix

    的某個屬性
    • timeoutInMilliseconds

      :表示逾時時間,機關為毫秒,預設為1000毫秒,如果你後端的響應超過此時間,就會觸發斷路器;

一個小例子:

@RestController
public class TestController {

	@Resource
    private RestTemplate restTemplate;

	@RequestMapping("/test")
	@HystrixCommand(fallbackMethod="error", 
		//ignoreExceptions = NullPointerException.class, 
		commandProperties={
			/*
			timeoutInMilliseconds屬性表示逾時時間  5000表示5秒的等待逾時,
			如果指定服務提供者是超過5秒鐘還沒有做出響應,則熔斷目前服務的調用
            */
           	@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="5000")
		}
    )
	public String test(){
        //注意:這裡使用了服務名從Eureka中擷取服務位址是以必須修改RestTemplateConfig中的内容
        ResponseEntity<String> result = restTemplate.getForEntity("http://SERVER-PROVIDER/test", String.class);
        return "Hystrix的服務消費者-----" + result.getBody();
    }

	
    //自定義的異常熔斷方法,方法名可以任意,主要用于當服務熔斷以後來替代服務提供者的響應資料
    //我們可以在熔斷方法中添加一個Throwable的參數用來擷取因程式抛出異常後熔斷服務的異常原因
    //注意:
    // 1、這個異常通常來講需要利用日志插件(log4j等)将異常資訊記錄到日志中, 這裡隻做簡單的輸出
    // 2、這個異常可能來自服務的提供者也能來自我們服務消費本身
    public String error(Throwable throwable) {
    	//通常要列印到日志檔案中,這裡隻做簡單的輸出處理
	    System.out.println(throwable.getMessage());   
        return "服務繁忙請稍後再試";
    }
}
           

4. Hystrix 的服務降級

有了服務的熔斷,随之就會有服務的降級,所謂服務降級,就是當某個服務熔斷之後,服務端提供的服務将不再被調用,此時由用戶端自己準備一個本地的 fallback 回調,傳回一個預設值來代表服務端的傳回;

這種做法,雖然不能得到正确的傳回結果,但至少保證了服務的可用,比直接抛出錯誤或服務不可用要好很多,當然這需要根據具體的業務場景來選擇;

5. Hystrix 的異常處理

在HystrixCommand實作的run()方法(後面會講)抛出異常時,除了HystrixBadRequestException之外,其他異常均會被Hystrix認為指令執行失敗并處罰服務降級的處理邏輯。

失敗類型 抛出的異常 異常原因 是否會被fallback
FAILURE HystrixRuntimeException 執行失敗
TIMEOUT HystrixRuntimeException 執行逾時
SHORT_CIRCUITED HystrixRuntimeException 斷路器打開
THREAD_POOL_REJECTED HystrixRuntimeException 線程池拒絕
SEMAPHORE_REJECTED HystrixRuntimeException 信号量拒絕
BAD_REQUEST HystrixBadRequestException 一般是由非法參數或者一些非系統異常引起的

6. 自定義Hystrix請求的服務異常熔斷處理

我們也可以自定義類繼承

HystrixCommand

來實作自定義的

Hystrix

請求,在

getFallback

方法中調用

getExecutionException

方法來擷取服務抛出的異常;

自定義熔斷器類:

public class MyHystrixCommand extends HystrixCommand {

    private RestTemplate restTemplate;
    private String url;

    public MyHystrixCommand(Setter setter, RestTemplate restTemplate, String url) {
        super(setter);
        this.restTemplate = restTemplate;
        this.url = url;
    }

    //這個方法不需要手動調用也不能手動調用,如果手動調用了以後就不能進行服務熔斷
    protected Object run() throws Exception {
        return restTemplate.getForEntity(url, String.class).getBody();
    }

    //異常熔斷方法,用于傳回熔斷後的響應資料來替代服務提供者的響應資訊
    protected Object getFallback() {
        //如果服務因為異常而熔斷的則用于擷取錯誤的異常資訊對象
        Throwable throwable = super.getExecutionException();
        //傳回熔斷的具體資料内容
        return "服務被熔斷了";
    }
}
           

Controller中調用自定義熔斷類:

@RestController
public class TestController {
	@Resource
    private RestTemplate restTemplate;

	@RequestMapping("/test1")
    public Object test1() {
        String url = "http://10-EUREKA-CLIENT-HYSTRIX-PROVIDER/test";
        MyHystrixCommand command = new MyHystrixCommand(com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")), restTemplate, url);
        Object result = command.execute();
        return result;
    }
}
           

7. Hystrix 儀表盤監控

(1)概述

Hystrix

儀表盤(

Hystrix Dashboard

),就像汽車的儀表盤實時顯示汽車的各項資料一樣,

Hystrix

儀表盤主要用來監控

Hystrix

的實時運作狀态,通過它我們可以看到

Hystrix

的各項名額資訊,進而快速發現系統中存在的問題進而解決它。

要使用

Hystrix

儀表盤功能,我們首先需要有一個

Hystrix Dashboard

,這個功能我們可以在原來的消費者應用上添加,讓原來的消費者應用具備Hystrix儀表盤功能,但一般地,微服務架構思想是推崇服務的拆分,Hystrix Dashboard也是一個服務,是以通常會單獨建立一個新的工程專門用做Hystrix Dashboard服務;

(2)Hystrix Dashboard搭建與配置

  1. 建立一個Spring Boot子產品,勾選相關依賴:
    1. Spring Cloud Circuit Breaker --> Hystrix Dashboard [Maintenance]

    2. Web --> Spring Web

    也可以直接添加Hystrix Dashboard的依賴:
    <dependency>
     	<groupId>org.springframework.cloud</groupId>
     	<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>
               
    如果手動添加依賴,必須添加Maven的依賴管理器
    <properties>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>
    
    <dependencyManagement>
     	<dependencies>
          	<dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
     		</dependency>
     	</dependencies>
    </dependencyManagement>
               
    注意:如果通過SpringBoot的開發工具建立Web工程那麼這個依賴以及依賴管理是自動添加的
  2. 在入口類上添加

    @EnableHystrixDashboard

    注解開啟儀表盤功能,如下:
    @SpringBootApplication
    @EnableHystrixDashboard
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
               
  3. 配置application.properties檔案:隻需要修改一下端口号即可:

至此,Hystrix監控環境就搭建好了。啟動服務,通路位址(

ip + 端口 + /hystrix

):

localhost:3721/hystrix

,進入如下頁面:

SpringCloud微服務基礎二(Hystrix、Feign和Zuul)寫在前面一、服務熔斷Hystrix二、聲明式服務消費Feign三、API網關Zuul

通過Hystrix Dashboard首頁面的文字介紹,我們可以知道,Hystrix Dashboard共支援三種不同的監控方式

  • 預設的叢集監控:通過URL:

    http://turbine-hostname:port/turbine.stream

    開啟,實作對預設叢集的監控。
  • 指定的叢集監控:通過URL:

    http://turbine-hostname:port/turbine.stream?cluster=[clusterName]

    開啟,實作對clusterName叢集的監控。
  • 單體應用的監控:通過URL:

    http://hystrix-app:port/hystrix.stream

    開啟,實作對具體某個服務執行個體的監控。

(3)對單體應用進行監控

首先,要有一個Eureka的服務注冊中心和一個服務提供者,可以參考上一篇部落格:Spring Cloud微服務基本使用(Eureka和Ribbon)

然後重新搭建服務消費者或者是修改服務消費者:

  1. 在建構服務的時候(Spring Boot),需要另外勾選 spring boot 的服務監控依賴和

    hystrix

    依賴:
    1. Spring Cloud Circuit Breaker --> Hystrix Dashboard [Maintenance]

    2. Ops --> Spring Boot Actuator

    也可以直接在現有項目上添加這兩個依賴(注意版本):
    <dependency>
     	<groupId>org.springframework.boot</groupId>
     	<artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <dependency>
     	<groupId>org.springframework.cloud</groupId>
     	<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
               
    如果手動添加依賴,必須添加Maven的依賴管理器
    <properties>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>
    
    <dependencyManagement>
     	<dependencies>
          	<dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
     		</dependency>
     	</dependencies>
    </dependencyManagement>
               
    注意:如果通過SpringBoot的開發工具建立Web工程那麼這個依賴以及依賴管理是自動添加的
  2. 配置檔案需要配置spring boot監控端點的通路權限:

    這個是用來暴露 endpoints 的,由于 endpoints 中會包含很多敏感資訊,除了 health 和 info 兩個支援直接通路外,其他的預設不能直接通路,是以我們讓它都能通路。或者指定:

    management.endpoints.web.exposure.include=hystrix.stream
               
  3. 通路入口:

    http://ip位址:端口号/actuator/hystrix.stream

    ,這個位址是填在儀表盤頁面第一個輸入框中的,剩下兩個輸入框随意指定。

    注意:如果出現一直

    Loading...

    的情況,則先通路服務中的任意一個其他接口,再通路

    /actuator/hystrix.stream

    接口。

(4)Hystrix儀表盤監控資料解讀

SpringCloud微服務基礎二(Hystrix、Feign和Zuul)寫在前面一、服務熔斷Hystrix二、聲明式服務消費Feign三、API網關Zuul

二、聲明式服務消費Feign

1. 什麼是Feign

Feign是Netflix公司開發的一個聲明式的REST調用用戶端;

Ribbon負載均衡、Hystrix服務熔斷是我們Spring Cloud中進行微服務開發非常基礎的元件,在使用的過程中我們也發現它們一般都是同時出現的,而且配置也都非常相似,每次開發都有很多相同的代碼,是以 Spring Cloud 基于

Netflix Feign

整合了

Ribbon

Hystrix

兩個元件,讓我們的開發工作變得更加簡單,就像 Spring Boot 是對 Spring+SpringMVC 的簡化一樣,Spring Cloud Feign 對

Ribbon

負載均衡、

Hystrix

服務熔斷進行簡化,在其基礎上進行了進一步的封裝,不僅在配置上大大簡化了開發工作,同時還提供了一種聲明式的Web服務用戶端定義方式;

2. 使用Feign代替RestTemplate通路服務提供者

  1. 建立一個Spring Boot子產品,勾選相關依賴:
    1. Spring Cloud Routing --> OpenFeign

    2. Spring Cloud Discovery --> Eureka Server

    3. Spring Cloud Circuit Breaker --> Hystrix [Maintenance]

    4. Web --> Spring Web

    也可以直接添加相關的依賴,這裡隻列出了 Feign 的依賴,其它的依賴之前都有列舉過,之後就不再贅述了:
    <dependency>
     	<groupId>org.springframework.cloud</groupId>
     	<artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
               
  2. 在項目入口類上添加

    @EnableFeignClients

    注解表示開啟 Spring Cloud Feign 的支援功能;
  3. 定義一個接口用來聲明服務
    1. 通過

      @FeignClient

      注解來标記目前接口是

      Feign

      的一個用戶端接口,并通過

      name

      屬性或者

      value

      屬性指定服務提供者的服務名稱(相當于是目前接口與一個服務提供者綁定,接口中的一個方法對應着服務提供者中的一個方法,調用接口中的方法就是調用服務提供者中對應的方法)。
    2. 在接口中定義方法,通過該方法通路服務提供者中對應的方法(相當于是兩個方法綁定),方法傳回值最好相同,方法名建議也相同,通過

      @RequestMapping

      注解的

      value

      屬性指定通路的服務提供者中服務的具體請求名稱

@FeignClient

有以下幾個較常用屬性:

屬性名 預設值 作用 備注
value 空字元串 調用服務名稱,和name屬性相同,如果項目使用了 Ribbon,name屬性會作為微服務的名稱,用于服務發現;
serviceId 空字元串 服務id,作用和name屬性相同 已過期
name 空字元串 調用服務名稱,和value屬性相同,如果項目使用了 Ribbon,name屬性會作為微服務的名稱,用于服務發現;
url 空字元串 url一般用于調試,可以手動指定@FeignClient調用的位址
decode404 false 配置響應狀态碼為404時是否應該抛出FeignExceptions
configuration {} Feign配置類,可以自定義 Feign的 Encoder、Decoder、LogLevel、Contract; 參考FeignClientsConfiguration
fallback void.class 定義容錯的處理類,當調用遠端接口失敗或逾時時,會調用對應接口的容錯邏輯,fallback指定的類必須實作@FeignClient标記的接口 底層依賴hystrix,啟動類要加上@EnableHystrix
fallbackFactory void.class 工廠類,用于生成fallback類示例,通過這個屬性我們可以實作每個接口通用的容錯邏輯,減少重複的代碼
path 空字元串 自動給所有方法的requestMapping前加上字首,類似與controller類上的requestMapping
  • 服務提供者(

    spring.application.name=server-provider

    ):
    @RestController
    public class Controller {
    
        @RequestMapping("/test")
        public String test() {
            return "Hello Spring Cloud!,This is Feign";
        }
    }
               
  • 服務消費者,與上面的服務提供者綁定:
    //@FeignClient 注解作用是标記目前接口是Feign的一個用戶端接口
    //屬性name 用于指定服務提供者的服務名稱
    @FeignClient(name ="server-provider")
    public interface TestService {
    
        //标記目前方法用于請求遠端服務提供者
        //屬性 /test 用于指定需要通路的服務的具體請求名稱
        @RequestMapping("/test")
        String test();
    }
               
  1. 接下來,就可以在 Controller 中調用服務提供者中的服務了:
    @RestController
    public class MyController {
        
        @Autowired
        private TestService testService;
    
        @RequestMapping("/web")
        public String web() {
            return testService.test();
        }
    }
               

實際上

@FeignClient

注解會對這個接口生成動态代理,從eureka的readonly中拿到其他服務資訊,進行http請求調用。

3. 負載均衡

Feign

預設內建了

Ribbon

,并和

Eureka

結合,預設實作了負載均衡的效果。

預設的負載均衡政策是輪詢,若要修改負載均衡政策,詳見上一篇部落格中的 Ribbon 的配置

4. 服務熔斷

  1. application.properties

    檔案開啟

    feign

    hystrix

    功能支援(支援熔斷),預設不支援
  2. 自定義一個服務熔斷器類,該類實作了

    Feign

    的用戶端接口,并實作了接口中的方法。當接口在調用服務提供者時被熔斷了,則會使用該類中實作的方法來替代服務提供的者的傳回資訊。
    //自定義一個服務熔斷器類,并實作Feign的用戶端接口
    @Component //将目前類定義到Spring上下文容器中
    public class MyFallBack implements TestService {
        //服務熔斷方法,當TestService接口中的這個方法在執行時被熔斷了則使用這個方法的傳回值
        //來替代服務提供的者的傳回資訊
        public String test() {
            return "服務被異常熔斷";
        }
    }
               
  3. 在對應的Feign的用戶端接口的

    @FeignClient

    注解中,通過

    fallback

    屬性指定使用的回調類,例如上面寫的Feign用戶端接口:
    //通過 fallback 屬性指定熔斷之後的回調類
    @FeignClient(name = "server-provider", fallback = MyFallback.class)
    public interface TestService {
    
        @RequestMapping("/test")
        String test();
    }
               

5. 擷取熔斷的異常資訊

剛才已經實作了遠端服務發生異常後可以進行服務的熔斷,但是不能擷取到遠端服務的異常資訊,如果要擷取遠端服務的異常資訊,就要使用

fallbackFactory

  1. 自定義異常熔斷工廠類,實作

    FallbackFactory<T>

    接口,範型

    <T>

    Feign

    的用戶端接口
  2. 重寫

    FallbackFactory<T>

    接口中的

    create

    方法,該方法的形參為

    Throwable

    ,傳回值類型為指定的範型

    T

  3. Feign

    用戶端接口的

    @FeignClient

    注解中,使用

    fallbackFactory

    屬性指定異常熔斷工廠類。
  • 自定義熔斷工廠類:
    //自定義異常熔斷的熔斷器工廠類
    //需要實作父接口FallbackFactory 并指定範型為某個Feign的用戶端接口
    //表示為某個接口進行服務熔斷
    @Component
    public class MyFallbackFactory implements FallbackFactory<TestService> {
        //create來自父接口用于建立具體的服務熔斷對象參數為異常的對象,可以利用這個對象
        //來實作異常資訊的擷取
        public TestService create(Throwable throwable) {
            return new TestService() {
                @Override
                public String test() {
                	//對異常資訊進行處理……
                    return "服務異常熔斷" + throwable.getMessage();
                }
            };
        }
    }
               
  • Feign

    用戶端接口:
    //通過 fallbackFactory 屬性指定熔斷之後的回調工廠類
    @FeignClient(name = "server-provider", fallbackFactory = MyFallbackFactory.class)
    public interface TestService {
    
        @RequestMapping("/test")
        String test();
    }
               

6. Feign調用丢失請求頭問題

SpringCloud微服務基礎二(Hystrix、Feign和Zuul)寫在前面一、服務熔斷Hystrix二、聲明式服務消費Feign三、API網關Zuul

Feign 在調用遠端服務時,會建立一個新的

Request

,是以要通過請求攔截器(

RequestInterceptor

)将原來的

request

中的請求頭加到新的

request

中。

@Configuration
public class GuliFeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
            	//底層用的是ThreadLocal
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                
                //擷取老請求
                HttpServletRequest request = attributes.getRequest();

                //給新請求同步老請求的cookie
                template.header("Cookie", request.getHeader("Cookie"));
            }
        };
    }
}
           

三、API網關Zuul

1. 概述

通過掌握 Eureka、Ribbon、Hystrix、Feign,我們已經可以基本搭建出一套簡略版的微服務架構了,如下圖:

SpringCloud微服務基礎二(Hystrix、Feign和Zuul)寫在前面一、服務熔斷Hystrix二、聲明式服務消費Feign三、API網關Zuul

在上面的架構圖中,我們的服務包括:内部服務

Service A

和内部服務

Service B

,這兩個服務都是叢集部署,每個服務部署了3個執行個體,他們都會通過

Eureka Server

注冊中心注冊與訂閱服務,而

Open Service

是一個對外的服務,也是叢集部署,外部調用方通過負載均衡裝置調用

Open Service

服務,比如負載均衡使用Nginx,這樣的實作是否合理,或者是否有更好的實作方式呢?接下來我們主要圍繞該問題展開讨論。

1、如果我們的微服務中有很多個獨立服務都要對外提供服務,那麼我們要如何去管理這些接口?特别是當項目非常龐大的情況下要如何管理?

2、在微服務中,一個獨立的系統被拆分成了很多個獨立的服務,為了確定安全,權限管理也是一個不可回避的問題,如果在每一個服務上都添加上相同的權限驗證代碼來確定系統不被非法通路,那麼工作量也就太大了,而且維護也非常不友善。

為了解決上述問題,微服務架構中提出了

API

網關的概念,它就像一個安檢站一樣,所有外部的請求都需要經過它的排程與過濾,然後

API

網關來實作請求路由、負載均衡、權限驗證等功能;

那麼Spring Cloud這個一站式的微服務開發架構基于

Netflix Zuul

實作了

Spring Cloud Zuul

,采用

Spring Cloud Zuul

即可實作一套

API

網關服務。

2. 使用Zuul搭建API網關

  1. 建立一個普通的 Spring Boot 子產品,在依賴項處勾選相關依賴:
    1. Spring Cloud Routing --> Zuul [Maintenance]
      <dependency>
       	<groupId>org.springframework.cloud</groupId>
       	<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
      </dependency>
                 
    2. Spring Cloud Discovery --> Eureka Discovery Client
  2. 在入口類上添加

    @EnableZuulProxy

    注解,開啟

    Zuul

    API

    網關服務功能。
  3. 配置 application.properties

    主要是配置 Eureka 和 Zuul 的路由規則,關于 Zuul 的路由規則,後面會細講。

    #配置服務内嵌的Tomcat端口
    server.port=8083
    #配置服務的名稱
    spring.application.name=zuul
    
    #配置路由規則 zuul.routes.api-zuul.path,其中 api-zuul 可以任意填寫
    # /api-zuul/** 表示請求的攔截規則為:以/api-zuul開頭的任意目錄以及子孫目錄中所有請求都會被攔截
    zuul.routes.api-zuul.path=/api-zuul/**
    #指向服務名字 用于對這個服務下的某個寫特定請求進行攔截
    # zuul.routes.api-zuul.serviceId,其中 api-zuul 要與上面配置的路由規則對應
    zuul.routes.api-zuul.serviceId=zuul-consumer-api-zuul
    
    #配置API網關到注冊中心上,API網關也将作為一個服務注冊到eureka-server上
    eureka.client.service-url.defaultZone=http://localhost:9100/eureka
               
    注意:

    zuul.routes.api-zuul.path

    zuul.routes.api-zuul.serviceId

    中,配置的

    api-zuul

    是路由的名字,可以任意定義,但是一組

    path

    serviceId

    映射關系的路由名要相同。例如,也可以寫成:

    zuul.routes.zuul-url.path

    zuul.routes.zuul-url.serviceId

    等。

    以上配置,我們的路由規則就是比對所有符合

    /api-zuul/**

    的請求,隻要路徑中帶有

    /api- zuul /

    都将被轉發到

    zuul-consumer-api-zuul

    服務上,至于

    zuul-consumer-api-zuul

    服務的位址到底是什麼,則由

    eureka-server

    注冊中心去分析,我們隻需要寫上服務名即可。
  4. 測試,從浏覽器通路網關

    http://localhost:8083/api-zuul/test

    ,則相當于通路了

    http://zuul-consumer-api-zuul/test

    zuul-consumer-api-zuul

    是服務名)。

3. 路由規則

(1)配置路由規則

前面講到的路由配置如下:

#配置路由規則
zuul.routes.api-zuul.path=/api-zuul/**
zuul.routes.api-zuul.serviceId=zuul-consumer-api-zuul
           

當通路位址符合

/api-zuul/**

規則的時候,會被自動定位到

zuul-consumer-api-zuul

服務上,不過兩行代碼有點麻煩,還可以簡化為:

zuul.routes

後面跟着的是服務名,服務名後面跟着的是路徑規則,這種配置方式更簡單。

(2)忽略某些路徑

如果我們不希望攔截某些請求,比如說我不需要攔截

/hello

接口路由,那麼可以按如下方式配置:

#過濾某些請求路徑,過濾掉之後,API網關不會對這些請求進行任何的攔截處理
zuul.ignored-patterns=/**/hello/**
           

(3)路由增加統一字首

我們可以統一的為路由規則增加字首,設定方式如下:

#為API網關添加統一通路字首
zuul.prefix=/myapi
           

此時我們的通路路徑就變成了

http://localhost:8080/myapi/api-zuul/test

(4)路由規則通配符

通配符 含義 舉例 說明
比對任意單個字元 /xxx/? 比對 /xxx/a, /xxx/b, /xxx/c等
* 比對任意數量的字元 /xxx/* 比對 /xxx/aaa,/xxx/bbb,/xxx/ccc等,無法比對 /xxx/a/b/c
** 比對任意數量的字元 /xxx/** 比對 /xxx/aaa,/xxx/bbb,/xxx/ccc等,也可以比對 /xxx/a/b/c

(5)請求到達

API

網關後,再轉發給自己本身

一般情況下

API

網關隻是作為各個微服務的統一入口,但是有時候我們可能也需要在

API

網關服務上做一些特殊的業務邏輯處理,那麼我們可以讓請求到達

API

網關後,再轉發給自己本身,由

API

網關自己來處理,那麼我們可以進行如下的操作:

  1. 首先在 Zuul 中建立一個 Controller:
    @RestController
    public class ApiController {
    
        @RequestMapping("/apitest")
        public String apiTest() {
            return "這是api網關處理的請求";
        }
    }
               
  2. 在 application.properties 中進行配置:
    # 攔截以 /geteway 開頭的任意請求并轉發到目前Api網關自身中的某個Controller中,而不是轉發給其他的服務
    zuul.routes.gateway.path=/gateway/**
    # Api自身處理請求的Controller路徑。
    # 路徑http://localhost:8083/gateway/apitest相當于http://localhost:8083/apitest
    # 如果zuul.routes.gateway.url設定成了:forward:/apitest,則請求路徑為http://localhost:8083/gateway,相當于http://localhost:8083/apitest
    zuul.routes.gateway.url=forward:/
               
  3. 測試:通過通路

    http://localhost:8083/gateway/apitest

    即可通路到API内部的業務邏輯處理

4. 請求過濾

Spring cloud Zuul就像一個安檢站,所有請求都會經過這個安檢站,是以我們可以在該安檢站内實作對請求的過濾。

  • 定義一個過濾器類并繼承自

    ZuulFilter

    ,并将該

    Filter

    作為一個

    Bean

    ZuulFilter

    類中重要的四個方法:
    • filterType

      方法的傳回值為

      String

      類型,表示過濾器的類型,過濾器的類型決定了過濾器在哪個生命周期執行,

      pre

      表示在路由之前執行過濾器,其他值還有

      post

      error

      route

      static

      ,當然也可以自定義。
    • filterOrder

      方法表示過濾器的執行順序,當過濾器很多時,我們可以通過該方法的傳回值來指定過濾器的執行順序。

      filterType

      相同時,數字越小,越先執行,可以是負數。
    • shouldFilter

      方法用來判斷過濾器是否執行,

      true

      表示執行,

      false

      表示不執行。
    • run

      方法則表示過濾的具體邏輯,如果請求位址中攜帶了

      token

      參數的話,則認為是合法請求,否則為非法請求,如果是非法請求的話,首先設定

      setSendZuulResponse(false)

      ,表示不對該請求進行路由,然後設定響應碼和響應值。這個run方法的傳回值目前暫時沒有任何意義,可以傳回任意值。
//自定義請求的網關過濾器
@Component
public class AuthFilter extends ZuulFilter {
    //定義目前過濾器的類型 pre表示在執行請求之前攔截過濾
    public String filterType() {
        return "pre";
    }

    //過濾器的排序,如果有多個按照傳回值大小依次執行
    public int filterOrder() {
        return 0;
    }

    //是否啟動目前過濾器 傳回true表示啟用
    public boolean shouldFilter() {
        return true;
    }

    //具體的請求攔截方法 傳回值沒有意義
    public Object run() throws ZuulException {
        //定義并擷取請求上下文對象, 這個對象來自Zuul的依賴包
        RequestContext requestContext = RequestContext.getCurrentContext();
        //擷取使用者請求對象
        HttpServletRequest request = requestContext.getRequest();
        //擷取請求身份令牌
        String token = request.getParameter("token");
        if (!"123".equals(token)) {//進入if表示目前請求沒有身份令牌或令牌錯誤,表示請求非法
            //設定false表示請求非法,不會繼續執行請求,而是立即傳回
            requestContext.setSendZuulResponse(false);
            //設定響應狀态碼為401,表示請求非法
            requestContext.setResponseStatusCode(401);
            //設定為使用者傳回響應資料的編碼格式
            requestContext.addZuulResponseHeader("content-type", "text/html;charset=utf-8");
            //設定為使用者傳回的具體響應資訊
            requestContext.setResponseBody("使用者請求非法");
        } else {
            System.out.println("請求合法可以通路---------------繼續通路服務提供者");
        }
        return null;
    }
}
           

5. 異常處理

Spring Cloud Zuul 對異常的處理是非常友善的,但是由于Spring Cloud處于迅速發展中,各個版本之間有所差異,本案例是以Hoxton.RC2

版本為例,來說明Spring Cloud Zuul中的異常處理問題。

首先我們來看一張官方給出的Zuul請求的生命周期圖:

SpringCloud微服務基礎二(Hystrix、Feign和Zuul)寫在前面一、服務熔斷Hystrix二、聲明式服務消費Feign三、API網關Zuul

在請求生命周期中,存在四種标準的

Filter

類型:

  • pre filter

    :在請求路由到目标之前執行。一般用于請求認證、負載均衡和日志記錄。
  • routing filter

    :處理目标請求。這裡使用Apache HttpClient或Netflix Ribbon構造對目标的HTTP請求。
  • post filter

    :在目标請求傳回後執行。一般會在此步驟添加響應頭、收集統計和性能資料等。
  • errot filter

    :整個流程某塊出錯時執行。

他們之間的關系:

  1. 正常情況下所有的請求都是按照

    pre

    route

    post

    的順序來執行,然後由

    post

    傳回

    response

  2. pre

    階段,如果有自定義的過濾器則執行自定義的過濾器
  3. pre

    routing

    post

    的任意一個階段如果抛異常了,則執行

    error

    過濾器

對于

error

過濾器,我們可以有兩種方式統一處理異常:

  1. 禁用

    zuul

    預設的異常處理

    SendErrorFilter

    過濾器,然後自定義我們自己的

    error filter

    過濾器
  2. 自定義全局

    error

    錯誤頁面

注意:自定義異常的過濾器和全局

error

錯誤頁面有沖突,二選一即可

(1)自定義Error過濾器

  1. 禁用 Zuul 的預設

    error

    過濾器,然後才能自定義異常過濾器類
  2. 自定義Error過濾器
    @Component
    public class ErrorFilter extends ZuulFilter {
        private static final Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
    
    	//指定過濾器類型
        @Override
        public String filterType() {
            return "error";
        }
        @Override
        public int filterOrder() {
            return 1;
        }
        @Override
        public boolean shouldFilter() {
            return true;
        }
        @Override
        public Object run() throws ZuulException {
            try {
                RequestContext context = RequestContext.getCurrentContext();
                ZuulException exception = (ZuulException)context.getThrowable();
                logger.error("進入系統異常攔截", exception);
                HttpServletResponse response = context.getResponse();
                response.setContentType("application/json; charset=utf8");
    
    			//exception.nStatusCode 擷取狀态碼
                response.setStatus(exception.nStatusCode);
                PrintWriter writer = null;
                try {
                    writer = response.getWriter();
    
    				//exception.getMessage擷取異常資訊,exception.errorCause擷取異常原因
                    writer.print("{code:"+ exception.nStatusCode +",message:\""+ exception.getMessage() +"\"}");
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if(writer!=null){
                        writer.close();
                    }
                }
            } catch (Exception var5) {
                ReflectionUtils.rethrowRuntimeException(var5);
            }
            return null;
        }
    }
               
  3. 測試,當之前自定義的

    pre

    過濾器(AuthFilter )出現異常時,便會執行

    error

    過濾器

(2)自定義全局error錯誤頁面

自定義一個

Controller

實作異常控制器的父接口

ErrorController

,并實作

getErrorPath()

方法,該方法的傳回值是一個

String

類型的請求位址,在這個請求位址中進行異常處理。當程式抛出異常之後,将會執行對應的請求路徑。

注意:Zuul 的預設

error

過濾器需要打開(上一個方法是需要關閉的),預設是打開的,或者使用下面的設定

小例子:

//自定義異常控制器,并實作異常控制器的父接口,這個接口是SpringBoot提供的
@RestController
public class MyErrorController implements ErrorController {
    //如果目前工程抛出異常以後,則利用這個方法的傳回值所對應的請求來顯示錯誤資訊
    @Override
    public String getErrorPath() {
        return "/error";
    }

    //在這個請求位址中進行異常處理
    @RequestMapping("/error")
    public String error() {
        //全局異常頁面可以使用Json的方式顯示錯誤資訊,也可以利用Thymeleaf顯示一個真正的html頁面
        return "這是一個全局異常頁面";
    }
}