天天看點

Spring Cloud Gateway核心概念和工作原理

​​Spring Cloud​​​ Gateway 是 ​​Spring​​​ 官方基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技術開發的網關,Spring Cloud Gateway 旨在為微服務架構提供一種簡單有效的、統一的 API 路由管理方式。

Spring Cloud Gateway 作為 Spring Cloud 生态系中的網關,其目标是替代 Netflix Zuul,它不僅提供統一的路由方式,并且基于 Filter 鍊的方式提供了網關基本的功能,例如:安全、監控/埋點和限流等。

Spring Cloud Gateway 依賴 Spring Boot 和 Spring WebFlux,基于 Netty 運作。它不能在傳統的 servlet 容器中工作,也不能建構成 war 包。

在 Spring Cloud Gateway 中有如下幾個核心概念需要我們了解:

1)Route

Route 是網關的基礎元素,由 ID、目标 URI、斷言、過濾器組成。當請求到達網關時,由 Gateway Handler Mapping 通過斷言進行路由比對(Mapping),當斷言為真時,比對到路由。

2)Predicate

Predicate 是 ​​Java​​ 8 中提供的一個函數。輸入類型是 Spring Framework ServerWebExchange。它允許開發人員比對來自 HTTP 的請求,例如請求頭或者請求參數。簡單來說它就是比對條件。

3)Filter

Filter 是 Gateway 中的過濾器,可以在請求發出前後進行一些業務上的處理。

Spring Cloud Gateway 工作原理

Spring Cloud Gateway 的工作原理跟 Zuul 的差不多,最大的差別就是 Gateway 的 Filter 隻有 pre 和 post 兩種。下面我們簡單了解一下 Gateway 的工作原理圖,如圖 1 所示。

Spring Cloud Gateway核心概念和工作原理

圖 1  Spring Cloud Gateway 工作原理

用戶端向 Spring Cloud Gateway 送出請求,如果請求與網關程式定義的路由比對,則該請求就會被發送到網關 Web 處理程式,此時處理程式運作特定的請求過濾器鍊。

過濾器之間用虛線分開的原因是過濾器可能會在發送代理請求的前後執行邏輯。所有 pre 過濾器邏輯先執行,然後執行代理請求;代理請求完成後,執行 post 過濾器邏輯。

Spring Cloud Gateway整合Eureka路由轉發

先建立一個 Gateway 項目,然後實作了一個最簡單的轉發功能,并進行 Eureka 路由的整合。

建立 Gateway 項目

建立一個 ​​Spring​​​ Boot 的 ​​Maven​​​ 項目,增加 ​​Spring Cloud​​ Gateway 的依賴,代碼如下所示。

org.springframework.boot
    spring-boot-starter-parent
    2.0.6.RELEASE
    



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            Finchley.SR2
            pom
            import
        
    


    
        org.springframework.cloud
        spring-cloud-starter-gateway
    
      

啟動類就按 Spring Boot 的方式即可,無須添加額外的注解。代碼如下所示。

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

路由轉發示例

下面來實作一個最簡單的轉發功能——基于 Path 的比對轉發功能。

Gateway 的路由配置對 yml 檔案支援比較好,我們在 resources 下建一個 application.yml 的檔案,内容如下:

server:
  port: 2001
spring:
  cloud:
    gateway:
      routes:
        - id: path_route
uri: http://c.biancheng.net
predicates:
  - Path=/spring_cloud      

當你通路 http://localhost:2001/spring_cloud 的時候就會轉發到 http://c.biancheng.net/spring_cloud。

如果我們要支援多級 Path,配置方式跟 Zuul 中一樣,在後面加上兩個​​

​*​

​号即可,比如:

