?Java | Spring Cloud Gateway 使用和一些實(shí)現(xiàn)細(xì)節(jié)
網(wǎng)關(guān)中間件
所謂的API網(wǎng)關(guān),就是指系統(tǒng)的統(tǒng)一入口,它封裝了應(yīng)用程序的內(nèi)部結(jié)構(gòu),為客戶(hù)端提供統(tǒng)一服務(wù),一些與業(yè)務(wù)本身功能無(wú)關(guān)的公共邏輯可以在這里實(shí)現(xiàn),諸如認(rèn)證、鑒權(quán)、監(jiān)控、路由轉(zhuǎn)發(fā)等。

| 中間件 | Nginx | Kong | Netflix Zuul | Spring Cloud Gateway | shenyu |
|---|---|---|---|---|---|
| 主要開(kāi)發(fā)語(yǔ)言 | C | Lua | Java | Java | Java |
| 依賴(lài)關(guān)系 | 無(wú) | 基于 Nginx_Lua模塊 | 無(wú) | 無(wú) | 無(wú) |
| 支持協(xié)議 | HTTP | HTTP, GRPC | HTTP, | HTTP | HTTP, WebSocket, Dubbo, GRPC |
| 擴(kuò)展 | 基于 Lua 腳本 | 基于 Lua 腳本 | Java 寫(xiě)過(guò)濾器 | Java 寫(xiě)過(guò)濾器、斷言 | Java 寫(xiě)插件 |
| 編程模型 | 多進(jìn)程 + io多路復(fù)用 | 基于 Nginx | Zuul 1 采用 Servlet, Zuul 2 采用 Netty | Spring WebFlux(Netty Reactor) | Netty Reactor |
| 配置頁(yè)面 | 無(wú) | 豐富 | 無(wú) | 無(wú) | 豐富 |
| 負(fù)載均衡 | 寫(xiě)死的 | 支持 Consul(間接可以支持使用 Consul 的 Spring Cloud) | Spring Cloud 相關(guān) | Spring Cloud 相關(guān) | 通過(guò)各種插件實(shí)現(xiàn) |
| GitHub | nginx/nginx | Kong/kong | Netflix/zuul | spring-cloud/spring-cloud-gateway | apache/incubator-shenyu |
Netflix Zuul 使用和一些實(shí)現(xiàn)
Zuul 1 實(shí)現(xiàn)請(qǐng)求轉(zhuǎn)發(fā)的細(xì)節(jié)
Spring Cloud Gateway 使用和一些實(shí)現(xiàn)細(xì)節(jié)
官網(wǎng)地址:https://docs.spring.io/spring-cloud-gateway/docs/2.2.8.RELEASE/reference/html/
默認(rèn)已經(jīng)提供的功能:
http 請(qǐng)求轉(zhuǎn)發(fā)和負(fù)責(zé)均衡
websocket 的請(qǐng)求轉(zhuǎn)發(fā)和負(fù)載均衡
限流
Spring Boot 項(xiàng)目中引入依賴(lài),具體的版本號(hào)視情況而定。
1<dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-gateway</artifactId>
4</dependency>
如果需要開(kāi)啟負(fù)載均衡,需要引入對(duì)應(yīng)的依賴(lài),比如使用 Nacos 則需要引入
1<dependency>
2 <groupId>com.alibaba.cloud</groupId>
3 <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
4</dependency>
日常使用
Spring Cloud Gateway 相關(guān)配置均在 spring.cloud.gateway 下,需要配置均在這里

