​Java | Spring Cloud Gateway 使用和一些实现细节

双鬼带单

共 24194字,需浏览 49分钟

 ·

2021-07-03 12:31

网关中间件

所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等。

Spring Cloud 图片
中间件NginxKongNetflix ZuulSpring Cloud Gatewayshenyu
主要开发语言CLuaJavaJavaJava
依赖关系基于 Nginx_Lua模块
支持协议HTTPHTTP, GRPCHTTP,HTTPHTTP, WebSocket, Dubbo, GRPC
扩展基于 Lua 脚本基于 Lua 脚本Java 写过滤器Java 写过滤器、断言Java 写插件
编程模型多进程 + io多路复用基于 NginxZuul 1 采用 Servlet, Zuul 2 采用 NettySpring WebFlux(Netty Reactor)Netty Reactor
配置页面丰富丰富
负载均衡写死的支持 Consul(间接可以支持使用 Consul 的 Spring Cloud)Spring Cloud 相关Spring Cloud 相关通过各种插件实现
GitHubnginx/nginxKong/kongNetflix/zuulspring-cloud/spring-cloud-gatewayapache/incubator-shenyu

Netflix Zuul 使用和一些实现

Zuul 1 实现请求转发的细节

Spring Cloud Gateway 使用和一些实现细节

官网地址:https://docs.spring.io/spring-cloud-gateway/docs/2.2.8.RELEASE/reference/html/


默认已经提供的功能:

  • http 请求转发和负责均衡

  • websocket 的请求转发和负载均衡

  • 限流

Spring Boot 项目中引入依赖,具体的版本号视情况而定。

1<dependency>
2  <groupId>org.springframework.cloud</groupId>
3  <artifactId>spring-cloud-starter-gateway</artifactId>
4</dependency>

如果需要开启负载均衡,需要引入对应的依赖,比如使用 Nacos 则需要引入

1<dependency>
2  <groupId>com.alibaba.cloud</groupId>
3  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
4</dependency>

日常使用

Spring Cloud Gateway 相关配置均在 spring.cloud.gateway 下,需要配置均在这里

一、全局跨域配置

 1spring:
2  cloud:
3    gateway:
4      globalcors:
5        cors-configurations:
6          '[/**]':     // 全部请求
7            allow-credentials: true
8            allowed-origins: "*"
9            allowed-headers: "*"
10            allowed-methods: "*"
11            max-age: 3600

各个参数可以定制化

二、负载均衡失效的配置

如果请求时,配置了负载均衡,且无法找对对应的服务实例,默然返回 502,通过 loadbalancer.use404 可以将其改为 404 返回

1spring:
2  cloud:
3    gateway:
4      loadbalancer:
5        use404: true

三、各种谓词路由的配置

1.  时间谓词路由

这个主要控制某个时间范围走指定的路由

指定时间点之前

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]

指定时间段范围内

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]

指定时间点之后

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 的请求,该 cookie 的值与 ch.p 正则表达式匹配。

3. Header 谓词路由

和 Cookie 谓词路由功能一样,只不过这次是从 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+

以下内容太多,看官网吧: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. 请求方法谓词路由

1spring:
2  cloud:
3    gateway:
4      routes:
5      - id: method_route
6        uri: https://example.org
7        predicates:
8        - Method=GET,POST

6. 路径参数谓词路由

1spring:
2  cloud:
3    gateway:
4      routes:
5      - id: path_route
6        uri: https://example.org
7        predicates:
8        - Path=/red/{segment},/blue/{segment}

可以通过来过去占位命名变量值

1Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);
2
3String segment = uriVariables.get("segment");

7. 查询参数谓词路由

请求参数中有 key 为 green 的请求参数

1spring:
2  cloud:
3    gateway:
4      routes:
5      - id: query_route
6        uri: https://example.org
7        predicates:
8        - Query=green

查询参数中有 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. 权重谓词路由

第一个参数是所在组,另一个是权重

 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

如何实现一个谓词

