一、Gateway概述
官网:
上一代zuul 1.X https://github.com/Netflix/zuul/wiki
当前gateway : 最新版 ,2.2.1.RELEASE
1、Gateway是什么
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用Zuul网关;
但在2.x版本中,zuul的升级就是一直跳票,SpringCloud最后自己研发了一个网关代替Zuul
那就是 SpringCloud Gateway ,gateway是zuul 1.x版本的替代。
Gateway是在Spring生态系统之上架构的API网关服务,基于Spring 5,Spring Boot2 和Project Reactor技术。
Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。
1.1 概述
- SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
- SpringCloud Gateway作为Spring cloud生态系统中的网关,目标是代替 Zuul,在SpringCloud2.0以上版本中,没有对新版本的Zuul 2.0以上实现最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty,【说穿了就是 SpringCloud Gateway是异步非阻塞式,响应式的框架】
- Spring Cloud Gateway的目标提供统一的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
一句话:SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
源码架构:
2、 能干嘛
3、微服务架构中网关在哪里
4、为什么选择gateway?
一方面因为Zuul1.0已经进入了维护阶段,同时Zuul2.0疯狂跳票。而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。
而且很多功能Zuul都没有用起来也非常的简单便捷。
Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的 Zuul 2.x,
但 Spring Cloud 貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
多方面综合考虑Gateway是很理想的网关选择。
4.1 SpringCloud Gateway具有如下特性
- 基于Spring Frameword5 ,Project Reactor 和 SpringBoot 2.0进行构建;
- 动态路由:能够匹配任何请求属性
- 可以对路由指定Predicate(断言)和Filter(过滤器)
- 集成Hystrix的断路器功能;
- 集成Spring Cloud的服务发现功能
- 易于编写的Predicate(断言)和Filter(过滤器)
- 请求限流功能;
- 支持路径重写
4.2 SpringCloud Gateway 与 Zuul的区别
在SpringCloud Finchley 正式版之前(现在H版),SpringCloud推荐的网关是Netflix提供的zuul。
- Zuul1.x 是一个基于阻塞 I/O的API网关
- Zuul1.x 基于Servlet2.5使用阻塞架构它不支持任何长连接 (如WebSocket)Zuul的设计模式和Nginx较像,每次I/O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能相对较差。
- Zuul 2.x理念更加先进,像基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul2.x的性能较Zuul 1.x有较大的提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求次数)是Zuul的1.6倍。
- Spring Cloud Gateway建立在Spring Framework 5、project Reactor和Spring Boot2 之上,使用非阻塞API
- Spring Cloud Gateway 还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验。
5、Zuul1.x模型
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。
Servlet的生命周期?servlet由servlet container进行生命周期管理。
container启动时构造servlet对象并调用servlet init()进行初始化;
container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。
container关闭时调用servlet destory()销毁servlet;
上述模式的缺点:
servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势
所以Zuul 1.X是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端6、gateway模型
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-new-framework
传统的Web框架,比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。
但是
在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)
Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。二、Gateway的三大核心概念与工作流程
1、Route(路由)
路由是构建网关的基本模块,它由ID,目标URI(Uniform Resource Identifier,统一资源标识符),一系列的断言和过滤器组成,如果断言为true则匹配该路由。2、Predicate(断言)
开发人员可以匹配Http请求中的所有内容(例如请求头或者请求参数),如果请求参数与断言相匹配则进行路由。3、Filter(过滤)
指的是Spring框架中的GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。4、总结
- web 请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制
- predicate 就是我们的匹配条件
filter:就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标的uri,就可以实现一个具体的路由了。
5、工作流程
- 客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。
- Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
- 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
- Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
四、入门配置
1、创建cloud-gateway-gateway-9527 模块
2、pom
做网关不需要添加 web starter 否则会报错
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jdk8cloud2021</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-gateway-gateway-9527</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--一般基础配置类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3、yml
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
# defaultZone: http://eureka7001.com:7001/eureka # 单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
4、主启动类(网关不需要业务类)
package com.atguigu.srpingcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class, args);
}
}
5. Gateaway9527网关如何做路由映射呢?
我们以前访问cloud-provider-payment8001中的controller方法,通过:
localhost:8001/payment/get/id 和 localhost:8001/payment/lb 就能访问到相应的方法。
现在我们不想暴露8001端口号,希望在8001外面套一层9527
yml新增网关配置
server:
port: 9527
spring:
application:
name: cloud-gateway
//=====================新增====================
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
//===================================================
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
# defaultZone: http://eureka7001.com:7001/eureka # 单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
- routes:可以为controller中所有的rest接口做路由
- uri + predicate拼接:http://localhost:8001/payment/get/** 就是具体的接口请求路径。其中uri是需要通过localhost:9527 映射的地址,即访问localhost:9527会转发到 localhost:8001
- predicate:断言http://localhost:8001下面有一个/payment/get/**这样的地址。如果找到了这个地址就返回true,可以用9527端口访问,进行端口的适配;找不到就返回false,不能用9527这个端口适配。慢慢的就不再暴露微服务本来的接口8001,转而使用统一网关9527。
/get/ 中表示通配符,因为这里是一个不确定的参数:/get/{id}
6、测试
启动7001、7002、cloud-provider-payment8001、9527
踩坑提示:gateway不需要spring-boot-starter-web依赖,否在会报错;原因是gateway底层使用的是webflux会与web冲突。
eureka集群中注册了9527和payment8001
6、Gateway网关路由有两种配置方式
6.1 在yml中配置——之前的方式
6.2 代码中注入RouteLocator的Bean
官网案例:
业务需求: 通过9527网关访问到百度新闻的网址;http://news.baidu.com/guonei
在config包下创建一个配置类 路由规则是:我现在访问/guonei,将会转发到http://news.baidu.com/guonei
package com.atguigu.srpingcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
/**
* 配置了一个id为path_route_atguigu的路由规则,
* 当访问地址 http://localhost:9527/guonei时会自动转发到地址:http://news.baidu.com/guonei
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
//构建一个路由器,这个routes相当于yml配置文件中的routes
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
//路由器的id是:path_route_atguigu,规则是我现在访问/guonei,将会转发到http://news.baidu.com/guonei
// 这里的id、path、uri都可以跟yml中的配置对上
// 通过localhost:9527 映射 http://news.baidu.com
routes.route("path_route_atguigu",
r -> r.path("/guonei").uri("http://news.baidu.com")).build();
return routes.build();
}
}
6.3 测试
五、进阶
5.1 通过微服务名实现动态路由
5.1.1 目前面临的问题
一个路由规则仅仅只对应一个接口方法,即我们将请求地址写死了。 试想一下:在分布式集群的情况下,会有多少个主机,多少个端口,多少个接口? 难道我们要为每一个接口都定义一个路由规则吗?
解决思路:我们前面用80调用8001和8002中的接口时,只认微服务名。访问接口时没有指定哪个端口。 那么我们在定义路由规则时也可以通过微服务名实现动态路由和负载均衡。
5.1.2 修改9527实现动态路由
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
pom:
//之前已经添加过了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
yml:spring.cloud.gateway.discovery.locator.enabled:true;
在添加uri的时候,开始是lb://微服务名
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
注意:
uri: lb://cloud-payment-service
lb://开头代表从注册中心中获取服务,后面接的就是你需要转发到的服务名称,而且找到的服务实现负载均衡。
配置好后的路由规则:
发送localhost:9527/xx/xxx请求,会去找cloud-payment-service服务名中对应微服务实例。 再根据具体的路径,找的具体的方法接口,并且开启了负载均衡。
5.1.3 测试
http://localhost:9527/payment/lb
8001、8002交替,负载均衡的轮循算法
六、Predicate 断言的使用
我们注意:Predicates 这是一个复数,其实有多种Predicate。 我们这里用的Predicate的是[Path],它是路由规则的其中一个。作用是,如果cloud-payment-service 的微服务实例中有/payment/get**的接口,就会返回true,路由规则生效。
但实际上存在多种Predicate:
每一个断言Predicate都有它独特的规则,多个Predicate断言是一个与&组合。
- Spring Cloud Gateway 将路由匹配作为Spring WebFlux Handler Mapping基础架构的一部分。
- Spring Cloud Gateway 包括许多内置的Route Predicate 工厂,所有的这些Predicate都和Http请求的不同属性匹配,多个Route Predicate可以进行组合。
- Spring Cloud Gateway 创建route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route,SpringCloud Gateway包含许多内置的Route Predicate Factories.
- 所有的 这些谓词都匹配Http的请求的各种属性,多种谓词工厂可以组合,并通过逻辑and
官网对gateway的断言每个都写了栗子:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-after-route-predicate-factory
1、常用的Predicate 断言
1. After Route Predicate:
- 匹配改断言时间之后的uri请求
- After=2021-09-28T19:14:51.514+08:00[Asia/Shanghai]
如何获得上述格式的时间呢?
public class TimeTest {
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
System.out.println(zbj); //2021-09-28T19:14:51.514+08:00[Asia/Shanghai]
// ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间
// System.out.println(zny);
}
}
配置yml
2.Before Route Predicate:
-
3.Between Route Predicate:
匹配改断言时间之间的uri请求
Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai]
4.Cookie Route Predicate:
- Cookie=chocolate, ch.p
Cookie Route Predicate需要两个参数,一个是Cookie name,一个是正则表达式。 路由规则会通过获取对应的Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由, 如果没有匹配上则不执行。
表示只有发送的请求有cookie,而且里面有username=zzyy这个数据才能访问4.1 利用curl命令发送POST/GET请求
不带cookie发送请求
cmd直接输入:curl http://localhost:9527/payment/lb
: 只发了一个GET请求,没有带Cookie
报错:
- 带cookie发送请求
curl http://localhost:9527/payment/lb --cookie "username=zzyy"
成功:
加入curl返回中文乱码
5.Header Route Predicate:
- Header=X-Request-Id, \d+
:带着这样的请求头才执行:请求头要有X-Request-Id属性,并且值为整数的正则表达式
测试:curl http://localhost:9527/payment/lb -H "X-Request-Id:123"
curl http://localhost:9527/payment/lb -H "X-Request-Id:-123"
6.Host Route Predicate:
- Host=**.atguigu.com
:只有指定主机可以访问,可以指定多个用“,”分隔开。
测试:
curl http://localhost:9527/payment/lb -H "Host: www.atguigu.com"
- 正确
curl http://localhost:9527/payment/lb -H "Host: java.atguigu.com"
- 正确
curl http://localhost:9527/payment/lb -H "Host: java.atguigu.net"
GET请求:
curl http://localhost:9527/payment/lb
- POST请求:
curl -X -POST http://localhost:9527/payment/lb
8.Path Route Predicate:
9.Query Route Predicate:
- Query=username, \d+
# 要有参数名username并且值还要是整数才能路由
2、小总结
说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。
七、Filter的使用
1、Filter是什么
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。 Filter链:同时满足一系列的过滤链。
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。
官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories
生命周期 :
pre 在业务逻辑之前
post 在业务逻辑之后
种类:
单一的:GatewayFilter
全局的:GlobalFilter
2、自定义全局过滤器(GlobalFilter)
两个主要接口介绍:implements GlobalFilter, Ordered
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("*********************come in MyLogGateWayFilter: "+ new Date());
//取出请求参数的uname对应的值
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
//如果uanme为空,就直接过滤掉,不走路由
if(uname == null){
log.info("************* 用户名为Null 非法用户 o(╥﹏╥)o");
//判断该请求不通过时:给一个回应,返回
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
//反之,调用下一个过滤器,也就是放行:在该环节判断通过的exchange放行,交给下一个filter判断
return chain.filter(exchange);
}
/**
* 这个过滤器的加载顺序,数字越小,优先级越高
* 设置这个过滤器在Filter链中的加载顺序。
*/
@Override
public int getOrder() {
return 0;
}
}
测试:
http://localhost:9527/payment/lb?uname=z3
发现报错了:
检查yml配置文件,发现predicates 断言设置了Query
加上Query需要的参数 username,或者将Query注释掉
http://localhost:9527/payment/lb?uname=z3&username=33
成功:
测试:http://localhost:9527/payment/lb 没有加uname,被过滤掉了