在Spring Cloud(09)——Hystrix断路器中,实现了分布式场景下服务发生故障时,服务的熔断和降级。现在我们用新一代网关Gateway来保护、增强和控制对于 API 服务的访问。
1、背景知识——API网关
网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。
API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。
2、Spring Cloud Gateway 详细概述
1、什么是Gateway
-
Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。
-
Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,其目标是替代 Netflix Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,==Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty==
-
Spring Cloud Gateway提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控/指标和限流等。
2、Gateway作用:
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
- 。。。。。。
3、微服务架构中网关的位置:
4、Spring Cloud Gateway具有如下特性:
- 基于Spring 5.0,Spring Boot 2.0和Project Reacto进行构建
- 动态路由:能够匹配任何请求属性
- 可以对路由指定Predicate(断言)和Filter(过滤器),并易于编写
- 集成Hystrix的断路器功能
- 集成Spring Cloud 服务发现功能
- 请求限流功能
- 支持路径重写
5、Spring Cloud Gateway 与Zull的区别:
- 在Spring Cloud Finchley 正式版之前,Spring Cloud 推荐使用的网关是 Netflix 提供的Zull
- Zull 1.x 是一个基于阻塞I / O 的API Gateway(API 网关)
- Zull 1.x 基于Servlet 2.5使用阻塞架构,它不支持任何长连接(如WecSocket)Zuul 的设计模式和Nginx较像,每次I / O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++ 实现,Zull用 Java实现,而JVM 本身会有第一次加载较慢的情况,使得Zuul 的性能相对较差。
- Zull 2.x 理念更先进,像基于Netty非阻塞和支持长连接,但SpringCloud 目前还没有整合。Zull 2.x 的性能较Zull 1.x 有较大的提升。在性能方面,根据官方提供的基准测试, Spring Cloud Gateway的 RPS(每秒请求数)是Zull 的1.6倍。
- Spring Cloud Gateway 建立在 Spring 5.0,Spring Boot 2.0和 Project Reactor之上,使用非阻塞 API。
- Spring Cloud Gateway 还支持 WebSocket,并且与Spring 紧密集成拥有更好的开发体验。
SpringCloud Gateway和Zuul主要的区别,是在底层的通信框架上。
Zuul使用的是Servlet 2.5阻塞架构,SpringCloud Gateway使用的是Webflux,Webflux是基于Servlet3.1的异步非阻塞框架。
6、Spring Cloud Gateway 三大核心概念:
- Filter(过滤器):
和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。
- Route(路由):
网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
- Predicate(断言):
这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。
7、Gateway工作流程:
工作流程图如下
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。
Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
Filter在 “pre” 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,
在“post” 类型的过滤器中可以做响应内容、响应头修改、日志的输出、流量监控等,有着非常重要的作用。
==核心逻辑:路由转发 + 执行过滤器链==
Zuul官网:github.com/netflix/zuu…
Gateway官网:spring.io/projects/sp…
3、Spring Cloud Gateway 入门配置
3.1、环境搭建
1、创建cloud -gateway-gateway9527模块
2、导入pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.cheng.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3、编写yml配置文件
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
4、创建主启动类
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class,args);
}
}
配置cloud-provider-payment8001服务提供者模块
用8001模块中下面两个方法进行测试:
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id){
Payment payment = paymentService.getPaymentById(id);
log.info("=======查询结果="+payment);
if (payment != null){
return new CommonResult(200,"查询执行成功,serverPort="+serverPort,payment);
}else {
return new CommonResult(500,"查询id为"+id+"执行失败",null);
}
}
@GetMapping(value = "/payment/lb")
public String getPaymentLb(){
return serverPort;
}
之前我们8001的服务接口都是对外暴露的,现在我们想在8001外面套一层gateway9527:
在8001的yml配置文件中增加配置:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #路由的id,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径向匹配的进行路由
- id: payment_routh2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/**
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
测试:
- 启动cloud-eureka-server7001和cloud-eureka-server7002模块
- 启动cloud-provider-payment8001模块
- 启动cloud -gateway-gateway9527网关模块
添加网关前的访问:http://localhost:8001/payment/get/2 和 http://localhost:8001/payment/lb
添加网关后的访问:http://localhost:9527/payment/get/2 和 http://localhost:9527/payment/lb
只要符合路由转发规则,网关9527可以成功的访问到8001模块提供的服务:
3.2、Spring Cloud Gateway 配置路由的两种方式
1、第一种:yml配置的方式,见前面步骤
2、第二种:编码的方式,代码中注入RouteLocator的Bean
业务需求:用编码的方式,通过9527网关访问到外网的百度新闻的地址 news.baidu.com/guonei
package com.cheng.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_baidu",
r -> r.path("/guonei")
.uri("https://news.baidu.com/guonei"))
.build();
return routes.build();
}
}
上面的配置类中,配置了一个id为path_route_baidu 的路由规则,
当请求:http://localhost:9527/guonei 时会自动跳转到 news.baidu.com/guonei
4、配置动态路由
默认情况下Gateway会根据注册中心的服务列表,==以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能==。
修改9527网关模块的yml位置文件:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名配置动态路由
routes:
- id: payment_routh #路由的id,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配路由后提供服务的路由地址 url的协议是lb,表示启用Gateway的负载均衡的功能
predicates:
- Path=/payment/get/** #断言,路径向匹配的进行路由
- id: payment_routh2
#uri: http://localhost:8001
uri: lb://cloud-payment-service #匹配路由后提供服务的路由地址
predicates:
- Path=/payment/lb/**
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
开启cloud-provider-payment8001和cloud-provider-payment8002模块进行测试:
访问请求:http://localhost:9527/payment/lb
第一次访问:
第二次访问:
不断实现了动态路由转发,还实现了轮询的负载均衡策略。
5、Predicate的使用
Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。
After Route Predicate Factory
匹配在指定日期时间之后发生的请求:
先获得当前时间:
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now);//2021-07-15T13:08:05.637+08:00[Asia/Shanghai]
断言条件:
predicates:
- Path=/payment/lb/**
- After=2021-07-15T14:08:05.637+08:00[Asia/Shanghai]
访问请求:http://localhost:9527/payment/lb
再访问一次:
我们把时间往后推迟一个小时,再访问一次:报404异常
Between Route Predicate Factory
匹配发生在datetime1之后和datetime2之前的请求。datetime2参数必须在datetime1之后。
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookie Route Predicate Factory
接受两个参数,Cookie名称和一个正则表达式)。路由规则会通过获取对应的Cookie名称和正则表达式去匹配,如果匹配就会执行路由,如果没有匹配则不执行。
断言条件:
predicates:
- Path=/payment/lb/**
- Cookie=username,wpc #键值对
使用curl发送请求进行测试:curl 是常用的命令行工具,用来请求 Web 服务器。
curl详细介绍: www.ruanyifeng.com/blog/2019/0…
不带cookie请求时:报404异常
带cookie请求时:成功访问
Header Route Predicate Factory
Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
断言条件:
predicates:
- Path=/payment/lb/**
- Header=X-Request-Id, \d+ #请求头要有X-Request-Id属性并且值为整数的正则表达式
使用 curl 测试,命令行输入: curl http://localhost:9527/payment/lb -H "X-Request-Id:88"
Host Route Predicate Factory
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。
断言条件:
predicates:
- Path=/payment/lb/**
- Host=**.baidu.com
使用 curl 测试,命令行输入: curl http://localhost:9527/payment/lb -H "Host: www.baidu.com"
Method Route Predicate Factory
可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。
断言条件:
predicates:
- Path=/payment/lb/**
- Method=GET
使用 curl 测试,命令行输入: curl http://localhost:9527/payment/lb (curl 默认是以 GET 的方式去请求)
我们再以 POST 的方式请求测试:curl -X POST http://localhost:9527/payment/lb 返回 404 没有找到,证明没有匹配上路由
Query Route Predicate Factory
Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。
断言条件:
predicates:
- Path=/payment/lb/**
- Query=keep, wei. #当请求中包含keep属性并且参数值是以wei开头的长度为4位的字符串才会进行匹配和路由。
使用 curl 测试,命令行输入: curl localhost:9527/payment/lb?keep=weip
RemoteAddr Route Predicate Factory
Predicate 也支持通过设置某个 ip 区间号段的请求才会路由,RemoteAddr Route Predicate 接受 cidr 符号(IPv4 或 IPv6 )字符串的列表(最小大小为1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子网掩码)。
断言条件:
predicates:
- Path=/payment/lb/**
- Query=keep, wei. #当请求中包含keep属性并且参数值是以wei开头的长度为4位的字符串才会进行匹配和路由。
如果请求的远程地址是 192.168.1.10,则此路由将匹配。
6、Filter的使用
6.1、过滤器工厂
GatewayFilter Factory 是 Spring Cloud Gateway 中提供的过滤器工厂。Spring Cloud Gateway 的路由过滤器允许以某种方式修改传入的 HTTP 请求或输出的 HTTP 响应,只作用于特定的路由。
Spring Cloud Gateway 中内置了很多过滤器工厂,直接采用配置的方式使用即可,同时也支持自定义 GatewayFilter Factory 来实现更复杂的业务需求。
1、AddRequestHeader 过滤器工厂
通过名称我们可以快速明白这个过滤器工厂的作用是添加请求头。
符合规则匹配成功的请求,将添加 X-Request-Foo:bar 请求头,将其传递到后端服务中,后方服务可以直接获取请求头信息。
2、RemoveRequestHeader 过滤器工、
RemoveRequestHeader 是移除请求头的过滤器工厂,可以在请求转发到后端服务之前进行 Header 的移除操作。
spring:
cloud:
gateway:
routes:
- id: removerequestheader_route
uri: http://c.biancheng.net
- RemoveRequestHeader=X-Request-Foo
3、SetStatus 过滤器工厂
SetStatus 过滤器工厂接收单个状态,用于设置 Http 请求的响应码。它必须是有效的 Spring Httpstatus(org.springframework.http.HttpStatus)。它可以是整数值 404 或枚举类型 NOT_FOUND。
spring:
cloud:
gateway:
routes:
- id: setstatusint_route
uri: http://c.biancheng.net
filters:
- SetStatus=401
4、RedirectTo过滤器工厂
RedirectTo 过滤器工厂用于重定向操作,比如我们需要重定向到百度。
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: http://c.biancheng.net
filters:
- RedirectTo=302, http://baidu.com
5、PrefixPath过滤器工厂
对所有的请求路径添加前缀:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath
访问/hello的请求被发送到example.org/mypath/hell…
6.3、自定义全局过滤器
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证、IP 访问限制等。
定义MyLogGatewayFilter类,实现GlobalFilter, Ordered 接口
package com.cheng.springcloud.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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;
@Component
@Slf4j
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String username = exchange.getRequest().getQueryParams().getFirst("username");
if (username == null){
log.info("=======用户名为null=======,非法用户");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange); //请求放行
}
@Override
public int getOrder() {
return 0;
}
}
访问请求: http://localhost:9527/payment/lb?username=dfds
如果不设置username的参数, http://localhost:9527/payment/lb 控制台提示错误信息