1.概述簡介
Gateway官方文檔:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/
在Spring Cloud 1.x中,網關使用的是Zuul 1,在Spring Cloud 2.x中,網關使用的是Gateway,因爲Zuul 2版本進展緩慢,所以Spring Cloud自己研發了網關,Gateway是原Zuul 1的替代版。Gateway採用異步非阻塞模型開發,性能上不需要擔心,雖然Netflix發佈了Zuul 2版本,但是Spring Cloud並沒有整合的計劃,所以才自己推出了Gateway的方案。
Spring Cloud Gateway是Spring Cloud的一個全新項目,基於Spring 5+Spring Boot 2.x+Project Reactor等技術開發的網關,旨在爲微服務架構提供一種簡單有效的統一API路由管理方式。
Spring Cloud Gateway作爲Spring Cloud生態系統中的網關,目標是替代Zuul 1,在Spring Cloud 2.x版本中,沒有對新版本Zuul 2最新高性能版本進行集成,仍然使用的Zuul 1非Reactor模式的老版本,爲了提升網關性能,Spring Cloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway目標是提供統一的路由方式,並且基於Filter鏈方式提供網關基本功能:安全、監控、指標、限流。
網關的作用:反向代理、鑑權、流量控制、熔斷、日誌監控等。
在微服務架構中,網關的位置,位於各個微服務的上一層,通過網關後,就訪問到了具體的微服務了。
Spring Cloud Gateway特性:
- 基於Spring Framework 5+Project Reactor+Spring Boot 2.x進行構建
- 動態路由:能夠匹配任何請求屬性
- 支持路由指定Predicate(斷言)和Filter(過濾器),而且編寫容易
- 集成了Hystrix斷路器功能
- 集成Spring Cloud服務發現功能
- 支持請求限流功能
- 支持路徑重寫功能
Spring Cloud Gateway和Zuul的區別:
- Zuul 1採用的是阻塞I/O的API網關
- Zuul 1使用Servlet2.5的阻塞架構實現,不支持任何長連接(如WebSocket),Zuul的設計模式和Nginx比較像,每次I/O都從工作線程中選擇一個執行,請求線程在工作線程完成之前一直是阻塞的,Nginx是C++實現,Zuul是Java實現,JVM在第一次的加載時候,會比較慢,所以Zuul的性能比較差
- Zuul 2設計理念跟先進,基於Netty非阻塞和支持長連接,但是目前Spring Cloud沒有整合它,Zuul 2的性能比Zuul 1性能有較大提升,Spring Cloud Gateway的性能也不錯,官方測試數據表示,Spring Cloud Gateway的RPS(每秒請求數)是Zuul 1的1.6倍
- Spring Cloud Gateway建立在Spring Framework 5+Project Reactor+Spring Boot 2.x之上,使用非阻塞API
- Spring Cloud Gateway還支持WebSocket,與Spring緊密集成擁有更好的開發體驗
Zuul 1模型缺點:Zuul 1採用傳統Servlet I/O處理模型,但請求進入servlet容器時,servlet容器會爲其綁定一個線程,在併發不高的情況下是適用的,當併發量增加,線程數就會增加,但是線程資源是非常昂貴的(線程上下文切換內存消耗大),會影響到請求處理時間。有些簡單的業務場景,並不需要每個請求分配一個線程,簡單業務的高併發下,實際可能只需要幾個線程就能扛得住,因此,每個請求分配一個線程,在高併發環境下並沒有優勢。
Gateway模型:Gateway模型採用的是WebFlux框架,這個框架是一個典型的非阻塞異步的框架,並且在Servlet 3.1後,支持了異步非阻塞,框架的核心是基於Reactor相關API實現的。相對於傳統Web框架,它可以運行在支持Servlet 3.1容器上的組件中(如Netty、Undertow等)。Spring WebFlux是Spring 5引入的新的響應式框架,區別於Spring MVC,不需要依賴Servlet API,完全異步非阻塞,並且基於Reactor來實現響應式流規範。
Spring WebFlux可以參考官網:https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html
2.三大核心概念
Route(路由)
路由是構建網關的基本模塊,由ID,目標URI一系列斷言和過濾器組成,如果斷言爲true,則匹配該路由。
Predicate(斷言)
參考Java8的java.util.function.Predicate,開發人員可以匹配HTTP請求中的所有內容(比如請求頭,請求參數等),如果請求與斷言匹配,則進行路由。
Filter(過濾)
類似於Web開發中的過濾器,這裏指的是Spring框架中GatewayFilter實例,使用過濾器可以在請求被路由之前或之後對請求進行修改。
總結
路由的功能是由斷言和過濾組合來實現的,一個Web請求發送後,先經過網關,網關裏的斷言和過濾用來判斷這個請求是否需要路由轉發,當斷言爲true,過濾器放行時候,這個請求進行路由轉發,此時,請求才到達具體的微服務模塊。
3.Gateway工作流程
客戶端向Spring Cloud Gateway發送請求,然後在Gateway Handler Mapping中找到與請求相匹配的路由,將其Web Handler。Handler通過指定的過濾器鏈將請求發送到實際服務,執行業務邏輯,然後返回。如果有post類型的過濾器,執行過濾器邏輯。
pre類型的過濾器可以做參數校驗、權限校驗、流量監控、日誌輸出、協議轉換等功能。
post類型的過濾器可以做響應內容、響應頭的修改、日誌輸出、流量監控等功能。
4.入門配置
創建cloud-gateway-gateway9527模塊,修改pom.xml,加入spring-cloud-starter-gateway和spring-cloud-starter-netflix-eureka-client的座標。
<?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">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-gateway-gateway9527</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
添加application.yml配置文件。
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true # true:將自己註冊進Eureka
fetch-registry: true # true:需要去註冊中心獲取其他服務地址
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
添加主啓動類。
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class, args);
}
}
通過yml給Gateway添加路由規則。
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
# - 是yml語法,代表數組的意思
- id: payment_route1 # 路由ID,沒有固定規則,需要保證唯一
uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
predicates:
- Path=/payment/get/** # 路徑匹配斷言
- id: payment_route2 # 路由ID,沒有固定規則,需要保證唯一
uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
predicates:
- Path=/payment/loadbalance # 路徑匹配斷言
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true # true:將自己註冊進Eureka
fetch-registry: true # true:需要去註冊中心獲取其他服務地址
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
相比之前的yml,這裏的yml多了Gateway結點,配置這個的目的在於隱藏localhost:8001的地址,如果想訪問payment8001服務的/payment/get/**地址,之前需要訪問http://localhost:8001/payment/get/1,配置了路由之後,訪問http://localhost:9527/payment/get/1也可以實現,於是,原來的8001端口就隱藏了。
配置路由有兩種方式,一種是上面的yml配置文件,另一種是是編碼方式配置,uri中記得帶http,否則訪問不到,這裏的uri可以寫任意地址,代表一個路由規則,訪問path路徑的時候,被路由到uri地址。
package com.atguigu.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes()
// 當訪問http://localhost:9527/payment/get/**的時候,請求會被路由到http://localhost:8001/payment/get/**
.route("payment_route1", r -> r.path("/payment/get/**").uri("http://localhost:8001/payment/get/**"))
// 當訪問http://localhost:9527/payment/loadbalance的時候,請求會被路由到http://localhost:8001/payment/loadbalance
.route("payment_route2", r -> r.path("/payment/loadbalance").uri("http://localhost:8001/payment/loadbalance"))
.build();
}
}
5.通過微服務名實現動態路由
現在存在的問題:真實服務地址寫死了,我們應該通過服務名來找服務,而不是通過服務地址找服務。
默認情況下,Gateway會根據註冊中心註冊的服務列表,以註冊中心上微服務名爲路徑創建動態路由轉發,從而實現動態路由的功能。
爲了演示網關的負載均衡,修改Provider8001和Provider8002的yml配置文件,將它們的註冊地址改成eureka7001的地址。啓動Eureka7001和Provider8001和Provider8002服務。修改Gateway9527模塊的application.yml配置文件,修改爲如下內容。因爲要修改application.yml,所以先把GatewayConfig類進行屏蔽,避免產生影響。
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
# - 是yml語法,代表數組的意思
- id: payment_route1 # 路由ID,沒有固定規則,需要保證唯一
# uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
uri: lb://cloud-payment-service # 使用微服務名稱查找路由地址,lb是用於識別負載均衡的前綴,不能修改
predicates:
- Path=/payment/get/** # 路徑匹配斷言
- id: payment_route2 # 路由ID,沒有固定規則,需要保證唯一
# uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
uri: lb://cloud-payment-service # 使用微服務名稱查找路由地址,lb是用於識別負載均衡的前綴,不能修改
predicates:
- Path=/payment/loadbalance # 路徑匹配斷言
discovery:
locator:
enabled: true # 開啓從註冊中心動態創建路由的功能,利用微服務名稱進行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true # true:將自己註冊進Eureka
fetch-registry: true # true:需要去註冊中心獲取其他服務地址
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
通過瀏覽器訪問http://localhost:9527/payment/loadbalance,在頁面端可以看到端口號的不斷變化,如果要實現負載均衡,注意uri中的lb是固定的,它是識別開啓負載均衡的標誌。
6.Predicate的使用
在Gateway9527啓動的時候,在Console可以看到如下內容,代表執行加載Predicate,我們目前使用的Predicate是Path。
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [After]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Before]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Between]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Cookie]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Header]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Host]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Method]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Path]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Query]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [ReadBodyPredicateFactory]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [RemoteAddr]
2020-06-17 07:31:15.501 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Weight]
2020-06-17 07:31:15.502 INFO 10848 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [CloudFoundryRouteService]
Spring Cloud Gateway將路由匹配作爲Spring WebFlux HandlerMapping基礎架構的一部分。
Spring Cloud Gateway包括許多內置的Route Predicate Factory,這些Predicate鬥魚HTTP請求的不同屬性匹配,多個Route Predicate Factory可以進行組合。
Spring Cloud Gateway創建Route對象時,使用Route Predicate Factory創建Predicate對象,Predicate對象可以賦值給Route。
根據官方文檔裏的說明,在application.yml裏配置上即可生效,這相當於加了一個條件,條件滿足就做路由轉發,不滿足就不路由轉發。
簡單說下吧,After Route Predicate、Before Route Predicate、Between Route Predicate需要用到一個DateTime值,這個值要使用ZonedDateTime類來獲取。Cookie Route Predicate、Header Route Predicate、Host Route Predicate的測試使用cmd來測試,通過發送curl命令即可完成,具體可以看一下curl命令的參數介紹。
測試Cookie Route Predicate
curl http://localhost:9527/payment/loadbalance --cookie "username=wangshaoyang"
測試Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "X-Request-Id:123"
測試Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "Host: www.atguigu.com"
Method Route Predicate是根據GET或POST做斷言,Path Route Predicate就是我們最初使用的方式,Query Route Predicate是根據請求裏所帶Query參數進行判斷的,RemoteAddr Route Predicate是對請求發起ip做的校驗,Weight Route Predicate是給負載均衡分配權重的,可以指定請求發到某一個uri的權重。
所以說,Predicate可以提供一組匹配規則,每當一個請求過來的時候,去檢驗這些規則,如果滿足規則,進行路由,否則,就提示出錯。
7.Filter的使用
路由過濾器可用於修改進入的HTTP請求和返回的HTTP響應,路由過濾器只能指定路由進行使用,Spring Cloud Gateway內置了多種路由過濾器,它們是通過Gateway Filter Factory產生的。
從生命週期來分類,可以分爲Pre和Post,類似於Spring AOP的前置通知和後置通知。從種類上來分類,可以分爲Gateway Filter和Global Filter。具體可以參考官網,這個的配置也是在application.yml里加配置即可。
Gateway Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#gatewayfilter-factories
Global Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#global-filters
下面來介紹自定義全局過濾器,我們可以根據業務需求做定製化處理。
package com.atguigu.springcloud.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 獲取Request,獲取參數
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
System.out.println(queryParams);
// 這裏可以對queryParams裏的值做一些校驗
if ("".equals(queryParams.getFirst("username"))) {
System.out.println("非法用戶");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
// 表示該請求已經處理完成
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 過濾器的優先級,值越小優先級越高
*/
@Override
public int getOrder() {
return 0;
}
}