Spring Cloud Gateway 深刻

Spring Cloud Gateway介紹

廢話很少說,看官方文檔的介紹html

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.react

有道翻譯一下:git

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

工做原理以下:web

image

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

  • Route
  • Filter
  • Forward

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

  • Yml or Properties File
  • Code

主要名詞以下:api

  • 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匹配的狀況下進一步去更加細緻的匹配請求路徑,如一下例子:bash

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概念

相關文章
相關標籤/搜索