SpringCloud:服务网关-GateWay
Gateway概述 / Gateway的三大核心概念与工作流程 / 入门配置 / 通过微服务名实现动态路由 / 常用的Predicate 断言 / Filter的使用
Gateway概述
1.gateway是什么
Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。
SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
2.能干嘛
反向代理、鉴权、流量控制、熔断、日志监控等
3.架构图
4.为什么选择gateway?
springcloud亲儿子产品,Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心,Zuul前景未知
4.1 SpringCloud Gateway具有如下特性
- 基于Spring Frameword5 ,Project Reactor 和 SpringBoot 2.0进行构建;
- 动态路由:能够匹配任何请求属性
- 可以对路由指定Predicate(断言)和Filter(过滤器)
- 集成Hystrix的断路器功能;
- 集成Spring Cloud的服务发现功能
- 易于编写的Predicate(断言)和Filter(过滤器)
- 请求限流功能;
- 支持路径重写
4.2SpringCloud 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紧密集成拥有更好的开发体验。
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.建module
创建cloud-gateway-gateway-9527 模块
2.pom
做网关不需要添加 web starter 否则会报错,新增 spring-cloud-starter-gateway
3.yaml
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.主启动类(网关不需要业务类)
@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新增网关配置
cloud下面为新增功能!!!
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。
这样就为这两个方法做了路由匹配
6.测试
启动7001、7002、cloud-provider-payment8001、9527
Gateway网关路由有两种配置方式
6.1 在yml中配置——之前的方式
6.2 代码中注入RouteLocator的Bean
@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/guonei")).build();
return routes.build();
}
}
通过微服务名实现动态路由
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
修改9527的yaml
- 增加配置
spring.cloud.gateway.discovery.locator.enabled:true;
- 在添加uri的时候,开始是
lb://微服务名
server:
port: 9527
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 # 断言,路径相匹配的进行路由
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 #集群版
uri: lb://cloud-payment-service
lb://开头代表从注册中心中获取服务,后面接的就是你需要转发到的服务名称,而且找到的服务实现负载均衡
测试
启动 8001和8002,让CLOUD-PAYMENT-SERVICE有8001和8002
http://localhost:9527/payment/lb
8001、8002交替,负载均衡的轮循算法
常用Predicate 断言的使用
1.After Route Predicat
匹配该断言时间之后的uri请求--就是这个时间之后,请求才会生效,之前报错404
- After=2022-07-13T22:53:39.126+08:00[Asia/Shanghai]
如何获取这样的时间格式?
public class Text01 {
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
System.out.println(zbj); //2022-07-13T22:53:39.126+08:00[Asia/Shanghai]
// ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间
// System.out.println(zny);
}
}
2.Before Route Predicate:
匹配改断言时间之前的uri请求
3.Between Route Predicate:
匹配改断言时间之间的uri请求
- Between=2022-07-13T22:53:39.126+08:00[Asia/Shanghai],2022-07-14T22:53:39.126+08:00[Asia/Shanghai]
4.Cookie Route Predicate:
Cookie Route Predicate需要两个参数,一个是Cookie name,一个是正则表达式。 路由规则会通过获取对应的Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由, 如果没有匹配上则不执行。
- Cookie=chocolate, ch.p
表示只有发送的请求有cookie,而且里面有username=zzyy这个数据才能访问
表示只有发送的请求有cookie,而且里面有username=zzyy这个数据才能访问
4.1利用curl命令发送POST/GET请求
cmd直接输入:
curl http://localhost:9527/payment/lb
: 只发了一个GET请求,没有带Cookie
带cookie发送请求
curl http://localhost:9527/payment/lb --cookie "username=tin"
5.Header Route Predicate
- Header=X-Request-Id, \d+
:带着这样的请求头才执行:请求头要有X-Request-Id属性,并且值为整数的正则表达式
测试:
正常:curl http://localhost:9527/payment/lb -H "X-Request-Id:123"
404: curl http://localhost:9527/payment/lb -H "X-Request-Id:-123"
6.Host Route Predicate:
- Host=**.tinstu.com
:只有指定主机可以访问,可以指定多个用“,”分隔开。
测试:
- 正确:
curl http://localhost:9527/payment/lb -H "Host: www.tinstu.com"
- 正确:
curl http://localhost:9527/payment/lb -H "Host: java.tinstu.com"
- 错误:
curl http://localhost:9527/payment/lb -H "Host: java.baidu.com"
7.Method Route Predicate:
- Method=GET
#只有get请求才能访问
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并且值还要是整数才能路由
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
自定义全局过滤器(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,被过滤掉了
http://localhost:9527/payment/lb?uname=z3 正常