Spring Cloud Gateway 深入

矢澤妮可發表於2018-12-10

Spring Cloud Gateway介紹

廢話不多說,看官方文件的介紹

This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

有道翻譯一下:

這個專案提供了一個建在Spring生態系統之上的API閘道器,包括:Spring 5, Spring Boot 2和project Reactor。Spring Cloud Gateway旨在提供一種簡單而有效的方式來路由到api,併為它們提供交叉關注,例如:安全性、監視/度量和彈性。

工作原理如下:

image

Gateway實際上提供了一個在路由上的控制功能,大體包含兩個大功能:

  • Route
  • Filter
  • Forward

我們可以通過Route去匹配請求的uri,而每個Router下可以配置一個Filter Chain,我們可以通過Filter去修飾請求和響應及一些類似鑑權等中間動作,通過Forward可以控制重定向和請求轉發(實際上Filter也可以做到,只不過這裡分開說清晰一點)。在路由控制上,Gateway表現的非常靈活,它有兩種配置方式:

  • Yml or Properties File
  • Code

主要名詞如下:

  • Route: Route the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true.
  • Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This allows developers to match on anything from the HTTP request, such as headers or parameters.
  • Filter: These are instances Spring Framework GatewayFilter constructed in with a specific factory. Here, requests and responses can be modified before or after sending the downstream request.

Route 作為Gateway中的基本元素,它有自己的ID、URI和一個Predicate集合、Filter集合。Predicate的作用是判斷請求的Uri是否匹配當前的Route,Filter則是匹配通過之後對請求和響應的處理及修飾,那麼在Gateway中Route基本結構如下

Gateway{
    Route1 {
        String id;
        String path;
        List<Predicate> predicates;
        List<Filter> filters;
    };
    Route2 {
        String id;
        String path;
        List<Predicate> predicates;
        List<Filter> filters;
    };
    ...
    ...
}
複製程式碼

Route中的ID作為它的唯一標識,path的作用是正則匹配請求路徑,Predicate則是在path匹配的情況下進一步去更加細緻的匹配請求路徑,如一下例子:

spring:
  cloud:
    gateway:
      routes:
      - id: before_route
        uri: http://example.org
        predicates:
        - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
複製程式碼

只匹配在Jan 20, 2017 17:42發起的請求

spring:
  cloud:
    gateway:
      routes:
      - id: cookie_route
        uri: http://example.org
        predicates:
        - Cookie=chocolate, “”
複製程式碼

只匹配請求中攜帶chocolate且值為ch.p的請求

更多例子這裡就不一一展開,有興趣的可以去看下官方文件: 傳送門

Spring Cloud Gateway 配置

Maven

<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-gateway</artifactId>
				<version>2.0.2.BUILD-SNAPSHOT</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
		</dependency>
		<dependency>
		    <groupId>org.yaml</groupId>
		    <artifactId>snakeyaml</artifactId>
		</dependency>
	</dependencies>
	<repositories>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/libs-snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
	</repositories>
複製程式碼

Yml

spring:
  cloud:
    gateway:
      routes:
      - id: cookie_route
        uri: http://example.org
        predicates:
        - Cookie=chocolate, ch.p
複製程式碼

Java Config

@Configuration
@RestController
@SpringBootApplication
public class Application {

	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
				.route(r -> r.path("/request/**")
						.and()
						.predicate(new Predicate<ServerWebExchange>() {
							@Override
							public boolean test(ServerWebExchange t) {
								boolean access = t.getRequest().getCookies().get("_9755xjdesxxd_").get(0).getValue().equals("32");
								return access;
							}
						})
						.filters(f -> f.stripPrefix(2)
							.filter(new GatewayFilter() {
								@Override
								public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
									return chain.filter(exchange);
								}
							}, 2)
							.filter(new GatewayFilter() {
								@Override
								public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
									return chain.filter(exchange);
								}
							}, 1))
						.uri("http://localhost:8080/hello")
						).build();
	}
	
	@GetMapping("/hello")
	public String hello() {
		return "hello";
	}

	public static void main(String[] args) throws ClassNotFoundException {
		SpringApplication.run(Application.class, args);
	}

}
複製程式碼

Yml配置和Java程式碼配置可以共存,Yml配置的好處是可以直接用自帶的一些謂詞和Filter,而Java程式碼配置更加靈活!

Spring Cloud Gateway使用

