Java | Spring Cloud Gateway 使用小技巧

我不是女神ヾ 2022-10-12 01:07 58阅读 0赞

网关中间件

本文中大部分内容都来自于 Spring Cloud Gateway 官网

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

在这里插入图片描述














































































中间件 Nginx Kong Netflix Zuul Spring Cloud Gateway shenyu
主要开发语言 C Lua Java Java Java
依赖关系 基于 Nginx_Lua模块
支持协议 HTTP HTTP, GRPC HTTP, HTTP HTTP, WebSocket, Dubbo, GRPC
扩展 基于 Lua 脚本 基于 Lua 脚本 Java 写过滤器 Java 写过滤器、断言 Java 写插件
编程模型 多进程 + io多路复用 基于 Nginx Zuul 1 采用 Servlet, Zuul 2 采用 Netty Spring WebFlux(Netty Reactor) Netty Reactor
配置页面 丰富 丰富
负载均衡 写死的 支持 Consul(间接可以支持使用 Consul 的 Spring Cloud) Spring Cloud 相关 Spring Cloud 相关 通过各种插件实现
GitHub nginx/nginx Kong/kong Netflix/zuul spring-cloud/spring-cloud-gateway apache/incubator-shenyu

文章目录

  • 网关中间件
  • Netflix Zuul 使用和一些实现
  • Spring Cloud Gateway 使用和一些实现细节
    • 日常使用
      • 一、全局跨域配置
      • 二、负载均衡失效的配置
      • 三、各种谓词路由的配置
          1. 时间谓词路由
          1. Cookie 谓词路由
          1. Header 谓词路由
          1. Host 谓词路由
          1. 请求方法谓词路由
          1. 路径参数谓词路由
          1. 查询参数谓词路由
          1. RemoteAddr 地址谓词路由
          1. 权重谓词路由
        • 如何实现一个谓词
      • 四、过滤器的配置
        • GatewayFilter
        • GlobalFilter
    • 注意事项
    • 常见过滤器的优先级和功能
    • 一些常见的操作
        1. 修改 response headers
        1. 修改 response 状态码
        1. 修改 response 示例
        1. 设置或者添加属性
        1. 统计请求时间示例
        1. 读取 Request Body
        1. 删除重复的 headers

Netflix Zuul 使用和一些实现

请看 [Zuul 是如何实现请求转发的](./zuul 是如何实现请求转发的.md)

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 下,需要配置均在这里

在这里插入图片描述

一、全局跨域配置

  1. spring:
  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 返回

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

三、各种谓词路由的配置

1. 时间谓词路由

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

指定时间点之前

  1. spring:
  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]

指定时间短范围内

  1. spring:
  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]

指定时间点之后

  1. spring:
  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]

cookie 中指定 key 的 值符合指定正则

  1. spring:
  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 里面判断

  1. spring:
  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 谓词路由

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: host_route
  6. uri: https://example.org
  7. predicates:
  8. - Host=**.somehost.org,**.anotherhost.org

5. 请求方法谓词路由

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

6. 路径参数谓词路由

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

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

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

7. 查询参数谓词路由

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

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

查询参数中有 key 为 name 的变量,且值符合 gree. 正则

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

8. RemoteAddr 地址谓词路由