一、全局跨域配置
1spring:
2 cloud:
3 gateway:
4 globalcors:
5 cors-configurations:
6 '[/**]': // 全部請(qǐng)求
7 allow-credentials: true
8 allowed-origins: "*"
9 allowed-headers: "*"
10 allowed-methods: "*"
11 max-age: 3600
各個(gè)參數(shù)可以定制化
二、負(fù)載均衡失效的配置
如果請(qǐng)求時(shí),配置了負(fù)載均衡,且無(wú)法找對(duì)對(duì)應(yīng)的服務(wù)實(shí)例,默然返回 502,通過(guò) loadbalancer.use404 可以將其改為 404 返回
1spring:
2 cloud:
3 gateway:
4 loadbalancer:
5 use404: true
三、各種謂詞路由的配置
1. 時(shí)間謂詞路由
這個(gè)主要控制某個(gè)時(shí)間范圍走指定的路由
指定時(shí)間點(diǎn)之前
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: before_route
6 uri: https://example.org
7 predicates:
8 - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
指定時(shí)間段范圍內(nèi)
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: between_route
6 uri: https://example.org
7 predicates:
8 - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
指定時(shí)間點(diǎn)之后
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: after_route
6 uri: https://example.org
7 predicates:
8 - After=2017-01-20T17:42:47.789-07:00[America/Denver]
2. Cookie 謂詞路由
cookie 中指定 key 的 值符合指定正則
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: cookie_route
6 uri: https://example.org
7 predicates:
8 - Cookie=chocolate, ch.p
此路由匹配具有名為 Chocolate 的 cookie 的請(qǐng)求,該 cookie 的值與 ch.p 正則表達(dá)式匹配。
3. Header 謂詞路由
和 Cookie 謂詞路由功能一樣,只不過(guò)這次是從 headers 里面判斷
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: header_route
6 uri: https://example.org
7 predicates:
8 - Header=X-Request-Id, \d+
以下內(nèi)容太多,看官網(wǎng)吧:https://docs.spring.io/spring-cloud-gateway/docs/2.2.8.RELEASE/reference/html/
4. Host 謂詞路由
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: host_route
6 uri: https://example.org
7 predicates:
8 - Host=**.somehost.org,**.anotherhost.org
5. 請(qǐng)求方法謂詞路由
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: method_route
6 uri: https://example.org
7 predicates:
8 - Method=GET,POST
6. 路徑參數(shù)謂詞路由
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: path_route
6 uri: https://example.org
7 predicates:
8 - Path=/red/{segment},/blue/{segment}
可以通過(guò)來(lái)過(guò)去占位命名變量值
1Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);
2
3String segment = uriVariables.get("segment");
7. 查詢(xún)參數(shù)謂詞路由
請(qǐng)求參數(shù)中有 key 為 green 的請(qǐng)求參數(shù)
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: query_route
6 uri: https://example.org
7 predicates:
8 - Query=green
查詢(xún)參數(shù)中有 key 為 name 的變量,且值符合 gree. 正則
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: query_route
6 uri: https://example.org
7 predicates:
8 - Query=name, gree.
8. RemoteAddr 地址謂詞路由
可以做白名單
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: remoteaddr_route
6 uri: https://example.org
7 predicates:
8 - RemoteAddr=192.168.1.1/24
9. 權(quán)重謂詞路由
第一個(gè)參數(shù)是所在組,另一個(gè)是權(quán)重
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: weight_high
6 uri: https://weighthigh.org
7 predicates:
8 - Weight=group1, 8
9 - id: weight_low
10 uri: https://weightlow.org
11 predicates:
12 - Weight=group1, 2
如何實(shí)現(xiàn)一個(gè)謂詞
默認(rèn)提供的謂詞實(shí)現(xiàn)都在 org.springframework.cloud.gateway.handler.predicate 包下,通過(guò)如果想自定義實(shí)現(xiàn)一個(gè)謂詞,只需繼承AbstractRoutePredicateFactory, 即可,看一下 時(shí)間謂詞路由 Before 是怎么實(shí)現(xiàn)的
1package org.springframework.cloud.gateway.handler.predicate;
2
3import java.time.ZonedDateTime;
4import java.util.Collections;
5import java.util.List;
6import java.util.function.Predicate;
7
8import org.springframework.web.server.ServerWebExchange;
9
10
11public class BeforeRoutePredicateFactory extends AbstractRoutePredicateFactory<BeforeRoutePredicateFactory.Config> {
12
13 public static final String DATETIME_KEY = "datetime";
14
15 public BeforeRoutePredicateFactory() {
16 super(Config.class);
17 }
18
19 @Override
20 public List<String> shortcutFieldOrder()
21 // 這里返回的 list 變量名需要和配置文件中一一對(duì)應(yīng),順序和變量名都得對(duì)應(yīng)上
22 // - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
23 // 比如這樣順序決定時(shí)間和變量名的對(duì)應(yīng)關(guān)系,這里的情況則為
24 // datetime 對(duì)應(yīng) 2017-01-20T17:42:47.789-07:00[America/Denver],
25 // 其中 datatime 又需要對(duì)應(yīng)上 Config.class 中的屬性名,這樣才能通過(guò)反射
26 // 將 2017-01-20T17:42:47.789-07:00[America/Denver] 映射到 Config.class 的 datetime 屬性上
27 // 所以這里需要注意下順序和變量名,否則可能會(huì)出現(xiàn) Config.class 無(wú)法取到值的情況
28 return Collections.singletonList(DATETIME_KEY);
29 }
30
31 @Override
32 public Predicate<ServerWebExchange> apply(Config config) {
33 return new GatewayPredicate() {
34
35 // 這里返回 boolean 來(lái)確定是否能命中斷言
36 @Override
37 public boolean test(ServerWebExchange serverWebExchange) {
38 final ZonedDateTime now = ZonedDateTime.now();
39 return now.isBefore(config.getDatetime());
40 }
41 };
42 }
43
44 public static class Config {
45
46 private ZonedDateTime datetime;
47
48 public ZonedDateTime getDatetime() {
49 return datetime;
50 }
51
52 public void setDatetime(ZonedDateTime datetime) {
53 this.datetime = datetime;
54 }
55 }
56}
通過(guò)上面的代碼可以確定出,只要 test 方法即可
注意 實(shí)現(xiàn)謂詞時(shí),需要以 XxxxRoutePredicateFactory 命名,其中 Xxxx 就是以后配置時(shí)的前綴了
四、過(guò)濾器的配置
過(guò)濾器分兩種:GlobalFilter 針對(duì)全局路由使用;GatewayFilter 針對(duì)指定的路由的使用
GatewayFilter
通過(guò)為 route 配置 filters 來(lái)顯示的生效
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: add_request_header_route
6 uri: https://example.org
7 filters:
8 - AddRequestHeader=X-Request-red, blue
如何自定義個(gè) GatewayFilter,在日常的開(kāi)發(fā)中,有些接口是需要登錄,有些不需要登錄,這里以驗(yàn)證為例,看一下如何定制 GatewayFilter
定制 GatewayFilter 需要實(shí)現(xiàn)的是 AbstractGatewayFilterFactory
1// 實(shí)現(xiàn) AbstractGatewayFilterFactory 這里也需要一個(gè) Config 對(duì)象
2// 和實(shí)現(xiàn)謂詞基本一致
3@Component
4@Slf4j
5public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<Config> {
6
7 private String message = "{\n"
8 + " \"code\": 401,\n"
9 + " \"errorMessage\": \"用戶(hù)身份信息失效,請(qǐng)重新登錄\""
10 + "}";
11
12 public AuthGatewayFilterFactory() {
13 super(Config.class);
14 }
15
16 @Override
17 public List<String> shortcutFieldOrder() {
18 // 這里也是注意順序和名稱(chēng)
19 return Arrays.asList("executor");
20 }
21
22 @Override
23 public GatewayFilter apply(Config config) {
24 return (exchange, chain) -> {
25 boolean valid = 這里應(yīng)該是驗(yàn)證邏輯,如果驗(yàn)證通過(guò)返回 true;
26 if (valid) {
27 // 如果驗(yàn)證通過(guò)了,就繼續(xù)走過(guò)濾鏈
28 return chain.filter(exchange);
29 } else {
30 // 驗(yàn)證沒(méi)通過(guò),直接返回 401
31 ServerHttpResponse response = exchange.getResponse();
32 //設(shè)置 headers
33 response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
34 //設(shè)置body
35 DataBuffer bodyDataBuffer = response.bufferFactory().wrap(message.getBytes());
36 response.setStatusCode(HttpStatus.UNAUTHORIZED);
37 return response.writeWith(Mono.just(bodyDataBuffer));
38 }
39 };
40 }
41
42 @ToString
43 public static class Config {
44
45 @Getter
46 @Setter
47 private String executor;
48
49 }
50
51}
為指定的路由配置該過(guò)濾器
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: add_request_header_route
6 uri: https://example.org
7 filters:
8 - Auth=JWT
看到這里應(yīng)該可以看出,這里也是感覺(jué)名字前綴來(lái)配置的
GlobalFilter
GlobalFilter 對(duì)全部的路由都有效,不要額外進(jìn)行配置,注入就能用。
自定義 GlobalFilter 直接實(shí)現(xiàn) GlobalFilter 即可
1@Component
2@Slf4j
3public class TimeStatisticalFilter implements GlobalFilter, Ordered {
4
5 private static final String START_TIME = "startTime";
6
7 @Override
8 public int getOrder() {
9 // 指定此過(guò)濾器位于NettyWriteResponseFilter之后
10 // 即待處理完響應(yīng)體后接著處理響應(yīng)頭
11 return Ordered.LOWEST_PRECEDENCE;
12 }
13
14 @Override
15 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
16 exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
17
18 return chain.filter(exchange).then(Mono.fromRunnable(() -> {
19 Long startTime = exchange.getAttribute(START_TIME);
20 if (startTime != null) {
21 long executeTime = (System.currentTimeMillis() - startTime);
22 log.info(exchange.getRequest().getURI().getRawPath() + " : " + executeTime + "ms");
23 }
24 }));
25 }
26}
注意事項(xiàng)
exchange 本身中對(duì) request、response 不能直接修改,如果需要修改,需要生成一個(gè)新的 exchange 對(duì)象進(jìn)行修改,調(diào)用鏈本身有順序,如果要自定義 Filter 注意優(yōu)先級(jí)的設(shè)置
常見(jiàn)過(guò)濾器的優(yōu)先級(jí)和功能
每個(gè)版本的 Spring Cloud Gateway 可能不一樣,具體看 org.springframework.cloud.gateway.config.GatewayAutoConfiguration 里面相關(guān)配置
| 名稱(chēng) | 優(yōu)先級(jí) | 是否啟用 | 請(qǐng)求階段 | 響應(yīng)階段 |
|---|---|---|---|---|
| RemoveCachedBodyFilter | HIGHEST_PRECEDENCE | 是 | 如果發(fā)現(xiàn)有 RequestBody 就去除 | |
| AdaptCachedBodyGlobalFilter | HIGHEST_PRECEDENCE + 1000 | 是 | 把 requestBody 緩存到 cachedRequestBody Attribute 中 | |
| DefaultValue | 什么都沒(méi)干,還拋出了一個(gè)異常 | |||
| ForwardPathFilter | 0 | 是 | set the path in the request URI if the {@link Route} URI has the scheme | |
| GatewayMetricsFilter | 0 | 是 | 記錄下發(fā)起時(shí)間 | 統(tǒng)計(jì)耗時(shí) |
| NettyWriteResponseFilter | -1 | 是 | 將最終的 exchange 請(qǐng)求寫(xiě)回客戶(hù)端 | |
| WebClientWriteResponseFilter | -1 | 否,代碼中無(wú)任何開(kāi)啟的方式 | 與 NettyWriteResponseFilter 類(lèi)似 | |
| RouteToRequestUrlFilter | 10000 | 是 | 將原始請(qǐng)求地址和路由配置的地址進(jìn)行替換,將替換成的新地址放在 GATEWAY_REQUEST_URL_ATTR 屬性中 | |
| ReactiveLoadBalancerClientFilter | 10150 | 是 | 如果是 lb 則根據(jù)服務(wù)發(fā)現(xiàn)找到應(yīng)的實(shí)例將實(shí)例地址設(shè)置成當(dāng)前請(qǐng)求的 host | |
| NoLoadBalancerClientFilter | 10150 | 當(dāng) ReactorLoadBalancer 不存在且 spring.cloud.gateway.loadbalancer 屬性存在 | ||
| WebsocketRoutingFilter | LOWEST_PRECEDENCE - 1 | WebSocket 的請(qǐng)求轉(zhuǎn)發(fā) | ||
| ForwardRoutingFilter | LOWEST_PRECEDENCE | 如果 shema 中含有 forward 則轉(zhuǎn)發(fā) | ||
| NettyRoutingFilter | LOWEST_PRECEDENCE | 是 | 如果 shema 中為 http 則轉(zhuǎn)發(fā)并寫(xiě)入 response | |
| WebClientHttpRoutingFilter | LOWEST_PRECEDENCE | 否,代碼中無(wú)任何開(kāi)啟的方式 | 和 NettyRoutingFilter 功能一樣,轉(zhuǎn)發(fā)請(qǐng)求的方式改為了 WebClient |
一些常見(jiàn)的操作
1. 修改 response headers
1ServerHttpResponse response = exchange.getResponse();
2response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8); // 這句如果出現(xiàn)異常,直接 catch 即可,不影響修改
2. 修改 response 狀態(tài)碼
1ServerHttpResponse response = exchange.getResponse();
2response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
3. 修改 response 示例
1ServerHttpResponse response = exchange.getResponse();
2
3//設(shè)置 headers
4response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
5
6//設(shè)置body
7DataBuffer bodyDataBuffer = response.bufferFactory().wrap("{}".getBytes());
8response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
9return response.writeWith(Mono.just(bodyDataBuffer));
4. 設(shè)置或者添加屬性
1exchange.getAttributes().put(key, value);
2exchange.getAttribute(key)
5. 統(tǒng)計(jì)請(qǐng)求時(shí)間示例
1public class TimeStatisticalFilter implements GlobalFilter, Ordered {
2
3 private static final String START_TIME = "startTime";
4
5 @Override
6 public int getOrder() {
7 // 這里可以通過(guò)設(shè)置不同的優(yōu)先級(jí)來(lái)統(tǒng)計(jì)不同的階段的時(shí)間
8 return Ordered.HIGHEST_PRECEDENCE;
9 }
10
11 @Override
12 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
13 exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
14 return chain.filter(exchange).then(Mono.fromRunnable(() -> {
15 Long startTime = exchange.getAttribute(START_TIME);
16 if (startTime != null) {
17 long executeTime = (System.currentTimeMillis() - startTime);
18 log.info(exchange.getRequest().getURI().getRawPath() + " : " + executeTime + "ms");
19 }
20 }));
21 }
22}
6. 讀取 Request Body
有一些情況,我們可能要讀取 Request Body,比如要對(duì) Request Body 加解密或者其他的判斷,如果只是讀取操作,可以使用 ReadBodyRoutePredicateFactory 來(lái)實(shí)現(xiàn),ReadBodyRoutePredicateFactory 配置有兩個(gè)參數(shù)需要配置:1. inClass 用來(lái)配置將body 轉(zhuǎn)換的類(lèi)型;2. Predicate 判斷什么情況下可以轉(zhuǎn)。
先配置一個(gè)永真的 Predicate 來(lái)確定執(zhí)行這個(gè)謂詞
1@Configuration
2public class Config {
3
4 @Bean
5 public Predicate bodyPredicate(){
6 return new Predicate() {
7 @Override
8 public boolean test(Object o) {
9 return true;
10 }
11 };
12 }
13}
增加配置 route 配置,對(duì)需要讀取 Request Body 的路由進(jìn)行配置,這里配置將 Request Body 轉(zhuǎn)換成 String,也方便后面使用的直接進(jìn)行其他轉(zhuǎn)換操作,例如 JSON。
1spring:
2 cloud:
3 gateway:
4 routes:
5 - id: common
6 uri: lb://hhhh
7 predicates:
8 - Path=/hhhh
9 - name: ReadBody
10 args:
11 inClass: '#{T(String)}'
12 predicate: '#{@bodyPredicate}'
在后面的操作中,可以直接使用以下語(yǔ)句來(lái)獲取 Request Body 來(lái)進(jìn)行其他操作
1String requestBody = exchange.getAttribute("cachedRequestBodyObject");
7. 刪除重復(fù)的 headers
1@Component
2public class RemoveDuplicateResponseHeaderFilter implements GlobalFilter, Ordered {
3
4 @Override
5 public int getOrder() {
6 // 指定此過(guò)濾器位于NettyWriteResponseFilter之后
7 // 即待處理完響應(yīng)體后接著處理響應(yīng)頭
8 return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
9 }
10
11 @Override
12 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
13 return chain.filter(exchange).then(Mono.defer(() -> {
14 exchange.getResponse().getHeaders().entrySet().stream()
15 .filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
16 .forEach(kv -> {
17 kv.setValue(new ArrayList<String>() {{
18 add(kv.getValue().get(0));
19 }});
20 });
21 return chain.filter(exchange);
22 }));
23 }
24}
為什么使用網(wǎng)關(guān)
正如開(kāi)始提到的它封裝了應(yīng)用程序的內(nèi)部結(jié)構(gòu),為客戶(hù)端提供統(tǒng)一服務(wù),一些與業(yè)務(wù)本身功能無(wú)關(guān)的公共邏輯可以在這里實(shí)現(xiàn),諸如認(rèn)證、鑒權(quán)、監(jiān)控、路由轉(zhuǎn)發(fā)等。既然 Nginx 也可以實(shí)現(xiàn)類(lèi)似的功能,為什么還用 Spring Cloud Gateway ?
轉(zhuǎn)發(fā):Nginx 性能更好,Spring Cloud Gateway 的性能差之,不過(guò)其可以整合服務(wù)發(fā)現(xiàn),更加靈活,謂詞方式更多
可擴(kuò)展性:Spring Cloud Gateway 可以自己定義過(guò)濾器更加的靈活
開(kāi)發(fā):相對(duì)于 Nginx 其對(duì) Java 開(kāi)發(fā)更友好
具體實(shí)現(xiàn)轉(zhuǎn)發(fā)的細(xì)節(jié)見(jiàn) Java | Spring Cloud Gateway 是如何工作的
