官网:https://docs.spring.io/spring-cloud-gateway/docs/3.1.3/reference/html/
Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
什么是网关?
网关(Gateway)又称网间连接器,网关在网络层实现服务之间网络互连。用程序的话来说就是
请求转发
。他就好比小区的门一样,你想进来小区,必须通过门才能进来。为什么要有这个门?为了方便管理以及安全考虑,就好比现在疫情期间,动不动小区封了,假如小区没有门,想要封小区还得费特别大的劲将小区给围住。
再回到程序当中,现在非常流行微服务系统,将整体应用拆分为了多个模块,那么问题来了,前端页面肯定会涉及到调用不同微服务的接口,一个微服务就代表着一个端口的应用,每个微服务都是独立的,那么前端在调用的时候不可能将访问地址设置成多个呀。设置成多个当然也可以,弊端就是开发过程当中我们涉及到环境(域名服务器ip)的切换,那还得改多个,因此网关为解决这个问题而生,网关可以将多个微服务访问的地址进行聚拢起来,你要访问哪个微服务的接口,直接访问网关,网关负责给你转发到对应的微服务地址上,因此前端只需要知道网关的地址服务地址就可以了。
Gateway和Nginx两个网关的区别
在学习Gateway网关的时候很容易联想到Nginx,甚至刚接触的时候会好奇很多架构图上面 画的两个网关竟然都用到了,那么他们两个到底有什么关联性?
下图当中圈出来的就是Gateway所在的位置。而Nginx属于在Gateway上层。甚至可以在外部请求的上层,因为Nginx可以用来做网关层的负载均衡,同时他还可以用来做流量入口。因为他本身就是服务器,实际开发当中都是通过Nginx来做流量入口的。
什么是流量入口?
流量入口的意思就是假如我有个html放到linux当中,那么外面的人怎么才能访问到linux当中的页面呢?Nginx可以用来解决这个问题,他本身就是一个服务器,使用过程就是开启端口然后监听请求,当请求来了,可以将请求 指向本机的任意可访问的html文件。
Gateway可以称之为微服务网关(业务网关)
,而Nginx可以作为整个应用的流量网关
,以及微服务网关的负载均衡。业务网关针对具体的业务需要提供特定的流控策略、缓存策略、鉴权认证策略等等。流量网关通常只专注于全局的Api管理策略,比如全局流量监控、日志记录、全局限流、黑白名单控制、接入请求到业务系统的负载均衡等,有点类似防火墙。Kong 就是典型的流量网关。
Gateway底层使用了Netty通讯框架
SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件。
目前在cloud官网当中新版本已经将zuul彻底移除了。Zuul1.0已经进入了维护阶段
,而Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。在版本选型上我们基本上不会再考虑zuul了。
Zuul 1.x 基于Servlet 2. 5使用
阻塞架构它不支持任何长连接
(如 WebSocket) Zuul 的设计模式和Nginx较像,每次 I/ O 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx 用C++ 实现,Zuul 用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得Zuul 的性能相对较差。
Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。 Zuul 2.x的性能较 Zuul 1.x 有较大提升。在性能方面,根据官方提供的基准测试, Spring Cloud Gateway 的 RPS(每秒请求数)是Zuul 的
1. 6 倍
。
Spring Cloud Gateway 建立 在 Spring Framework 5、 Project Reactor 和 Spring Boot 2 之上,
使用非阻塞 API
。Spring Cloud Gateway还支持WebSocket
, 并且与Spring紧密集成拥有更好的开发体验
java.util.function.Predicate
,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
客户端向 Spring Cloud Gateway
发出请求。然后在 Gateway Handler Mapping
中找到与请求相匹配的路由,将其发送到 Gateway Web Handler
。
Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)
执行业务逻辑。
Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等
,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等
有着非常重要的作用。
核心逻辑:路由转发+执行过滤器链
假如我有个8001端口的服务,然后他有以下两个接口,但是我又不想让别人通过8001端口访问,我想让他通过9527访问怎么办?很简单通过Gateway搭建一个网关服务即可解决该问题。
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentMapper paymentMapper;
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentMapper.selectById(id);
log.info("*****查询结果:{}", payment);
if (payment != null) {
return new CommonResult(200, "查询成功, 服务端口:" + serverPort, payment);
} else {
return new CommonResult(444, "没有对应记录,查询ID: " + id + ",服务端口:" + serverPort, null);
}
}
@GetMapping(value = "/payment/lb")
public String getPaymentLB() {
return serverPort;
}
}
接下来搭建一个9527端口的Gateway入门级别的网关服务
1、以下是使用到的核心依赖,一般都会采用聚合工程,由父工程存放dependencyManagement当中的依赖,其他子模块引入使用的组件即可,单纯的练习图省劲的话,创建一个独立可运行的boot项目也可以。
关于搭建聚合我这里就尽量减省步骤了,不会搭建的可以看我的这一篇文章:https://blog.csdn.net/weixin_43888891/article/details/125267683?spm=1001.2014.3001.5502
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.6.8version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>2021.0.3version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
2、添加配置
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/** # 断言,路径相匹配的进行路由
3、测试访问:http://localhost:9527/payment/get/1
4、总结
通过以上示例,我们可以基本的掌握Gateway到底是干什么的,说白了就是请求转发。
如果程序引用了spring-cloud-starter-gateway
,但不希望启用网关,请设置spring.cloud.gateway.enabled=false
设置完之后再想通过网关来转发请求,会直接404。
java代码配置:
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
RouteLocatorBuilder.Builder routes = builder.routes();
routes.route("path_route", r ->
r.path("/payment/get/**").uri("http://localhost:8001")).build();
return routes.build();
}
}
配置文件配置
spring:
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
这两种可以实现同样的结果,假如两个都存在,并且Path是一样的,代码配置的要比配置文件配置的级别高,这时候会路由到代码配置的uri当中。
实际开发当中我们基本上都会用配置文件的方式来配置,很少会用java配置。
以上示例是直接将请求转发到了ip+端口服务上。只有配合注册中心使用,才能通过服务名称
来调用完成微服务的负载均衡。
如果对注册中心不是很了解的可以去看一下我之前的文章,有Eureka、Consul、zookeeper注册中心教程,所谓的注册中心我们可以把它当做就是一个map,服务的ip+端口号(或者是域名)就是value,而服务名称当做key。然后调用者通过服务名称来获取到调用服务的域名。这样假如微服务是集群,但是他们的服务名称是一样的,这样就可以很轻松的通过服务名来获取集群的ip+端口,有了ip+端口,就剩下负载均衡轮询调用了。
这里我直接就是通过Eureka注册中心来进行演示了。正常情况下,我们可以选择将网关服务注册到注册中心,也可以不进行注册,网关服务最重要的是要具备服务发现功能,至于要不要注册上去我们可以根据自己的业务场景来决定。
1.引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
2.修改配置
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
# false表示不向注册中心注册自己。
register-with-eureka: true
# false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
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/** # 断言,路径相匹配的进行路由
enabled: true
通过服务名称去注册中心获取ip和地址这不用我们操心,都由框架为我们解决了,我们只需要知道要调用的服务名称即可。
uri: lb://cloud-payment-service:需要注意的是uri的协议为Ib,表示启用Gateway的负载均衡功能。
3.启动类添加注解:@EnableEurekaClient
(代表的是启用Eureka)
4.测试:首先将两个提供者(两个提供者是一样的服务,只不过启动了两个,称之为集群)注册到注册中心,然后由网关来进行转发。
启动网关服务的时候会发现有一排日志,如下所示:
Route Predicate Factories是什么?
首先我们会发现一共有12个,他们每个都有自己的应用场景,我们也可以看看官网介绍:
Spring Cloud Gateway 将路由匹配为 Spring WebFluxHandlerMapping基础架构的一部分。Spring Cloud Gateway 包含许多内置的路由谓词工厂。所有这些谓词都匹配 HTTP 请求的不同属性。
您可以将多个路由谓词工厂与逻辑and语句结合起来
。(谓词工厂也就是我们所说的断言)
作用:如果请求与断言相匹配则进行路由,如果不匹配直接404
作用:就是通过设置一个时间,然后After代表的是后的意思,也就是设置的时间
之后
是可以访问这个路由的,在这个时间之前是访问不了的,注意:这个设置的是时区时间。
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now(); // 从默认时区中的系统时钟获取当前日期时间
System.out.println(zbj);
// ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间
// System.out.println(zny);
}
执行结果:
用法示例: 如下示例当中会发现我实际上相当于设置了两个predicates(断言),path也算是一个,After又是一个,在实际开发当中,根据自己的实际场景可以随便使用断言。
spring:
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/** # 断言,路径相匹配的进行路由
- After=2022-08-20T00:10:15.434859+08:00[Asia/Shanghai]
- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai]
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
- Cookie=username,zzyy
不带cookie访问直接404
带上cookie访问
- Header=X-Request-Id, \d+
- Host=**.baidu.com
正确:curl http://localhost:9527/payment/lb -H “Host: www.baidu.com”
正确:curl http://localhost:9527/payment/lb -H “Host: java.baidu.com”
错误:curl http://localhost:9527/payment/lb -H “Host: java.baidu.net”
- Method=GET
- Path=/payment/lb/**
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
示例:http://localhost:9527/payment/lb?username=31
- RemoteAddr=192.168.1.1/24
该路由会将约 80% 的流量转发到weighthigh.org,将约 20% 的流量转发到weightlow.org
- XForwardedRemoteAddr=192.168.1.1/24
可以把它当做IF判断,当满足的时候才能路由到uri,否则直接报异常。
过滤器可以在执行方法前
和执行方法后
进行过滤,所谓的过滤就是可以在请求上加一些操作,例如匹配到路由后可以在请求上添加个请求头,或者参数等等。
Gateway过滤器分为了两种:路由过滤器 和 全局过滤器
(1)AddRequestHeader GatewayFilter:相当于是给匹配到路由的request,添加Header
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
filters:
- AddRequestHeader=X-Request-red, blue
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(HttpServletRequest request, @PathVariable("id") Long id) {
String header = request.getHeader("X-Request-red");
System.out.println(header);
Payment payment = paymentMapper.selectById(id);
log.info("*****查询结果:{}", payment);
if (payment != null) {
return new CommonResult(200, "查询成功, 服务端口:" + serverPort, payment);
} else {
return new CommonResult(444, "没有对应记录,查询ID: " + id + ",服务端口:" + serverPort, null);
}
}
(2)The RequestRateLimiter GatewayFilter Factory:通过这个Filter就可以利用redis来完成限流
通过上面示例足以明白Filter的作用,官方提供了30多种Filter,如果有针对于单个路由需要过滤的,可以上官网查看,看看哪个符合自己的使用场景直接使用即可。
注意:下面这两种filters写法是等价的!
spring:
cloud:
gateway:
routes:
- id: setstatus_route
uri: https://example.org
filters:
- name: AddRequestHeader
args:
status: X-Request-red, blue
- id: setstatusshortcut_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue
全局过滤器到底什么时候用?
利用全局过滤器我们可以用来做统一网关鉴权,以及全局日志记录等等。
怎么用?
实现implements GlobalFilter, Ordered两个接口,然后重写两个方法即可。一个是filter方法,一个是getOrder方法。全局过滤器可以存在多个,多个的时候根据getOrder方法的返回值大小就行排序执行,数字最小的过滤器优先执行。
@Component //必须加,必须加,必须加
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("time:" + new Date() + "\t 执行了自定义的全局过滤器: " + "MyLogGateWayFilter" + "hello");
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
System.out.println("****用户名为null,无法登录");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
// 这个就是继续执行的意思
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
假如我请求没有写到uname的参数,这时候直接406!
带上之后访问成功!
除了以上常用的全局过滤器外,官网还提供了很多,比如Netty全局过滤器,websocket全局过滤器,这些基本上很少会用到,真正用到的时候看官网即可。
以下示例分别显示了如何设置全局前置和后置过滤器:其实还是基于全局过滤器,只不过展现的方式不一样了,这里是直接通过bean注解来注入到容器,然后使用的是匿名类。注释掉的代码是官网给的案例!
@Configuration
public class GateWayFilter {
@Bean
public GlobalFilter customGlobalFilter() {
// return (exchange, chain) -> exchange.getPrincipal()
// .map(Principal::getName)
// .defaultIfEmpty("Default User")
// .map(userName -> {
// //adds header to proxied request
// exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build();
// return exchange;
// })
// .flatMap(chain::filter);
return new GlobalFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("请求前执行,这里可以放请求前的逻辑");
exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", "lisi").build();
return chain.filter(exchange);
}
};
}
@Bean
public GlobalFilter customGlobalPostFilter() {
// return (exchange, chain) -> chain.filter(exchange)
// .then(Mono.just(exchange))
// .map(serverWebExchange -> {
// //adds header to response
// serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER",
// HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked" : "It did not work");
// return serverWebExchange;
// })
// .then();
return new GlobalFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
System.out.println("请求后执行,这里是当网关拿到转发服务的请求响应后会执行");
//adds header to response
serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER",
HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked" : "It did not work");
return serverWebExchange;
}).then();
}
};
}
}
在Spring Cloud Gateway中GlobalFilter可以方便的全局拦截或统计,有时候希望在某些路由中可以跳过GlobalFilter,可以通过GatewayFilter与GlobalFilter组合来实现。
1.GlobalFilter全局过滤器如下
@Component //必须加,必须加,必须加
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (exchange.getAttribute(IgnoreGlobalFilterFactor.ATTRIBUTE_IGNORE_GLOBAL_FILTER) != null) {
return chain.filter(exchange);
}
System.out.println("1111111111");
// 这个就是继续执行的意思
return chain.filter(exchange);
}
/**
* 这里假如设置1的话,他会优先于IgnoreGlobalFilterFactor过滤器,
* 我们主要要依靠IgnoreGlobalFilterFactor过滤器来进行赋值,然后全局过滤器根据赋值没赋值决定执行不执行过滤器
* 所以一定要保证IgnoreGlobalFilterFactor要比全局拦截器执行早,那么我们全局过滤器的这个order数值就要设置的大一点
* @return
*/
@Override
public int getOrder() {
return 10;
}
}
2.自定义GatewayFilter
@Component
public class IgnoreGlobalFilterFactor extends AbstractGatewayFilterFactory<IgnoreGlobalFilterFactor.Config> {
public static final String ATTRIBUTE_IGNORE_GLOBAL_FILTER = "@ignoreGlobalFilter";
public IgnoreGlobalFilterFactor() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return this::filter;
}
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(ATTRIBUTE_IGNORE_GLOBAL_FILTER, true);
return chain.filter(exchange);
}
// 这个名称就是yml当中设置的名称,也就是这个过滤器会去yml当中获取,看看有在filters当中设置IgnoreGlobalFilter,如果设置了执行,执行相当于是赋值了一下,然后在全局过滤器根据是否能拿到这个值来决定是否跳过过滤器
@Override
public String name() {
return "IgnoreGlobalFilter";
}
public static class Config {
}
}
3.yml配置
IgnoreGlobalFilter这个就是我们自定义的,假如哪个路由要跳过直接在filters当中设置即可。
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 #匹配后提供服务的路由地址
filters:
- IgnoreGlobalFilter
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
spring:
cloud:
gateway:
httpclient:
connect-timeout: 1000
response-timeout: 5s
一旦路由转发的请求,在返回response超过了5s,网关服务会直接返回给客户单504!
假如单个路由的不想受全局超时限制可以这样做
- id: per_route_timeouts
uri: https://example.org
predicates:
- name: Path
args:
pattern: /delay/{timeout}
metadata:
response-timeout: -1
- id: per_route_timeouts
uri: https://example.org
predicates:
- name: Path
args:
pattern: /delay/{timeout}
metadata:
response-timeout: 200
connect-timeout: 200
所谓的执行器API就是端点监控。Gateway给我们提供了大量的API,我们可以通过接口的形式访问到当前网关的路由以及Filter等状态信息。
1.要引入actuator依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
2.开启执行器API
management:
endpoint:
gateway:
enabled: true
endpoints:
web:
exposure:
include: gateway
这样就可以通过api来直接访问了,具体有哪些API我们可以参照官网来看。
logging:
level:
org.springframework.cloud.gateway: trace
开启之后只要是通过网关调用服务都会有如下日志!