11. Gateway服务网关集成
1. Gateway是什么?
Gateway关键特性:路由、断言、过滤。
Spring Cloud Gateway是 Spring Cloud 的一个全新项目,基于 Spring 6.0+Spring Boot 3.0和 Project Reactor 等技术开发的网关
,它旨在为微服务架构提供一种简单有效的统一的 API路由管理方式。
Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代Zuul。
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关,在SpringCloud Finchley 正版之前,Spring Cloud 推荐的网关是 Netflix 提供的Zuul,但在2.x版本中,SpringCloud最后自己研发了一个网关Gateway替代Zuul。
在Spring Cloud 2.0以上版本中,没有对新版本Zuul2.0以最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud gateway,其于WebFlux框架实现的,而WebFlux框架底层,则使用了高性能的Reactor模式通讯框架Nettey。
Spring Cloud Gateway的目标提供统一的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如: 安全,监控/指标,和限流。
Spring WebFlux
是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。 Spring WebFlux官网信息
2. Spring Cloud Gateway 与 Zuul的区别(选型问题)
1、Zuul 1.x,是个其于阻塞I /O的API Gateway
2、Zuul 1.x
基于Sevlet 2.5使用阻塞架构
。它不支持任何长连接(如 WebSocket) Zuul 的设计模式和Nginx较像,每次 I /O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx 用C++ 实现,Zuul 用Java 实现,而 JVM 本身会有第一次加载较慢的情况,便得Zuul 的性能相对较差。
3、Zuul 2.x
理念更先进,想基于Netty非阻塞和支持长连接
,SpringCloud目前还没有整合。Zuul 2.x的性能较 Zuul 1.x 有较大提在性能方面,根据官方提供的基准测试,Spring Cloud Gateway 的 RPS (每秒请求数) 是Zuul的1.6倍。
4、Spring Cloud Gateway
建立在 Spring Framework 6、 Project Reactor 和 Spring Boot 3之上,使用非阻塞API。Gateway是基于异步非阻塞模型
上进行开发的,所以性能好,虽然Netflex发布了最新的Zuul2,但SpringCloud没有整合的计划。
5、Spring Cloud Gateway还支持 WebSocket,并目与Spring紧密集成
拥有更好的开发体验。
6、通过官网可知:Zuul1.0已经进入了维护阶段,而Gateway是SpringCloud团队研发
的,值得信赖。
Netflex 公司的zuul,官网地址:zuul官网
Spring社区的Gateway,官网地址:Gateway官网。
两者都是网关。Netflex 的zuul本来要升级,核心人员跳槽了,另一方面zuul升级到zuul2分歧大,导致zuul不维护了,zuul2研发中,于是等不及了,Spring社区出现了新一代网关技术Gateway,所以我们接下来主要学习Gateway。
3. Spring Cloud Gateway特性
通过官网可知,Spring Cloud Gateway有如下特性:
- (3.x版本以下)基于Spring Framework5,Project Reactor和 Spring Boot 2.0进行构建; (目前最高版本4.x版本)基于Spring Framework6,Project Reactor和 Spring Boot 3.0进行构建;
- 动态路由:能够匹配任何请求属性;
- 可以对路由指定Predicate(断言)和Filter(过滤器);
- 集成Hystrix的断路器功能;
- 集成Spring Cloud服务发现功能;
- 易于编写的Predicate(断言)和Filter(过滤器);
- 请求限流功能;
- 支持路径重写;
4. Gateway三大概念与工作流程
4.1 重要概念(路由、断言、过滤)
1)Route(路由): 路由是构建网关的基本模块,由ID,目标URI,一系列的断言和过滤器组成,如果断言为true,则匹配该路由。
2)Predicate(断言): 参考java8的java.util.function.Predicate开发人员可以匹配HTTP请求中的所有内容(例如请求头、请求参数),如果请求与断言相匹配则进行路由。
3)Filter(过滤) 指的是由Spring框架中GatewayFilter实例,使用过滤器,可以在请求被路由之前或者之后对请求进行修改。 web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前和后,通过Filter进行一些精细化控制和管理,predicate就是我们的匹配条件,有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。
4.2 工作流程(核心:路由转发+执行过滤链)
从官网工作流程图可知:客户向 Spring Cloud Gateway 发出请求,然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler。
Handler 再通过指定的过滤器链来将请求发这到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前 (“pre”) 或之后(“post”) 执行业务逻辑。
Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重更的作用。
5. Gateway项目集成与配置
具体项目集成可查看:SpringCloud Gateway网关集成与配置
6. Gateway动态路由
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
1、配置文件增加开启路由的配置
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
2、将之前写死的uri换成微服务名称(注册中心上显示的服务名)
uri: lb://cloud-payment #匹配后提供服务的路由地址
需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。 lb://serviceName是spring cloudgateway在微服务中自动为我们创建的负载均衡uri
完整配置:
server:
port: 9527
spring:
application:
name: cloud-gateway #微服务应用的名字
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment #匹配后提供服务的路由地址
predicates:
- Path=/payment/timeout/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment #匹配后提供服务的路由地址
predicates:
- Path=/payment/ok/** # 断言,路径相匹配的进行路由
eureka:
client:
register-with-eureka: true #向注册中心注册自己
fetch-registry: true #从EurekaServer抓取已有的注册信息,集群必须设置成true,才能配合ribbon负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
instance-id: gateway9527 #主机名称修改
prefer-ip-address: true #访问路径可以显示ip
hostname: cloud-gateway-service
测试负载均衡效果, 8001/8002两个端口切换:
7. Gateway的Predicate
1、网关项目启动时,控制台可以看到有很多种类型的断言。我们上边演示的是Path类型。
Spring Cloud Gateway包括许多内置的Route Predicate工厂。 所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合。 Spring Cloud Gateway 创建 Route 对象时, 使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。
2、 具体各种断言配置参考官网示例:
3、演示一种,以After为例:
//官网上美国时间,我们获取当前时间:
public static void main(String[] args) {
ZonedDateTime obj=ZonedDateTime.now();
System.out.println(obj);
}
输出:2023-06-06T15:36:36.892+08:00[Asia/Shanghai]
spring:
application:
name: cloud-gateway #微服务应用的名字
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment #匹配后提供服务的路由地址
predicates:
- Path=/payment/timeout/** # 断言,路径相匹配的进行路由
- After=2023-06-06T15:36:36.892+08:00[Asia/Shanghai] #
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment #匹配后提供服务的路由地址
predicates:
- Path=/payment/ok/** # 断言,路径相匹配的进行路由
After断言表示:在这个时间后,请求才有效果,在这个时间前,请求会404
8. Gateway的Filter
1、过滤器查看官网,37种之多(不常用)
参照官网添加相应的filters标签配置即可:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue
2、查看官网,自定义过滤器10种之多(比较常用)
自定义全局GlobalFilter主要用于全局日志记录、统一网关鉴权等。 实现两个重要接口 GlobalFilter
,Ordered
,容器注入,即可。
代码示例:
package com.qytest.springcloud.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Date;
@Component
@Slf4j
public class LogFilter implements GlobalFilter, Ordered {
@Bean
public GlobalFilter customFilter() {
return new LogFilter();
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("start global filter" + new Date());
//假设请求参数没有username认为非法用户,打印在日志中
String username = exchange.getRequest().getQueryParams().getFirst("username");
if (username == null) {
System.out.println("username 为空,非法用户!");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
测试访问(不带username):http://localhost:9527/payment/ok/1