默认提供的谓词实现都在 org.springframework.cloud.gateway.handler.predicate 包下,通过如果想自定义实现一个谓词,只需继承AbstractRoutePredicateFactory, 即可,看一下 时间谓词路由 Before 是怎么实现的

 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 变量名需要和配置文件中一一对应,顺序和变量名都得对应上
22    // - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
23    // 比如这样顺序决定时间和变量名的对应关系,这里的情况则为
24    // datetime 对应 2017-01-20T17:42:47.789-07:00[America/Denver],
25    // 其中 datatime 又需要对应上 Config.class 中的属性名,这样才能通过反射
26    // 将 2017-01-20T17:42:47.789-07:00[America/Denver] 映射到 Config.class 的 datetime 属性上
27    // 所以这里需要注意下顺序和变量名,否则可能会出现 Config.class 无法取到值的情况
28        return Collections.singletonList(DATETIME_KEY)
;
29    }
30
31    @Override
32    public Predicate<ServerWebExchange> apply(Config config) {
33        return new GatewayPredicate() {
34
35      // 这里返回 boolean 来确定是否能命中断言
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}

通过上面的代码可以确定出,只要 test 方法即可

注意 实现谓词时,需要以 XxxxRoutePredicateFactory 命名,其中 Xxxx 就是以后配置时的前缀了

四、过滤器的配置

过滤器分两种:GlobalFilter 针对全局路由使用;GatewayFilter  针对指定的路由的使用

GatewayFilter

通过为 route 配置 filters 来显示的生效

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

如何自定义个 GatewayFilter,在日常的开发中,有些接口是需要登录,有些不需要登录,这里以验证为例,看一下如何定制 GatewayFilter

定制 GatewayFilter 需要实现的是 AbstractGatewayFilterFactory

 1// 实现 AbstractGatewayFilterFactory 这里也需要一个 Config 对象
2// 和实现谓词基本一致
3@Component
4@Slf4j
5public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<Config{
6
7  private String message = "{\n"
8      + " \"code\": 401,\n"
9      + " \"errorMessage\": \"用户身份信息失效,请重新登录\""
10      + "}";
11
12  public AuthGatewayFilterFactory() {
13    super(Config.class);
14  }
15
16  @Override
17  public List<String> shortcutFieldOrder() {
18    // 这里也是注意顺序和名称
19    return Arrays.asList("executor");
20  }
21
22  @Override
23  public GatewayFilter apply(Config config) {
24    return (exchange, chain) -> {
25      boolean valid = 这里应该是验证逻辑,如果验证通过返回 true;
26      if (valid) {
27        // 如果验证通过了,就继续走过滤链
28        return chain.filter(exchange);
29      } else {
30                // 验证没通过,直接返回 401
31        ServerHttpResponse response = exchange.getResponse();
32        //设置 headers
33        response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
34        //设置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}

为指定的路由配置该过滤器

1spring:
2  cloud:
3    gateway:
4      routes:
5      - id: add_request_header_route
6        uri: https://example.org
7        filters:
8        - Auth=JWT

看到这里应该可以看出,这里也是感觉名字前缀来配置的

GlobalFilter

GlobalFilter 对全部的路由都有效,不要额外进行配置,注入就能用。

自定义 GlobalFilter 直接实现 GlobalFilter 即可

 1@Component
2@Slf4j
3public class TimeStatisticalFilter implements GlobalFilterOrdered {
4
5  private static final String START_TIME = "startTime";
6
7  @Override
8  public int getOrder() {
9    // 指定此过滤器位于NettyWriteResponseFilter之后
10    // 即待处理完响应体后接着处理响应头
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}

注意事项

exchange 本身中对 request、response 不能直接修改,如果需要修改,需要生成一个新的 exchange 对象进行修改,调用链本身有顺序,如果要自定义 Filter 注意优先级的设置

常见过滤器的优先级和功能

每个版本的 Spring Cloud Gateway 可能不一样,具体看 org.springframework.cloud.gateway.config.GatewayAutoConfiguration 里面相关配置

名称优先级是否启用请求阶段响应阶段
RemoveCachedBodyFilterHIGHEST_PRECEDENCE
如果发现有 RequestBody 就去除
AdaptCachedBodyGlobalFilterHIGHEST_PRECEDENCE + 1000把 requestBody 缓存到 cachedRequestBody Attribute 中
DefaultValue

什么都没干,还抛出了一个异常
ForwardPathFilter0set the path in the request URI if the {@link Route} URI has the scheme
GatewayMetricsFilter0记录下发起时间统计耗时
NettyWriteResponseFilter-1
将最终的 exchange 请求写回客户端
WebClientWriteResponseFilter-1否,代码中无任何开启的方式
与 NettyWriteResponseFilter 类似
RouteToRequestUrlFilter10000将原始请求地址和路由配置的地址进行替换,将替换成的新地址放在 GATEWAY_REQUEST_URL_ATTR  属性中
ReactiveLoadBalancerClientFilter10150如果是 lb 则根据服务发现找到应的实例将实例地址设置成当前请求的 host
NoLoadBalancerClientFilter10150当 ReactorLoadBalancer 不存在且 spring.cloud.gateway.loadbalancer 属性存在

WebsocketRoutingFilterLOWEST_PRECEDENCE - 1WebSocket  的请求转发

ForwardRoutingFilterLOWEST_PRECEDENCE
如果 shema 中含有 forward 则转发
NettyRoutingFilterLOWEST_PRECEDENCE如果 shema 中为 http 则转发并写入 response
WebClientHttpRoutingFilterLOWEST_PRECEDENCE否,代码中无任何开启的方式和 NettyRoutingFilter 功能一样,转发请求的方式改为了 WebClient

一些常见的操作

1. 修改 response headers

1ServerHttpResponse response = exchange.getResponse();
2response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);  // 这句如果出现异常,直接 catch 即可,不影响修改

2. 修改 response 状态码

1ServerHttpResponse response = exchange.getResponse();
2response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);

3. 修改 response 示例

1ServerHttpResponse response = exchange.getResponse();
2
3//设置 headers
4response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
5
6//设置body
7DataBuffer bodyDataBuffer = response.bufferFactory().wrap("{}".getBytes());
8response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
9return response.writeWith(Mono.just(bodyDataBuffer));

4. 设置或者添加属性

1exchange.getAttributes().put(key, value);
2exchange.getAttribute(key)

5. 统计请求时间示例

 1public class TimeStatisticalFilter implements GlobalFilterOrdered {
2
3  private static final String START_TIME = "startTime";
4
5  @Override
6  public int getOrder() {
7    // 这里可以通过设置不同的优先级来统计不同的阶段的时间
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,比如要对 Request Body 加解密或者其他的判断,如果只是读取操作,可以使用 ReadBodyRoutePredicateFactory 来实现,ReadBodyRoutePredicateFactory 配置有两个参数需要配置:1. inClass 用来配置将body 转换的类型;2. Predicate 判断什么情况下可以转。

先配置一个永真的 Predicate 来确定执行这个谓词

 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 配置,对需要读取 Request Body 的路由进行配置,这里配置将 Request Body 转换成 String,也方便后面使用的直接进行其他转换操作,例如 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}'

在后面的操作中,可以直接使用以下语句来获取 Request Body 来进行其他操作

1String requestBody = exchange.getAttribute("cachedRequestBodyObject");

7. 删除重复的 headers

 1@Component
2public class RemoveDuplicateResponseHeaderFilter implements GlobalFilterOrdered {
3
4  @Override
5  public int getOrder() {
6    // 指定此过滤器位于NettyWriteResponseFilter之后
7    // 即待处理完响应体后接着处理响应头
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}

为什么使用网关

正如开始提到的它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等。既然 Nginx 也可以实现类似的功能,为什么还用 Spring Cloud Gateway ?

  • 转发:Nginx 性能更好,Spring Cloud Gateway 的性能差之,不过其可以整合服务发现,更加灵活,谓词方式更多

  • 可扩展性:Spring Cloud Gateway 可以自己定义过滤器更加的灵活

  • 开发:相对于 Nginx 其对 Java 开发更友好

具体实现转发的细节见 Java | Spring Cloud Gateway 是如何工作的



浏览 89
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报