可以做白名单

  1. spring:
  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. 权重谓词路由

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

  1. spring:
  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 是怎么实现的

  1. package org.springframework.cloud.gateway.handler.predicate;
  2. import java.time.ZonedDateTime;
  3. import java.util.Collections;
  4. import java.util.List;
  5. import java.util.function.Predicate;
  6. import org.springframework.web.server.ServerWebExchange;
  7. public class BeforeRoutePredicateFactory extends AbstractRoutePredicateFactory<BeforeRoutePredicateFactory.Config> {
  8. public static final String DATETIME_KEY = "datetime";
  9. public BeforeRoutePredicateFactory() {
  10. super(Config.class);
  11. }
  12. @Override
  13. public List<String> shortcutFieldOrder()
  14. // 这里返回的 list 变量名需要和配置文件中一一对应,顺序和变量名都得对应上
  15. // - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
  16. // 比如这样顺序决定时间和变量名的对应关系,这里的情况则为
  17. // datetime 对应 2017-01-20T17:42:47.789-07:00[America/Denver],
  18. // 其中 datatime 又需要对应上 Config.class 中的属性名,这样才能通过反射
  19. // 将 2017-01-20T17:42:47.789-07:00[America/Denver] 映射到 Config.class 的 datetime 属性上
  20. // 所以这里需要注意下顺序和变量名,否则可能会出现 Config.class 无法取到值的情况
  21. return Collections.singletonList(DATETIME_KEY);
  22. }
  23. @Override
  24. public Predicate<ServerWebExchange> apply(Config config) {
  25. return new GatewayPredicate() {
  26. // 这里返回 boolean 来确定是否能命中断言
  27. @Override
  28. public boolean test(ServerWebExchange serverWebExchange) {
  29. final ZonedDateTime now = ZonedDateTime.now();
  30. return now.isBefore(config.getDatetime());
  31. }
  32. };
  33. }
  34. public static class Config {
  35. private ZonedDateTime datetime;
  36. public ZonedDateTime getDatetime() {
  37. return datetime;
  38. }
  39. public void setDatetime(ZonedDateTime datetime) {
  40. this.datetime = datetime;
  41. }
  42. }
  43. }

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

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

四、过滤器的配置

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

GatewayFilter

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

  1. spring:
  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
  5. public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<Config> {
  6. private String message = "{\n"
  7. + " \"code\": 401,\n"
  8. + " \"errorMessage\": \"用户身份信息失效,请重新登录\""
  9. + "}";
  10. public AuthGatewayFilterFactory() {
  11. super(Config.class);
  12. }
  13. @Override
  14. public List<String> shortcutFieldOrder() {
  15. // 这里也是注意顺序和名称
  16. return Arrays.asList("executor");
  17. }
  18. @Override
  19. public GatewayFilter apply(Config config) {
  20. return (exchange, chain) -> {
  21. boolean valid = 这里应该是验证逻辑,如果验证通过返回 true;
  22. if (valid) {
  23. // 如果验证通过了,就继续走过滤链
  24. return chain.filter(exchange);
  25. } else {
  26. // 验证没通过,直接返回 401
  27. ServerHttpResponse response = exchange.getResponse();
  28. //设置 headers
  29. response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
  30. //设置body
  31. DataBuffer bodyDataBuffer = response.bufferFactory().wrap(message.getBytes());
  32. response.setStatusCode(HttpStatus.UNAUTHORIZED);
  33. return response.writeWith(Mono.just(bodyDataBuffer));
  34. }
  35. };
  36. }
  37. @ToString
  38. public static class Config {
  39. @Getter
  40. @Setter
  41. private String executor;
  42. }
  43. }

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

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

注意事项

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

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

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















































































































名称 优先级 是否启用 请求阶段 响应阶段
RemoveCachedBodyFilter HIGHEST_PRECEDENCE 如果发现有 RequestBody 就去除
AdaptCachedBodyGlobalFilter HIGHEST_PRECEDENCE + 1000 把 requestBody 缓存到 cachedRequestBody Attribute 中
DefaultValue 什么都没干,还抛出了一个异常
ForwardPathFilter 0 set the path in the request URI if the {@link Route} URI has the scheme
GatewayMetricsFilter 0 记录下发起时间 统计耗时
NettyWriteResponseFilter -1 将最终的 exchange 请求写回客户端
WebClientWriteResponseFilter -1 否,代码中无任何开启的方式 与 NettyWriteResponseFilter 类似
RouteToRequestUrlFilter 10000 将原始请求地址和路由配置的地址进行替换,将替换成的新地址放在 GATEWAY_REQUEST_URL_ATTR 属性中
ReactiveLoadBalancerClientFilter 10150 如果是 lb 则根据服务发现找到应的实例将实例地址设置成当前请求的 host
NoLoadBalancerClientFilter 10150 当 ReactorLoadBalancer 不存在且 spring.cloud.gateway.loadbalancer 属性存在
WebsocketRoutingFilter LOWEST_PRECEDENCE - 1 WebSocket 的请求转发
ForwardRoutingFilter LOWEST_PRECEDENCE 如果 shema 中含有 forward 则转发
NettyRoutingFilter LOWEST_PRECEDENCE 如果 shema 中为 http 则转发并写入 response
WebClientHttpRoutingFilter LOWEST_PRECEDENCE 否,代码中无任何开启的方式 和 NettyRoutingFilter 功能一样,转发请求的方式改为了 WebClient

