Spring Cloud Gateway 服务网关
Spring Cloud Gateway是 Spring Cloud 生态系统中的一个 API 网关服务,用于替换由Zuul开发的网关服务,基于Spring 5.0+Spring Boot 2.0+WebFlux等技术开发,提供了网关的基本功能,例如安全、监控、埋点和限流等,旨在为微服务架构提供一种简单而有效的统一 API 路由管理方式。
1.网关路由
1.1.认识网关
什么是网关?顾明思议,网关就是网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
1.2 Gateway特性
Gateway具有以下主要特性:
-
动态路由:能够匹配任何请求属性上的路由
-
断言(Predicate)和过滤器(Filter):针对特定路由的灵活配置
-
集成 Hystrix 断路器:提供熔断功能
-
服务发现集成:与 Eureka、Consul 等服务发现组件无缝集成
-
请求限流:支持基于多种策略的限流
-
路径重写:支持请求路径的重写
1.3 Gateway相关术语
① 路由(Route):路由是网关的基本组件,Gateway包含多个路由,每个路由包含唯一的ID(路由编号)、目标URI(即请求最终被转发到的目的地URI)、路由断言集合和过滤器集合。
② 断言(Predicate):实际上就是Java 8 Function Predicate的断言功能,即匹配条件,只有满足条件的请求才会被路由到目标URI。输入类型是 Spring Framework ServerWebExchange。其允许开发人员自行匹配来自HTTP请求的任何内容,例如HTTP头或参数。
③ 过滤器(Filter):作用类似于拦截加工,对于经过过滤器的请求和响应,都可以进行修改,例如Spring Framework GatewayFilter实例,可以在发送下游请求之前或之后修改请求和响应。
2. Gateway工作流程
当Gateway客户端向Gateway服务端发送请求时,请求首先被HttpWebHandlerAdapter提取组装成网关上下文,然后网关上下文会传递到DispatcherHandler中。DispatcherHandler是所有请求的分发处理器,主要负责分发请求对应的处理器,比如将请求分发到对应的RoutePredicateHandlerMapping(路由断言处理映射器)。路由断言处理映射器主要用于路由查找,以及找到路由后返回对应的FilterWebHandler。FilterWebHandler主要负责组装过滤器链并调用过滤器执行一系列过滤处理,然后把请求转到后端对应的代理服务处理,处理完毕之后将反馈信息
Spring Cloud Gateway 的核心流程:
客户端请求 → Gateway Handler Mapping → Gateway Web Handler → 过滤器链 → 代理服务
3. Spring Cloud Gateway案例
案例说明:创建两个简单的微服务模拟服务提供者和网关
3.1 父工程
创建父工程统一管理Spring Boot、Spring Cloud和Spring Cloud Alibaba,pom.xml文件代码如下所示。
4.0.0 com.hl shop 1.0.0 pom order-consumer order-provider org.springframework.boot spring-boot-starter-parent 2.7.12 17 17 UTF-8 2021.0.3 2021.0.4.0 2.7.12 org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring-cloud-alibaba.version} pom import org.projectlombok lombok 1.18.22 org.springframework.boot spring-boot-starter-test
3.2 service-provider微服务——服务提供者
在父工程中创建service-provider微服务,整合Nacos注册中心,并创建一个“/hello”接口来模拟服务提供者。
① 修改pom.xml文件,追加Nacos服务发现组件spring-cloud-starter-alibaba-nacos-discovery,代码如下所示。
4.0.0 com.hl shop 1.0.0 order-provider 17 17 UTF-8 org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery
② 在service-provider微服务的src/main/resources目录下创建application.yml文件,配置服务端口号为8081、微服务名为“service-provider”、Nacos注册中心地址为“localhost:8848”,代码如下所示。
server: port: 8081spring: application: name: service-provide profiles: active: dev cloud: nacos: server-addr: localhost:8848 # nacos地址
③ 按照Spring Boot规范创建项目启动类ServiceProviderApplication,在该启动类上追加@EnableDiscoveryClient注解(该注解表示向Nacos注册中心注册微服务),开启服务注册与发现功能,代码如下所示。
package com.hl;import com.hl.config.OpenFeignLoggerConfig;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication@EnableFeignClientspublic class ServiceProviderApplication{ public static void main(String[] args) { SpringApplication.run(ServiceProviderApplication.class, args); }}
④ 创建ProviderController类,在该类上追加@RestController注解,在该类中定义一个hello()方法,返回“hello”及传进来的实参name。
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ProviderController { @GetMapping(\"/hello\") public String hello(@RequestParam String name) { return \"hello \" + name + \"!\"; } }
3.3 service-gateway微服务——网关
创建微服务service-gateway,并整合到Nacos注册中心。
① 修改pom.xml文件,追加Nacos服务发现组件spring-cloud-starter-alibaba-nacos-discovery及Gateway依赖spring-cloud-starter-gateway,修改后的pom.xml文件代码如下所示。
4.0.0 com.hl shop 1.0.0 service-gateway 17 17 UTF-8 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-loadbalancer org.springframework.cloud spring-cloud-starter-gateway
② 在service-gateway微服务的src/main/resources目录下创建application.yml文件,配置服务端口号为8001、微服务名为“service-gateway、Nacos注册中心地址为“localhost:8848”,spring.cloud.gateway.discovery.locator.enabled为true开启Gateway服务发现,即Gateway将使用服务发现来动态路由请求,application.yml代码如下所示。
server: port: 8001 spring: application: name: service-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true
③ 按照Spring Boot规范创建项目启动类ServiceGatewayApplication,在该启动类上追加@EnableDiscoveryClient注解,开启服务注册与发现功能,代码如下所示。
@SpringBootApplication @EnableDiscoveryClient public class ServiceGatewayApplication { public static void main(String[] args) { SpringApplication.run(ServiceGatewayApplication.class, args); } }
3.4 测试Gateway路由转发
在IDEA工具中启动两个微服务:service-provider和service-gateway。启动Nacos,访问http://localhost:8848/nacos,选择“服务管理”的“服务列表”,可发现service-provider和service-gateway微服务实例,说明微服务已成功注册到了Nacos注册中心
通过Gateway端口号8001及服务名“service-provider”访问服务接口,即访问http://localhost: 8001/service-provider/hello?name=gateway,其中显示了“hello gateway”。至此基于网关实现了路由转发。
4. Gateway过滤器工厂
路由规则的定义语法如下:
spring: cloud: gateway: routes: - id: item uri: lb://item-service predicates: - Path=/items/**,/search/** filters: - AddRequestHeader=X-Request-red, blue
四个属性含义如下:
-
id
:路由的唯一标示 -
predicates
:路由断言,其实就是匹配条件 -
filters
:路由过滤条件 -
uri
:路由目标地址,lb://
代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。
过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。Gateway内置丰富的过滤器,例如AddRequestHeader、AddRequestParameter、AddResponseHeader、RemoveRequestHeader、StripPrefix、RewritePath、LoadBalancerClientFilter
4.1 AddRequestHeaderGatewayFilterFacotry
顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。
使用的使用只需要在application.yaml中这样配置:
spring: cloud: gateway: routes: - id: test_route uri: lb://test-service predicates: -Path=/test/** filters: - AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
如果想要让过滤器作用于所有的路由,则可以这样配置:
spring: cloud: gateway: default-filters: # default-filters下的过滤器可以作用于所有路由 - AddRequestHeader=key, value routes: - id: test_route uri: lb://test-service predicates: -Path=/test/**
5. Gateway路由断言工厂
Gateway包括很多路由断言,当HTTP请求进入Gateway之后,由于实际工作中Gateway中存在多个路由,因此路由断言会根据配置的路由规则对请求进行断言匹配,若匹配成功则从相应路由转发。
名称
说明
示例
After
是某个时间点后的请求
- After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before
是某个时间点之前的请求
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between
是某两个时间点之前的请求
- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie
请求必须包含某些cookie
- Cookie=chocolate, ch.p
Header
请求必须包含某些header
- Header=X-Request-Id, \\d+
Host
请求必须是访问某个host(域名)
- Host=**.somehost.org,**.anotherhost.org
Method
请求方式必须是指定方式
- Method=GET,POST
Path
请求路径必须符合指定规则
- Path=/red/{segment},/blue/**
Query
请求参数必须包含指定参数
- Query=name, Jack或者- Query=name
RemoteAddr
请求者的ip必须是指定范围
- RemoteAddr=192.168.1.1/24
weight
权重处理
5.1 Header路由断言
Header路由断言有两个参数:Header名称(name)和正则表达式形式的值(value)。该路由断言用于匹配具有给定名称且值与正则表达式匹配的HTTP头。
spring: cloud: gateway: default-filters: # default-filters下的过滤器可以作用于所有路由 - AddRequestHeader=key, value routes: - id: test_route uri: lb://test-service predicates: - Header=X-Request-Id, \\d+
6. 动态路由
路由规则是网关的核心内容,配置在应用的属性配置文件中,服务启动的时候将路由规 则加载到内存中,这属于静态路由方式。网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator
在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以我们无法利用配置热更新来实现路由更新。
可采用 Nacos 实现动态路由,把路由更新规则保存在分布式配置 中心 Nacos 中,通过 Nacos 的监听机制,动态更新每个实例的路由规则。因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。
6.1 监听Nacos配置变更
在Nacos官网中给出了手动监听Nacos配置变更的SDK:https://nacos.io/zh-cn/docs/sdk.html
监听配置:
如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。
public void addListener(String dataId, String group, Listener listener)
请求参数说明:
参数名
参数类型
描述
dataId
string
配置 ID,保证全局唯一性,只允许英文字符和 4 种特殊字符(\".\"、\":\"、\"-\"、\"_\")。不超过 256 字节。
group
string
配置分组,一般是默认的DEFAULT_GROUP。
listener
Listener
监听器,配置变更进入监听器的回调函数。
示例代码:
String serverAddr = \"{serverAddr}\";String dataId = \"{dataId}\";String group = \"{group}\";// 1.创建ConfigService,连接NacosProperties properties = new Properties();properties.put(\"serverAddr\", serverAddr);ConfigService configService = NacosFactory.createConfigService(properties);// 2.读取配置String content = configService.getConfig(dataId, group, 5000);// 3.添加配置监听器configService.addListener(dataId, group, new Listener() { @Override public void receiveConfigInfo(String configInfo) { // 配置变更的通知处理 System.out.println(\"recieve1:\" + configInfo); } @Override public Executor getExecutor() { return null; }});
这里核心的步骤有2步:
-
创建ConfigService,目的是连接到Nacos
-
添加配置监听器,编写配置变更的通知处理逻辑
第一步:
由于我们采用spring-cloud-starter-alibaba-nacos-config
自动装配,因此ConfigService
已经在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration
中自动创建好了:
NacosConfigManager中是负责管理Nacos的ConfigService的,具体代码如下:
因此,只要我们拿到NacosConfigManager
就等于拿到了ConfigService
第二步:
编写监听器。虽然官方提供的SDK是ConfigService中的addListener,不过项目第一次启动时不仅仅需要添加监听器,也需要读取配置,因此建议使用的API是这个:
String getConfigAndSignListener(
String dataId, // 配置文件id
String group, // 配置组,走默认
long timeoutMs, // 读取配置的超时时间
Listener listener // 监听器
) throws NacosException;
既可以配置监听器,并且会根据dataId和group读取配置并返回。我们就可以在项目启动时先更新一次路由,后续随着配置变更通知到监听器,完成路由更新。
6.2 更新路由
Gateway 提供了修改路由的接口 RouteDefinitionWriter,只有通过这个接口才能修改动态路由。
package org.springframework.cloud.gateway.route;import reactor.core.publisher.Mono;/** * @author Spencer Gibb */public interface RouteDefinitionWriter { /** * 更新路由到路由表,如果路由id重复,则会覆盖旧的路由 */ Mono save(Mono route); /** * 根据路由id删除某个路由 */ Mono delete(Mono routeId);}
这里更新的路由,也就是RouteDefinition,包含下列常见字段:
-
id:路由id
-
predicates:路由匹配规则
-
filters:路由过滤器
-
uri:路由目的地
将来我们保存到Nacos的配置也要符合这个对象结构,将来我们以JSON来保存,格式如下:
{ \"id\": \"item\", \"predicates\": [{ \"name\": \"Path\", \"args\": {\"_genkey_0\":\"/items/**\", \"_genkey_1\":\"/search/**\"} }], \"filters\": [], \"uri\": \"lb://item-service\"}
以上JSON配置就等同于:
spring: cloud: gateway: routes: - id: item uri: lb://item-service predicates: - Path=/items/**,/search/**
6.3 .实现动态路由
首先引入依赖:
com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.springframework.cloud spring-cloud-starter-bootstrap
然后在Nacos控制台添加路由,路由文件名为gateway-routes.json
,类型为json
:
[ { \"id\": \"item\", \"predicates\": [{ \"name\": \"Path\", \"args\": {\"_genkey_0\":\"/items/**\", \"_genkey_1\":\"/search/**\"} }], \"filters\": [], \"uri\": \"lb://item-service\" }]
然后在网关gateway
的resources
目录创建bootstrap.yaml
文件,内容如下:
server: port: 8080 # 端口spring: application: name: gateway cloud: nacos: server-addr: localhost config: file-extension: yaml shared-configs: - dataId: gateway-routes.json # 动态路由配置
然后,在gateway
中定义配置监听器:
package com.hmall.gateway.route;import cn.hutool.json.JSONUtil;import com.alibaba.cloud.nacos.NacosConfigManager;import com.alibaba.nacos.api.config.listener.Listener;import com.alibaba.nacos.api.exception.NacosException;import com.hmall.common.utils.CollUtils;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.route.RouteDefinition;import org.springframework.cloud.gateway.route.RouteDefinitionWriter;import org.springframework.stereotype.Component;import reactor.core.publisher.Mono;import javax.annotation.PostConstruct;import java.util.HashSet;import java.util.List;import java.util.Set;import java.util.concurrent.Executor;@Slf4j@Component@RequiredArgsConstructorpublic class DynamicRouteLoader { private final RouteDefinitionWriter writer; private final NacosConfigManager nacosConfigManager; // 路由配置文件的id和分组 private final String dataId = \"gateway-routes.json\"; private final String group = \"DEFAULT_GROUP\"; // 保存更新过的路由id private final Set routeIds = new HashSet(); @PostConstruct public void initRouteConfigListener() throws NacosException { // 1.注册监听器并首次拉取配置 String configInfo = nacosConfigManager.getConfigService() .getConfigAndSignListener(dataId, group, 5000, new Listener() { @Override public Executor getExecutor() { return null; } @Override public void receiveConfigInfo(String configInfo) { updateConfigInfo(configInfo); } }); // 2.首次启动时,更新一次配置 updateConfigInfo(configInfo); } private void updateConfigInfo(String configInfo) { log.debug(\"监听到路由配置变更,{}\", configInfo); // 1.反序列化 List routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class); // 2.更新前先清空旧路由 // 2.1.清除旧路由 for (String routeId : routeIds) { writer.delete(Mono.just(routeId)).subscribe(); } routeIds.clear(); // 2.2.判断是否有新的路由要更新 if (CollUtils.isEmpty(routeDefinitions)) { // 无新路由配置,直接结束 return; } // 3.更新路由 routeDefinitions.forEach(routeDefinition -> { // 3.1.更新路由 writer.save(Mono.just(routeDefinition)).subscribe(); // 3.2.记录路由id,方便将来删除 routeIds.add(routeDefinition.getId()); }); }}