上文已經說過,Gateway支援兩種配置,本文主要以Java Config的方式著重講解,因為官方文件中對於Yml配置的講解已經足夠深入,如有興趣可以進入傳送門

Route

一個Route的配置可以足夠簡單

@Configuration
@RestController
@SpringBootApplication
public class Application1 {

	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
				.route(r -> r.path("/user/**")
						.uri("http://localhost:8080/hello")
						).build();
	}
	
	@GetMapping("/hello")
	public String hello() {
		return "hello";
	}

	public static void main(String[] args) throws ClassNotFoundException {
		SpringApplication.run(Application1.class, args);
	}

}
複製程式碼

上述Demo定義了一個Route,並且path值為/**,意味著匹配多層uri,如果將path改為/*則意味著只能匹配一層。所以執行上面的程式,那麼所有的請求都將被轉發到http://localhost:8080/hello

如果uri的配置並沒有一個確定的資源,例如http://ip:port,那麼/**所匹配的路徑將會自動拼裝在uri之後:

request http://當前服務/user/1
forward http://ip:port/user/1
複製程式碼

這種方式更適合服務之間的轉發,我們可以將uri設定為ip:port也可以設定為xxx.com域名,但是不能自己轉發自己的服務,例如

request http://當前服務/user/1
forward http://當前服務/user/1
複製程式碼

這就導致了HTTP 413的錯誤,無限轉發至自己,也就意味著請求死鎖,非常致命!最好的方式如下:

我們擬定開兩個服務佔用的埠分別是80808081,我們假如要從8080服務通過/user/**路由匹配轉發至8081服務,可以這樣做:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
	return builder.routes()
			.route("hello", r -> r
					.path("/user/**")
					.and()
					.uri("http://localhost:8081")
					).build();
}
複製程式碼

工作跟蹤:

request http://localhost:8080/user/hello
forward http://localhost:8081/user/hello
複製程式碼

8081服務介面定義:

@GetMapping("/user/hello")
public String hello() {
	return "User Say Hello";
}
複製程式碼

Reponse Body:

User Say Hello
複製程式碼

當Gateway代替Zuul時,也就是說在服務間的通訊由Zuul轉換成Gateway之後,uri的寫法將會變成這個樣子:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
	return builder.routes()
			.route(r -> r.path("user/**")
					.uri("lb://USER_SERVER_NAME")
					).build();
}
複製程式碼

上述程式碼將user/**所匹配的請求全部轉發到USER_SERVER_NAME服務下:

request /user/1
forward http://USER_SERVER_HOST:USER_SERVER_PORT/user/1
複製程式碼

其中lb的含義其實是load balance的意思,我想開發者以lb來區分路由模式可能是負載均衡意味著多服務的環境,因此lb可以表示轉發物件從指定的uri轉變成了服務!

Predicate

Predicate是Java 8+新出的一個庫,本身作用是進行邏輯運算,支援種類如下:

  • isEqual
  • and
  • negate
  • or 另外還有一個方法test(T)用於觸發邏輯計算返回一個Boolean型別值。

Gateway使用Predicate來做除path pattern match之外的匹配判斷,使用及其簡單:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
	return builder.routes()
			.route("hello", r -> r
					.path("/user/**")
					.and()
					.predicate(e -> e.getClass() != null)
					.uri("http://localhost:8081")
					).build();
}
複製程式碼

輸入的e代表著ServerWebExchange物件,我們可以通過ServerWebExchange獲取所有請求相關的資訊,例如Cookies和Headers。通過Lambda語法去編寫判斷邏輯,如果一個Route中所有的Predicate返回的結果都是TRUE則匹配成功,否則匹配失敗。

Tp:path和predicate需要使用and連結,也可以使用or連結,分別代表不同的邏輯運算!

Filter

Filter的作用類似於Predicate,區別在於,Predicate可以做請求中斷,Filter也可以做,Filter可以做Reponse的修飾,Predicate並做不到,也就是說Filter最為最後一道攔截,可以做的事情有很多,例如修改響應報文,增加個Header或者Cookie,甚至修改響應Body,相比之下,Filter更加全能!

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
	return builder.routes()
			.route("hello", r -> r
					.path("/user/**")
					.and()
					.predicate(e -> e.getClass() != null)
					.filters(fn -> fn.addResponseHeader("developer", "Nico"))
					.uri("http://localhost:8081")
					).build();
}
複製程式碼

Spring Cloud Gateway 工作原理

Gateway是基於Spring MVC之上的閘道器路由控制器,我們可以直接定位到Spring MVC的org.springframework.web.reactive.DispatcherHandler類,它的handle方法將會處理解析Request之後的ServerWebExchange物件。

進入handle方法,將會使用Flux遍歷org.springframework.web.reactive.DispatcherHandler.handlerMappingsServerWebExchange進行處理,handlerMappings中包含著一下處理器:

org.springframework.web.reactive.function.server.support.RouterFunctionMapping@247a29b6, org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping@f6449f4, org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping@535c6b8b,
org.springframework.web.reactive.handler.SimpleUrlHandlerMapping@5633e9e
複製程式碼

以上只是一個簡單請求的處理器,Flux的concatMap方法會將每個處理器的Mono合併成一個Flux,然後呼叫org.springframework.web.reactive.DispatcherHandler類中的invokeHandler方法開始處理ServerWebExchange,處理完畢之後將緊接著處理返回值,這時使用handleResult方法,具體實現如下:

@Override
public Mono<Void> handle(ServerWebExchange exchange) {
	if (logger.isDebugEnabled()) {
		ServerHttpRequest request = exchange.getRequest();
		logger.debug("Processing " + request.getMethodValue() + " request for [" + request.getURI() + "]");
	}
	if (this.handlerMappings == null) {
		return Mono.error(HANDLER_NOT_FOUND_EXCEPTION);
	}
	return Flux.fromIterable(this.handlerMappings)
			.concatMap(mapping -> mapping.getHandler(exchange))
			.next()
			.switchIfEmpty(Mono.error(HANDLER_NOT_FOUND_EXCEPTION))
			.flatMap(handler -> invokeHandler(exchange, handler))
			.flatMap(result -> handleResult(exchange, result));
}
複製程式碼

Gateway起作用的關鍵在於invokeHandler方法中:

private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
	if (this.handlerAdapters != null) {
		for (HandlerAdapter handlerAdapter : this.handlerAdapters) {
			if (handlerAdapter.supports(handler)) {
				return handlerAdapter.handle(exchange, handler);
			}
		}
	}
	return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}
複製程式碼

對應的處理器的介面卡匹配到本身之後,將會觸發介面卡的處理方法,每個處理器都會實現一個對應的介面,他們大致都有一個共同的特點:

public interface Handler {

	Mono<Void> handle(ServerWebExchange exchange);

}
複製程式碼

每個介面卡的處理方法中都會在穿插入適配邏輯程式碼之後呼叫處理器的handle方法,Spring Cloud Gateway的所有Handler都在org.springframework.cloud.gateway.handler包下:

org.springframework.cloud.gateway.handler.AsyncPredicate<T>
org.springframework.cloud.gateway.handler.FilteringWebHandler
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping
複製程式碼

可以看到,上述三個處理器分別處理了Filter和Predicate,有興趣的朋友可以去看一下這些類內部的具體實現,這裡就不一一細說。

總的來講,Spring Cloud Gateway的原理是在服務載入期間讀取自己的配置將資訊存放在一個容器中,在Spring Mvc 處理請求的時候取出這些資訊進行邏輯判斷過濾,根據不同的處理結果觸發不同的事件!

而請求轉發這一塊有興趣的同學可以去研究一下!

TP: path的匹配其實也是一個Predicate邏輯判斷

Spring Cloud Gateway 總結

筆者偶然間看到spring-cloud-netflix的issue下的一個回答傳送門

Lovnx:Zuul 2.0 has opened sourcing,Do you intend to integrate it in some next version?
spencergibb:No. We created spring cloud gateway instead.
複製程式碼

這才感覺到Spring Cloud果然霸氣,也因此接觸到了Spring Cloud Gateway,覺得很有必要學習一下,總體來講,Spring Cloud Gateway簡化了之前過濾器配置的複雜度,也在新的配置方式上增加了微服務的閘道器配置,可以直接代替掉Zuul,期待著Spring會整出自己的註冊中心來。

筆者學藝不精,以上闡述有誤的地方,希望批評指出,聯絡方式:ainililia@163.com

相關文件

官方文件 Spring Cloud Gateway 入門 響應式程式設計之Reactor的關於Flux和Mono概念

相關文章