Spring Cloud Gateway是Spring Cloud官方推出的第二代網關框架,取代Zuul網關。網關作為流量的,在微服務系統中有着非常作用,網關常見的功能有路由轉發、權限校驗、限流控制等作用。
Spring Cloud Gateway是Spring官方最新推出的一款基於Spring Framework 5,Project Reactor和Spring Boot 2之上開發的網關。與zuul1.0不同的是,gateway是異步非阻塞的(netty+webflux實現),zuul1.0是同步阻塞請求的。gateway的數據是封裝在ServerWebExchange中,zuul是存放在RequestContext里的(這里是重點,圈起來!)Gateway相對於Zuul來說,在路由的配置上更加多樣化,配置更加簡便。
官方文檔 : https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-starter
Gateway 核心概念:
-
Route 路由,它是網關的基礎元素,包含ID、目標URI、斷言、過濾器組成,當前請求到達網關時,會通過Gateway Handler Mapping,基於斷言進行路由匹配,當斷言為true時,匹配到路由進行轉發
-
Predicate,斷言,學過java8的同學應該知道這個函數,它可以允許開發人員去匹配HTTP請求中的元素,一旦匹配為true,則表示匹配到合適的路由進行轉發
-
Filter,過濾器,可以在請求發出的前后進行一些業務上的處理,比如授權、埋點、限流等。
Gateway 工作模型:
其中,predicate就是我們的匹配條件;而filter,就可以理解為一個無所不能的攔截器。有了這兩個元素,再加上目標uri,就可以實現一個具體的路由了。客戶端向 Spring Cloud Gateway 發出請求,如果請求與網關程序定義的路由匹配,則該請求就會被發送到網關 Web 處理程序,此時處理程序運行特定的請求過濾器鏈。過濾器之間用虛線分開的原因是過濾器可能會在發送代理請求的前后執行邏輯。所有 pre 過濾器邏輯先執行,然后執行代理請求;代理請求完成后,執行 post 過濾器邏輯。
Predicate 路由斷言:
Spring Cloud Gateway將路由匹配為Spring WebFlux HandlerMapping基礎設施的一部分。Spring Cloud Gateway包括許多內置的路由謂詞工廠。所有這些謂詞都匹配HTTP請求的不同屬性。您可以使用邏輯和語句組合多個路由謂詞工廠。
Gateway 的路由斷言機制以 AbstractRoutePredicateFactory 為基礎,實現了如下多種方式。再官方文檔內提供了配置說明
Gateway 網關的實現:
要在項目中引入Spring Cloud Gateway,引入相關依賴,然后只需要一些簡單的配置即可構建好一個 Gateway 網關服務。
1.引入依賴(基於 spring-cloud-dependencies Hoxton.SR4 版本)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2. 以 PathRoutePredicateFactory 路由斷言為例子演示,做以下配置:
server:
port: 9544
spring:
application:
name: gateway-service
cloud:
gateway:
enabled: true
discovery:
locator:
enabled: false #gateway開啟服務注冊和發現的功能
lowerCaseServiceId: true #請求路徑上的服務名配置為小寫
routes:
- id: ribbon-server
uri: lb://RIBBON-SERVER #uri以lb://開頭(lb代表從注冊中心獲取服務),后面接的就是你需要轉發到的服務名稱
predicates:
- Path=/demo/**
filters:
- StripPrefix=1 # 代表 Path 的值中將第一段舍棄,本例子就是轉發的時候為 /** 將/demo 去除。
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/
instance:
instance-id: gateway-service
3.啟動服務訪問相關的路徑即可看到效果。
自定義Predicate 路由斷言的實現:
創建一個類,繼承 AbstractRoutePredicateFactory 類,實現對應方法:
@Component public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> { public AuthRoutePredicateFactory() { super(Config.class); } private static final String NAME_KEY = "name"; private static final String VALUE_KEY = "value"; @Override public List<String> shortcutFieldOrder() { //屬性進行匹配對應
return Arrays.asList(NAME_KEY, VALUE_KEY); } @Override public Predicate<ServerWebExchange> apply(Config config) { //Header中攜帶了某個值,進行header的判斷
return (exchange -> { HttpHeaders headers = exchange.getRequest().getHeaders(); List<String> headerList = headers.get(config.getName()); return headerList.size() > 0; }); } public static class Config { private String name; private String value; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } }
yml 配置如下:
spring: application: name: gateway-service cloud: gateway: enabled: true discovery: locator: enabled: false #gateway開啟服務注冊和發現的功能 lowerCaseServiceId: true #請求路徑上的服務名配置為小寫 routes: - id: cookie_route predicates: - Auth=Authorization,token filters: - StripPrefix=1 uri: lb://RIBBON-SERVER
其中 Authorization 為 name,token 為 value,打個斷點看看就行了。
Filter 請求過濾器:
Filter分為全局過濾器和路由過濾器,當請求與路由匹配時,過濾Web處理程序會將的所有實例GlobalFilter
和所有特定GatewayFilter
於路由的實例添加到過濾器鏈中。該組合的過濾器鏈按org.springframework.core.Ordered
接口排序,您可以通過實現該getOrder()
方法進行設置。
需要獲取到更詳細的信息可以慘開官網 :https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories
這里演示一下自定義的過濾器(全局、路由)
1.全局過濾器只需要實現接口 org.springframework.cloud.gateway.filter.GlobalFilter,而后無需任何配置,即可生效。
@Component public class CustomGlobalFilter implements GlobalFilter, Ordered { Logger logger= LoggerFactory.getLogger(CustomGlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { logger.info("custom global filter"); return chain.filter(exchange); } @Override public int getOrder() { return -1; } }
2.路由過濾器 需要繼承 org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
每個過濾器工廠都對應一個實現類,並且這些類的名稱必須以 GatewayFilterFactory 結尾,這是Spring Cloud Gateway的一個約定,例如 AddRequestHeader 對應的實現類為 AddRequestHeaderGatewayFilterFactory
@Component public class WuzzDefineGatewayFilterFactory extends AbstractGatewayFilterFactory<WuzzDefineGatewayFilterFactory.WuzzConfig>{ private static final String NAME_KEY="name"; Logger logger= LoggerFactory.getLogger(WuzzDefineGatewayFilterFactory.class); public WuzzDefineGatewayFilterFactory() { super(WuzzConfig.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList(NAME_KEY); } @Override public GatewayFilter apply(WuzzConfig config) { //Filter pre post
return ((exchange,chain)->{ logger.info("[pre] Filter Request, name:"+config.getName()); //TODO
return chain.filter(exchange).then(Mono.fromRunnable(()->{ //TODO
logger.info("[post]: Response Filter"); })); }); } public static class WuzzConfig{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } }
3. 配置路由規則
spring: application: name: gateway-service cloud: gateway: enabled: true discovery: locator: enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199 routes: - id: config_route predicates: - Path=/demo/** filters: - StripPrefix=1 - WuzzDefine=Hello Wuzz uri: lb://RIBBON-SERVER
4. 啟動測試,發送一個請求,可以在控制台看到如下信息,說明過濾器均生效:
使用自帶的限流過濾器 :
spring: application: name: gateway-service cloud: gateway: enabled: true discovery: locator: enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199 routes: - id: config_route predicates: - Path=/demo/** filters: - StripPrefix=1 - WuzzDefine=Hello Wuzz uri: lb://RIBBON-SERVER - id: ratelimiter_route predicates: - Path=/ratelimiter/** filters: - StripPrefix=1 - name: RequestRateLimiter args: deny-empty-key: true keyResolver: '#{@ipAddressKeyResolver}' redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 2 uri: lb://RIBBON-SERVER
以上配置了限流過濾器,
- replenishRate:令牌桶中令牌的填充速度,代表允許每秒執行的請求數。
- burstCapacity:令牌桶的容量,也就是令牌桶最多能夠容納的令牌數。表示每秒用戶最大能夠執行的請求數量。
其中還需要配置一個 keyResolver:
@Component public class IpAddressKeyResolver implements KeyResolver{ @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } }
然后啟動測試,迅速刷新頁面訪問接口,會限流:
動態路由:
Spring Cloud Gateway 提供了 Endpoint 端點,暴露路由信息,有獲取所有路由、刷新路由、查看單個路由、刪除路由等方法,源碼在 org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint 中,想訪問端點中的方法需要添加 spring-boot-starter-actuator 依賴,並在配置文件中暴露所有端點
management: endpoints: web: exposure: include: "*"
列舉幾個常用的操作API 。詳細信息及配置查看官方文檔:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#actuator-api
- /actuator/gateway/routes GET 獲取路由列表
- /actuator/gateway/globalfilters GET 全局過濾器列表
- /actuator/gateway/routefilters GET 路由過濾器列表
- /actuator/gateway/refresh POST 刷新新增的路由
- /gateway/routes/{id_route_to_create} POST/DELETS 新增或者刪除
- ......
新增路由示例 :
然后調用一下 刷新的接口,即可生效。但是默認情況下,我們新增的路由只是保存在內存中,萬一服務重啟,則配置信息丟失,這個時候就需要將路由信息持久化。
路由持久化--基於redis
1. 引入 redis 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
2. 實現 org.springframework.cloud.gateway.route.RouteDefinitionRepository 接口,重寫路由的相關操作方法
@Component public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { private final static String GATEWAY_ROUTE_KEY="gateway_dynamic_route"; @Autowired RedisTemplate<String,String> redisTemplate; @Override public Flux<RouteDefinition> getRouteDefinitions() { List<RouteDefinition> routeDefinitionList=new ArrayList<>(); redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route->{ routeDefinitionList.add(JSON.parseObject(route.toString(),RouteDefinition.class)); }); return Flux.fromIterable(routeDefinitionList); } @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route.flatMap(routeDefinition -> { redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,routeDefinition.getId(),JSON.toJSONString(routeDefinition)); return Mono.empty(); }); } @Override public Mono<Void> delete(Mono<String> routeId) { return routeId.flatMap(id->{ if(redisTemplate.opsForHash().hasKey(GATEWAY_ROUTE_KEY,id)){ redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id); return Mono.empty(); } return Mono.defer(()->Mono.error(new Exception("routeDefinition not found:"+routeId))); }); } }
3.配置路由
spring: application: name: gateway-service cloud: gateway: enabled: true discovery: locator: enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199 routes: - id: config_route predicates: - Path=/demo/** filters: - StripPrefix=1 - WuzzDefine=Hello Wuzz uri: lb://RIBBON-SERVER
4. 測試,啟動后按照之前一樣創建一個路由,這個時候發現 redis里面已經保存了這個路由配置信息
也可以通過配置中心,比如 config 、Nacos 實現動態的配置。