- id: path_route2
uri: http://c.biancheng.net
predicates:
  - Path=/spring_cloud/**      

這樣一來,上面的配置就可以支援多級 Path,比如通路 http://localhost:2001/spring_cloud/view/1 的時候就會轉發到 http://c.biancheng.net/spring_cloud/view/1。

整合 Eureka 路由

添加 Eureka Client 的依賴,代碼如下所示。

org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client
      

配置基于 Eureka 的路由:

- id: user-service
uri: lb://user-service
predicates:
  - Path=/user-service/**      

uri 以​

​lb://​

​開頭(lb 代表從注冊中心擷取服務),後面接的就是你需要轉發到的服務名稱,這個服務名稱必須跟 Eureka 中的對應,否則會找不到服務,錯誤代碼如下:

org.springframework.cloud.gateway.support.NotFoundException: Unable to find instance for user-service1      

整合 Eureka 的預設路由

Zuul 預設會為所有服務都進行轉發操作,我們隻需要在通路路徑上指定要通路的服務即可,通過這種方式就不用為每個服務都去配置轉發規則,當新加了服務的時候,不用去配置路由規則和重新開機網關。

在 Spring Cloud Gateway 中當然也有這樣的功能,通過配置即可開啟,配置如下:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true      

開啟之後我們就可以通過位址去通路服務了,格式如下:

http://網關位址/服務名稱(大寫)/**
http://localhost:2001/USER-SERVICE/user/get?id=1      

這個大寫的名稱還是有很大的影響,如果我們從 Zuul 更新到 Spring Cloud Gateway 的話意味着請求位址有改變,或者重新配置每個服務的路由位址,通過源碼發現可以做到相容處理,再增加一個配置即可:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          lowerCaseServiceId: true      

配置完成之後我們就可以通過小寫的服務名稱進行通路了,如下所示:

http://網關位址/服務名稱(小寫)/**
http://localhost:2001/user-service/user/get?id=1      

注意:開啟小寫服務名稱後大寫的服務名稱就不能使用,兩者隻能選其一。

配置源碼在 org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties 類中,代碼所示。

@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
public class DiscoveryLocatorProperties {
    /**
     * 服務名稱小寫配置, 預設為false
     *
     */
    private boolean lowerCaseServiceId = false;
}      

Spring Cloud Gateway的常用路由斷言工廠

​​Spring Cloud​​ Gateway 内置了許多路由斷言工廠,可以通過配置的方式直接使用,也可以組合使用多個路由斷言工廠。接下來為大家介紹幾個常用的路由斷言工廠類。

1)Path 路由斷言工廠

Path 路由斷言工廠接收一個參數,根據 Path 定義好的規則來判斷通路的 URI 是否比對。

spring:
  cloud:
    gateway:
      routes:
        - id: host_route
    uri: http://c.biancheng.net
    predicates:
      - Path=/blog/detail/{segment}      