一些常见的操作

1. 修改 response headers

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

2. 修改 response 状态码

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

3. 修改 response 示例

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

4. 设置或者添加属性

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

5. 统计请求时间示例

  1. public class TimeStatisticalFilter implements GlobalFilter, Ordered {
  2. private static final String START_TIME = "startTime";
  3. @Override
  4. public int getOrder() {
  5. // 这里可以通过设置不同的优先级来统计不同的阶段的时间
  6. return Ordered.HIGHEST_PRECEDENCE;
  7. }
  8. @Override
  9. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  10. exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
  11. return chain.filter(exchange).then(Mono.fromRunnable(() -> {
  12. Long startTime = exchange.getAttribute(START_TIME);
  13. if (startTime != null) {
  14. long executeTime = (System.currentTimeMillis() - startTime);
  15. log.info(exchange.getRequest().getURI().getRawPath() + " : " + executeTime + "ms");
  16. }
  17. }));
  18. }
  19. }

6. 读取 Request Body

有一些情况,我们可能要读取 Request Body,比如要对 Request Body 加解密或者其他的判断,如果只是读取操作,可以使用 ReadBodyRoutePredicateFactory 来实现,ReadBodyRoutePredicateFactory 配置有两个参数需要配置:1. inClass 用来配置将body 转换的类型;2. Predicate 判断什么情况下可以转。

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

  1. @Configuration
  2. public class Config {
  3. @Bean
  4. public Predicate bodyPredicate(){
  5. return new Predicate() {
  6. @Override
  7. public boolean test(Object o) {
  8. return true;
  9. }
  10. };
  11. }
  12. }

增加配置 route 配置,对需要读取 Request Body 的路由进行配置,这里配置将 Request Body 转换成 String,也方便后面使用的直接进行其他转换操作,例如 JSON。

  1. spring:
  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 来进行其他操作

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

7. 删除重复的 headers

  1. @Component
  2. public class RemoveDuplicateResponseHeaderFilter implements GlobalFilter, Ordered {
  3. @Override
  4. public int getOrder() {
  5. // 指定此过滤器位于NettyWriteResponseFilter之后
  6. // 即待处理完响应体后接着处理响应头
  7. return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
  8. }
  9. @Override
  10. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  11. return chain.filter(exchange).then(Mono.defer(() -> {
  12. exchange.getResponse().getHeaders().entrySet().stream()
  13. .filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
  14. .forEach(kv -> {
  15. kv.setValue(new ArrayList<String>() { {
  16. add(kv.getValue().get(0));
  17. }});
  18. });
  19. return chain.filter(exchange);
  20. }));
  21. }
  22. }

发表评论

表情:
评论列表 (有 0 条评论,58人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Spring Cloud -- GateWay

    1. 为什么需要网关 > 在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务

    相关 Spring cloud gateway

    网关核心功能是路由转发,因此不要有耗时操作在网关上处理,让请求快速转发到后端服务上。 早期的cloud中使用的是基于Zuul的网关,但是由于Zuul1.x是阻塞的,后面clo