gateway 官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/
在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。
这种客户端直接请求服务的方式存在以下问题:
API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。
API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例。
对于服务数量众多、复杂度较高、规模比较大的系统来说,使用 API 网关具有以下好处:
常见的 API 网关实现方案主要有以下 5 种:
SpringCloud Gateway 是在建立在 Spring 生态系统之上的 API 网关服务,基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,以及提供一些强大的过滤器功能,例如:安全,监控/指标、熔断、限流、重试等。
Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
Zuul 1.x 的 IO 模型:
SpringCloud 中所集成的 Zuul 1.x 版本,采用的是 Tomcat 容器,使用的是传统的 Servlet IO 处理模型。servlet 由 servlet container 进行生命周期管理:
上述模式的缺点:servlet 是一个简单的网络 IO 模型,当请求进入 servlet container 时,servlet container 就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个请求分配一个线程,只需要一个或几个线程就能应对极大并发的请求,这种业务场景下 servlet 模型没有优势。
所以 Zuul 1.x 是基于 servlet 阻塞 IO 模型的 API 网关,即 Spring 实现了处理所有 request 请求的一个 servlet(DispatcherServlet),并由该 servlet 阻塞式处理处理。每次 I/О 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成。所以 Zuul 1.x 无法摆脱 servlet 模型的弊端。
虽然 Zuul 2.0 开始,使用了 Netty 非阻塞和支持长连接,并且已经有了大规模 Zuul 2.0 集群部署的成熟案例,但是,SpringCloud 官方已经没有集成该版本的计划了。
GateWay 非阻塞异步模型
传统的Web框架,比如说:Struts2,SpringMVC 等都是基于 Servlet APl 与 Servlet 容器基础之上运行的。
但是在 Servlet3.1 之后有了异步非阻塞的支持。而 WebFlux 是一个典型非阻塞异步的框架,它的核心是基于 Reactor 的相关 API 实现的。Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖 Servlet APl,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。
SpringCloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务
过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。
Spring Cloud Gateway 工作流程说明如下:
Spring Cloud Gateway
上。Spring Cloud Gateway
通过 Gateway Handler Mapping
找到与请求相匹配的路由,将其发送给 Gateway Web Handler
。Gateway Web Handler
通过指定的过滤器链(Filter Chain
),将请求转发到实际的服务节点中,执行业务逻辑,返回响应结果返回给客户端。Filter
)可以在请求被转发到服务端前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等。总而言之,客户端发送到 Spring Cloud Gateway
的请求需要通过一定的匹配条件,才能定位到真正的服务节点。在将请求转发到服务进行处理的过程前后(pre 和 post),我们还可以对请求和响应进行一些精细化控制。
Predicate 就是路由的匹配条件,而 Filter 就是对请求和响应进行精细化控制的工具。有了这两个元素,再加上目标 URI,就可以实现一个具体的路由了。
核心逻辑:路由转发+执行过滤链
做网关不需要添加 web starter
<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>cloud2022artifactId>
<groupId>com.jm.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-gateway-gateway9527artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.jm.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
package com.jm.springcloud;
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);
}
}
我们目前不想暴露8001端口,希望在8001外面套一层9527
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:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
启动网关前访问:http://localhost:8001/get/payment/31
启动网关后访问:http://localhost:9527/get/payment/31
spring:
application:
name: cloud-gateway
cloud:
gateway: #网关路由配置
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议与服务名对应
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/getPaymentById/** #断言,路径相匹配的进行路由
- Method=GET #只能时GET请求时,才能访问
- id: payment_routh2 #路由的ID,没有固定规则但要求唯一,建议与服务名对应
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
当请求路径为 /guonei
时,转发到 https://news.baidu.com/guonei
。
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route",
r -> r.path("/guonei")
.uri("https://news.baidu.com/guonei"));
return routes.build();
}
}
相当于给网关配置一个负载均衡,因为看上面的配置把8001写死了
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
开启动态路由:spring.cloud.gateway.discovery.locator.enabled:true;
在添加uri的时候,开头是 lb://
微服务名,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 #lb:负载均衡协议
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #lb:负载均衡协议
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
测试:
因为开启了8001和8002两个端口,所以网关负载均衡的效果是 8001/8002切换
Spring Cloud Gateway 通过 Predicate 断言来实现 Route 路由的匹配规则。简单点说,Predicate 是路由转发的判断条件,请求只有满足了 Predicate 的条件,才会被转发到指定的服务上进行处理。
使用 Predicate 断言需要注意以下 3 点:
参考gateway官网的断言的例子:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-after-route-predicate-factory
常用的Route Predicate
After Route Predicate
- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
Before Route Predicate
- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
- Before=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
Between Route Predicate
- Between=2020-03-08T10:59:34.102+08:00[Asia/Shanghai] , 2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
Cookie Route Predicate
不带cookies访问
带上cookies访问 - Cookie=username,atguigu #并且Cookie是username=atguigu 才能访问
Cookie Route Predicate 需要两个参数,一个时Cookie name,一个是正则表达式。
路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上就不执行
Header Route Predicate
两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行;
Host Route Predicate
- Host=**.atguigu.com
Method Route Predicate
- Method=GET
Path Route Predicate
== Query Route Predicate==
- Query=username, \d+ #要有参数名称并且是正整数才能路由
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 #lb:负载均衡协议
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #lb:负载均衡协议
#filters:
# - AddRequestParameter=X-Request-Id,1024 #过滤器工厂会在匹配的请求头加上一对请求头,名称为X-Request-Id值为1024
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
#- After=2022-11-26T19:14:10.785+08:00[Asia/Shanghai]
#- Before=2022-11-26T19:22:29.119+08:00[Asia/Shanghai]
#- Between=2022-11-26T19:14:10.785+08:00[Asia/Shanghai], 2022-11-26T19:22:29.119+08:00[Asia/Shanghai]
#- Cookie=username,zzyy
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.atguigu.com
#- Method=GET
#- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
路由过滤器可用于修改进入的Http请求和返回的Http响应,路由过滤器只能过滤指定路由进行使用。
Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生
声明周期 :
种类:
implements GlobalFilter
,Ordered
全局日志记录
统一网关鉴权
…
package com.jm.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;
import java.util.Date;
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("************come in MyLogGateWayFilter: "+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname==null){
log.info("********用户名为null,非法用户,/(ㄒoㄒ)/~~");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}