如果請求路徑為 /blog/detail/xxx,則此路由将比對。也可以使用正則,例如 /blog/detail/** 來比對 /blog/detail/ 開頭的多級 URI。

我們通路本地的網關:http://localhost:2001/blog/detail/36185 ,可以看到顯示的是 http://c.biancheng.net/blog/detail/36185 對應的内容。

2)Query 路由斷言工廠

Query 路由斷言工廠接收兩個參數,一個必需的參數和一個可選的正規表達式。

spring:
  cloud:
    gateway:
      routes:
        - id: query_route
      uri: http://c.biancheng.net
      predicates:
        - Query=foo, ba.      

如果請求包含一個值與 ba 比對的 foo 查詢參數,則此路由将比對。bar 和 baz 也會比對,因為第二個參數是正規表達式。

測試連結:http://localhost:2001/?foo=baz。

3)Method 路由斷言工廠

Method 路由斷言工廠接收一個參數,即要比對的 HTTP 方法。

spring:
  cloud:
    gateway:
      routes:
        - id: method_route
  uri: http://baidu.com
  predicates:
    - Method=GET      

4)Header 路由斷言工廠

Header 路由斷言工廠接收兩個參數,分别是請求頭名稱和正規表達式。

spring:
  cloud:
    gateway:
      routes:
        - id: header_route
  uri: http://example.org
  predicates:
    - Header=X-Request-Id, \d+      

如果請求中帶有請求頭名為 x-request-id,其值與 \d+ 正規表達式比對(值為一個或多個數字),則此路由比對。

更多路由斷言工廠的用法,可以參考官方文檔進行學習。

自定義路由斷言工廠

自定義路由斷言工廠需要繼承 AbstractRoutePredicateFactory 類,重寫 apply 方法的邏輯。

在 apply 方法中可以通過 exchange.getRequest() 拿到 ServerHttpRequest 對象,進而可以擷取到請求的參數、請求方式、請求頭等資訊。

apply 方法的參數是自定義的配置類,在使用的時候配置參數,在 apply 方法中直接擷取使用。

命名需要以 RoutePredicateFactory 結尾,比如 CheckAuthRoutePredicateFactory,那麼在使用的時候 CheckAuth 就是這個路由斷言工廠的名稱。代碼如下所示。

@Component
public class CheckAuthRoutePredicateFactory
        extends AbstractRoutePredicateFactory {
    public CheckAuthRoutePredicateFactory() {
        super(Config.class);
    }
    @Override
    public Predicate apply(Config config) {
        return exchange -> {
            System.err.println("進入了CheckAuthRoutePredicateFactory\t" + config.getName());
            if (config.getName().equals("zhangsan")) {
                return true;
            }
            return false;
        };
    }
    public static class Config {
        private String name;
        public void setName(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
    }
}      

使用示例如下所示:

spring:
  cloud:
    gateway:
      routes:
  - id: customer_route
  uri: http://c.biancheng.net
  predicates:
    - name: CheckAuth
  args:
    name: zhangsan      

Spring Cloud Gateway過濾器工廠的使用

GatewayFilter Factory 是 ​​Spring Cloud​​​ Gateway 中提供的過濾器工廠。​​Spring​​​ Cloud Gateway 的路由過濾器允許以某種方式修改傳入的 HTTP 請求或輸出的 HTTP 響應,隻作用于特定的路由。

Spring Cloud Gateway 中内置了很多過濾器工廠,直接采用配置的方式使用即可,同時也支援自定義 GatewayFilter Factory 來實作更複雜的業務需求。

spring:
  cloud:
    gateway:
      routes:
        - id: add_request_header_route
  uri: http://c.biancheng.net
  filters:
    - AddRequestHeader=X-Request-Foo, Bar      

接下來為大家介紹幾個常用的過濾器工廠類。

1. AddRequestHeader 過濾器工廠

通過名稱我們可以快速明白這個過濾器工廠的作用是添加請求頭。

符合規則比對成功的請求,将添加 X-Request-Foo:bar 請求頭,将其傳遞到後端服務中,後方服務可以直接擷取請求頭資訊。代碼如下所示。

@GetMapping("/hello")
public String hello(HttpServletRequest request) throws Exception {
    System.err.println(request.getHeader("X-Request-Foo"));
    return "success";
}      

2. RemoveRequestHeader 過濾器工廠

RemoveRequestHeader 是移除請求頭的過濾器工廠,可以在請求轉發到後端服務之前進行 Header 的移除操作。

spring:
  cloud:
    gateway:
      routes:
  - id: removerequestheader_route
  uri: http://c.biancheng.net
    - RemoveRequestHeader=X-Request-Foo      

3. SetStatus 過濾器工廠

SetStatus 過濾器工廠接收單個狀态,用于設定 Http 請求的響應碼。它必須是有效的 Spring Httpstatus(org.springframework.http.HttpStatus)。它可以是整數值 404 或枚舉類型 NOT_FOUND。

spring:
  cloud:
    gateway:
      routes:
        - id: setstatusint_route
  uri: http://c.biancheng.net
  filters:
    - SetStatus=401      

4. RedirectTo過濾器工廠

RedirectTo 過濾器工廠用于重定向操作,比如我們需要重定向到百度。

spring:
  cloud:
    gateway:
      routes:
        - id: prefixpath_route
  uri: http://c.biancheng.net
  filters:
    - RedirectTo=302, http://baidu.com      

以上為大家介紹了幾個過濾器工廠的使用,後面還會為大家介紹 Retry 重試、RequestRateLimiter 限流、Hystrix 熔斷過濾器工廠等内容,其他的大家可以自行參考官方文檔進行學習。

自定義Spring Cloud Gateway過濾器工廠

自定義 Spring Cloud Gateway 過濾器工廠需要繼承 AbstractGatewayFilterFactory 類,重寫 apply 方法的邏輯。命名需要以 GatewayFilterFactory 結尾,比如 CheckAuthGatewayFilterFactory,那麼在使用的時候 CheckAuth 就是這個過濾器工廠的名稱。

自定義過濾器工廠代碼如下所示。

@Component
public class CheckAuth2GatewayFilterFactory
        extends AbstractGatewayFilterFactory {
    public CheckAuth2GatewayFilterFactory() {
        super(Config.class);
    }
    @Override
    public GatewayFilter apply(Config config) {
    return (exchange, chain) -> {
      System.err.println("進入了CheckAuth2GatewayFilterFactory" + config.getName());
      ServerHttpRequest request = exchange.getRequest().mutate()
      .build();
      return
      chain.filter(exchange.mutate().request(request).build());
    }
  }
    public static class Config {
        private String name;
        public void setName(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
    }
}      

使用如下:

filters:
  - name: CheckAuth2
  args:
    name: 張三      

如果你的配置是 Key、Value 這種形式的,那麼可以不用自己定義配置類,直接繼承 AbstractNameValueGatewayFilterFactory 類即可。

AbstractNameValueGatewayFilterFactory 類繼承了 AbstractGatewayFilterFactory,定義了一個 NameValueConfig 配置類,NameValueConfig 中有 name 和 value 兩個字段。

我們可以直接使用,AddRequestHeaderGatewayFilterFactory、AddRequestParameterGatewayFilterFactory 等都是直接繼承的 AbstractNameValueGatewayFilterFactory。

繼承 AbstractNameValueGatewayFilterFactory 方式定義過濾器工廠,代碼如下所示。

@Component
public class CheckAuthGatewayFilterFactory extends AbstractNameValueGatewayFilter-actory {
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return (exchange, chain) -> {
            System.err.println("進入了CheckAuthGatewayFilterFactory" + config.getName() + "\t" + config.getValue());
            ServerHttpRequest request = exchange.getRequest().mutate().build();
            return chain.filter(exchange.mutate().request(request).build());
        };
    }
}      

使用如下:

filters:
        - CheckAuth=zhangsan,男      

Spring Cloud Gateway全局過濾器(GlobalFilter)

全局過濾器作用于所有的路由,不需要單獨配置,我們可以用它來實作很多統一化處理的業務需求,比如權限認證、IP 通路限制等。

接口定義類 org.springframework.cloud.gateway.filter.GlobalFilter,具體代碼如下所示。

public interface GlobalFilter {
    Mono filter(ServerWebExchange exchange, GatewayFilterChain chain);
}      

​​Spring Cloud​​ Gateway 自帶的 GlobalFilter 實作類有很多,如圖 1 所示。

Spring Cloud Gateway核心概念和工作原理

 圖 1  架構自帶全局過濾器

有轉發、路由、負載等相關的 GlobalFilter,感興趣的朋友可以去看下源碼自行了解。我們如何通過定義 GlobalFilter 來實作我們的業務邏輯?

這裡給出一個官方文檔上的案例,代碼如下所示。

@Configuration
public class ExampleConfiguration {
    private Logger log = LoggerFactory.getLogger(ExampleConfiguration.class);
    @Bean
    @Order(-1)
    public GlobalFilter a() {
        return (exchange, chain) -> {
            log.info("first pre filter");
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("third post filter");
            }));
        };
    }
    @Bean
    @Order(0)
    public GlobalFilter b() {
        return (exchange, chain) -> {
            log.info("second pre filter");
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("second post filter");
            }));
        };
    }
    @Bean
    @Order(1)
    public GlobalFilter c() {
        return (exchange, chain) -> {
            log.info("third pre filter");
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("first post filter");
            }));
        };
    }
}      
2021-8-26 16:08:52.406  INFO 55062 --- [ioEventLoop-4-1] c.c.gateway.config.ExampleConfiguration  : first pre filter
2021-8-26 16:08:52.406  INFO 55062 --- [ioEventLoop-4-1] c.c.gateway.config.ExampleConfiguration  : second pre filter
2021-8-26 16:08:52.407  INFO 55062 --- [ioEventLoop-4-1] c.c.gateway.config.ExampleConfiguration  : third pre filter
2021-8-26 16:08:52.437  INFO 55062 --- [ctor-http-nio-7] c.c.gateway.config.ExampleConfiguration  : first post filter
2021-8-26 16:08:52.438  INFO 55062 --- [ctor-http-nio-7] c.c.gateway.config.ExampleConfiguration  : second post filter
2021-8-26 16:08:52.438  INFO 55062 --- [ctor-http-nio-7] c.c.gateway.config.ExampleConfiguration  : third post filter      
@Component
public class IPCheckFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        return 0;
    }
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        // 此處寫得非常絕對, 隻作示範用, 實際中需要采取配置的方式
        if (getIp(headers).equals("127.0.0.1")) {
            ServerHttpResponse response = exchange.getResponse();
            ResponseData data = new ResponseData();
            data.setCode(401);
            data.setMessage("非法請求");
            byte[] datas = JsonUtils.toJson(data).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(datas);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            return response.writeWith(Mono.just(buffer));
        }
        return chain.filter(exchange);
    }
    // 這裡從請求頭中擷取使用者的實際IP,根據Nginx轉發的請求頭擷取
    private String getIp(HttpHeaders headers) {
        return "127.0.0.1";
    }
}