SpringCloud Gateway
SpringCloud Gateway教程
提示
本章节记录一下 SpringCloud 下的微服务网关 Gateway 的入门教程,版本环境如下👇
框架 | 版本 |
---|---|
SpringBoot | 2.5.3 |
SpringCloud | 2020.0.3 |
SpringCloudAlibaba | 2021.1 |
nacos | 14.1 |
Gateway | 3.0.3 |
Gateway 简介
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
网关作为流量的入口,常用功能包括路由转发、权限校验、限流控制等。而 springcloud gateway 作为 SpringCloud 官方推出的第二代网关框架,取代了 Zuul 网关
声明:Spring Cloud Gateway 底层使用了高性能的通信框架Netty
官方网址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
核心概念
- Route(路由): 这是网关的基本构建块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配
- Predicate(断言): Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring5.0 框 架中的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配 来自于 http request 中的任何信息,比如请求头和参数等
- Filter(过滤器): 一个标准的 Spring webFilter。Spring cloud gateway 中的 filter 分为两种类型的 Filter,分别是
Gateway Filter
和Global Filter
。过滤器 Filter 将会对请求和响应进行修改 处理
工作原理如下图👇
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler
Handler 再将请求交给一个过滤器链,请求到达目标服务之前,会执行所有过滤器的 pre 方法。请求到达目标服务处理之后再依次执行所有过滤器的 post 方法
总之就是:满足某些断言(predicates)就路由到指定的地址(uri),使用指定的过滤器(filter)
搭建 Gateway 环境
要使用 SpringCloud Gateway 需要导入相应的依赖,之后需要设置 Gateway 的路由配置
我使用了 spring-boot 2.5.3
作为 parent 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
在 dependencyManagement 中,我们需要指定 SringCloud 的版本,以便保证我们能够引入我们想要的 SpringCloud Gateway版本,所以需要用到 dependencyManagement
<properties>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
最后导入我们的 Gateway 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
此外,请检查一下你的依赖中是否含有 spring-boot-starter-web,如果有,请干掉它。因为我们的SpringCloud Gateway是一个netty+webflux 实现的web服务器,和 Springboot Web 本身就是冲突的
基础 URI 路由配置方式
如果请求的目标地址,是单个的URI资源路径,配置文件示例如下:
server:
port: 88
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.zenghr.cn
predicates:
- Path=/blog
参数说明:
- id: 我们自定义的路由 ID,保持唯一
- uri: 目标服务地址
- predicates: 路由条件,Predicate 接受一个输入参数,返回一个布尔值结果
上面这段配置的意思是,配置了一个 id 为 url-proxy-1 的URI代理规则,路由的规则为:
当访问地址 http://localhost:88/blog/1.html
时,会路由到上游地址 https://blog.zenghr.cn/blog/1.html
基于代码的路由配置方式
转发功能代码也可以通过代码的方式来实现,我们可以自定义 RouteLocator
Bean 实现自定义制转发规则
@Configuration
public class GatewayConfiguration {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("url-proxy-1",
p -> p.predicate(e -> e.getRequest().getURI().getPath().startsWith("/blog"))
.filters(f -> f.rewritePath("/blog/(?<remaining>.*)", "/${remaining}"))
.uri("https://blog.zenghr.cn"))
.route("url-proxy-2", p -> p.path("/blog2").uri("https://blog.zenghr.cn"))
.build();
}
}
我们注释掉 xml 配置文件中的 Gateway 路由设置,重启服务, 当访问地址 http://localhost:88/blog/1.html
时,会路由到上游地址 https://blog.zenghr.cn/1.html
, 能够访问说明我们的代码配置成功
参数说明
通过 RouteLocatorBuilder 的routes,可以逐一建立路由,每调用route一次可建立一条路由规则
p 的代表是 PredicateSpec,可以透过它的 predicate 来进行断言,要实现的接口就是Java 8的 Predicate,通过 exchange 取得了路径,然后判断它是不是以 /blog 开头,对于简单的情况,也可以通过 path 等来进行断言
filters 是用来设置过滤器,rewritePath 方法会使用内建的过滤器重写路径
注册中心相结合的路由配置方式
在 uri 的 schema 协议部分为自定义的 lb:类型,表示从微服务注册中心(如 Nacos)订阅服务,并且进行服务的路由
在我的项目中,使用的是 gateway + nacos 的方式来实现的
导入 nacos 的服务注册依赖,需要把 gateway 服务注册到 nacos,才能发现其他服务
<!-- nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
如果需要使用 nacos 作为配置中心,也可以导入相应的依赖
我们的配置文件内容需要如下设置👇
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 开启服务发现功能,默认为 false
routes:
- id: member_route
uri: lb://mall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/member/(?<segment>.*),/$\{segment}
参数说明
- locator.enabled: 开启服务发现,需要开启该配置
- lb: 代表 负载均衡
注意
由于 SpringCloud 2020 弃用了 Ribbon,因此 Alibaba 在 2021 版本 nacos
中删除了 Ribbon的 jar 包,因此无法通过lb路由到指定微服务,出现了503情况
所以只需要引入 springcloud loadbalancer 包即可
<!--客户端负载均衡loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
Gateway 匹配规则
Spring Cloud Gateway 是通过 Spring WebFlux 的 HandlerMapping 做为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了很多 Predicates 工厂,这些 Predicates 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates 工厂可以组合使用
接下来我们介绍 Spring Cloud GateWay 内置几种 Predicate 的使用
通过请求参数匹配
Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式
server:
port: 88
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.zenghr.cn
predicates:
- Query=url,blog
这样配置,只要请求中包含 url 属性的参数,并且参数值是 blog 即可匹配路由
通过 Cookie 匹配
Cookie Route Predicate 可以接收两个参数,一个是 Cookie name ,一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行
server:
port: 88
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.zenghr.cn
order: 0
predicates:
- Cookie=sessionId, test
通过 Header 属性匹配
Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行
server:
port: 88
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.zenghr.cn
order: 0
predicates:
- Header=X-Request-Id, \d+
通过请求方式匹配
可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由
server:
port: 88
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.zenghr.cn
order: 0
predicates:
- Method=GET
这里我们就只列出一些常用的匹配方式,如果需要查询其他匹配方式,可以查看 SpringCloud Gateway 的官方文档
过滤器 Filter
过滤器就是在请求的传递过程中,对请求和响应做一些手脚
在 Gateway 中,Filter的生命周期只有两个:"pre"
和 "post"
PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
在 Gateway 中,Filter 的作用范围两种:
- GatewayFilter:应用到单个路由或者一个分组的路由上
- GlobalFilter:应用到所有的路由上
定义局部过滤器
局部过滤器是针对单个路由的过滤器,在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器
开发小 Tip :如果不知道自定义可以查看 Gateway 中内置的过滤器,使用 cv 大法 咔咔改造一顿就好了
需求:实现自定义过滤器统计请求耗时
一、第一步编写 Filter 类,根据其内置的过滤器发现名称格式有规律:XxxGatewayFilterFactory
spring:
application:
name: api-gateway
gateway:
routes:
- id: product_route
uri: lb://product-server
predicates:
- Path=/product-server/**
filters:
- StripPrefix=1
下面是自己写的自定义过滤器代码
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
public static final String PARTS_KEY = "parts";
public TimeGatewayFilterFactory() {
super(TimeGatewayFilterFactory.Config.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList("parts");
}
public GatewayFilter apply(TimeGatewayFilterFactory.Config config) {
return new GatewayFilter() {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 不启用情况
if (!config.parts) {
return chain.filter(exchange);
}
// 前置操作
long startTime = System.currentTimeMillis();
exchange.getAttributes().put("startTime", startTime);
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 后置操作
long start = (long) exchange.getAttributes().get("startTime");
System.out.println("接口耗时:" + (System.currentTimeMillis() - start) + "ms");
}));
}
public String toString() {
return GatewayToStringStyler.filterToStringCreator(TimeGatewayFilterFactory.this).append("parts", config.getParts()).toString();
}
};
}
public static class Config {
private boolean parts;
public Config() {
}
public boolean getParts() {
return this.parts;
}
public void setParts(boolean parts) {
this.parts = parts;
}
}
}
二、在指定的路由中添加路由规则
spring:
application:
name: api-gateway
gateway:
routes:
- id: product_route
uri: lb://product-server
predicates:
- Path=/product-server/**
filters:
- StripPrefix=1
- Time=true
访问 product-server 服务时将会打印接口耗时日志
自定义全局过滤器
全局过滤器作用于所有路由,无需配置,通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
编写全局过滤器,继承于 GlobalFilter 类
@Component
public class AuthGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 业务代码...
return chain.filter(exchange);
}
}
Gateway 实现动态路由刷新
提示
通常在Nacos接入了Spring Cloud的Gateway后还需自定义实现动态的路由配置来提供后续更为灵活的接口发布与维护,这里主要记录实现步骤
参考文档:
集成 Nacos 的相关操作请看上文的 注册中心相结合的路由配置方式 ,这里就不再详细说明
Nacos 配置文件
application.yml 配置如下
# 自定义的配置项,用于设置路由信息所载的配置文件,比如这里是 group + dataId
gateway:
dynamicRoute:
dataId: gateway.yaml
group: dev
动态更新配置文件
指定路由配置文件,用于启动时创建Nacos Config文件监听
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
public class DynamicRoutingFileProperties {
/**
* 配置文件id
*/
@Value("${gateway.dynamicRoute.dataId}")
private String dataId;
/**
* 配置分组
*/
@Value("${gateway.dynamicRoute.group}")
private String groupId;
private Long timeout = 3000L;
}
动态路由监听
/**
* 动态路由监听
*
* 添加对对应配置文件更新时的监听,实现动态路由刷新。一般的,为了保证
* 仅在启动时注册指定的对应文件(通常这个文件也是动态配置,这里暂时没
* 有实现当更换路由配置文件时的刷新)更新时对正在运行的路由信息进行刷
* 新。
*
* 该类主要实现yaml解析,并构建definition对象,对于具体的刷新逻辑
* @see DynamicRouteService
*/
@Slf4j
@Component
@RefreshScope
public class DynamicRouteServiceListener implements CommandLineRunner {
@Autowired
private DynamicRouteService dynamicRouteService;
@Autowired
NacosConfigManager nacosConfigManager;
@Autowired
private DynamicRoutingFileProperties dynamicRoutingFileProperties;
/**
* 添加配置文件更新监听
*/
private void dynamicRouteListener () {
try {
ConfigService configService = nacosConfigManager.getConfigService();
// first process ed add listener
processConfigInfo(configService.getConfigAndSignListener(
dynamicRoutingFileProperties.getDataId(),
dynamicRoutingFileProperties.getGroupId(),
dynamicRoutingFileProperties.getTimeout(),
new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
processConfigInfo(configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
}
));
} catch (NacosException e) {
log.error("add config listener fail !!!");
e.printStackTrace();
}
}
/**
* 处理配置信息
*
* @param configInfo 配置string
*/
private void processConfigInfo(String configInfo) {
if (Objects.isNull(configInfo)) return;
// 解析yaml文件获取路由定义
List<RouteDefinition> targetRouteDefinitions = getRouteDefinitionsByYaml(configInfo);
// 更新路由信息
dynamicRouteService.refresh(targetRouteDefinitions);
}
/**
* 通过yaml str解析出route定义
*
* @param configInfo yaml str
* @return RouteDefinition array
*/
private List<RouteDefinition> getRouteDefinitionsByYaml(String configInfo) {
Yaml yaml = new Yaml();
Map<Object, Object> document = yaml.load(configInfo);
List<Map<String, Object>> routeList = (List<Map<String, Object>>) document.get("routes");
List<RouteDefinition> targetRouteDefinitions = new ArrayList<>(routeList.size());
for (Map<String, Object> routeItem : routeList) {
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId((String) routeItem.get("id"));
routeDefinition.setUri(URI.create((String) routeItem.get("uri")));
List<String> predicateStrList = (List<String>) routeItem.get("predicates");
List<PredicateDefinition> predicateDefinitions = predicateStrList.stream().map(PredicateDefinition::new).collect(Collectors.toList());
routeDefinition.setPredicates(predicateDefinitions);
List<String> filterStrList = (List<String>) routeItem.get("filters");
if (CollectionUtils.isNotEmpty(filterStrList)) {
List<FilterDefinition> filterDefinitions = filterStrList.stream().map(FilterDefinition::new).collect(Collectors.toList());
routeDefinition.setFilters(filterDefinitions);
}
Object orderObj = routeItem.get("order");
int order = Objects.isNull(orderObj) ? 0 : (int) orderObj;
routeDefinition.setOrder(order);
targetRouteDefinitions.add(routeDefinition);
}
return targetRouteDefinitions;
}
@Override
public void run(String... args) {
Long startTime = System.currentTimeMillis();
dynamicRouteListener();
Long completeTime = System.currentTimeMillis();
log.info("dynamic router cost {}ms to initialization routes and registered configurer.", completeTime - startTime);
}
}
动态路由服务实现
/**
* 动态路由服务实现
*
* 具体的路由信息变更刷新,因监听文件更新仅可拿到全量的路由配置,
* 为了减轻整体逻辑负担,使用merge逻辑更新definition并发出变
* 更通知。
*
* 虽然仅变更不通知,可以简易的通过全量删除并全量添加即可实现路由
* 更新,但并不保证后续是否存在对历史definition对象的引用,故此
* 处使用更保险的策略。
*
* 又因为merge策略,可能导致对部分definition更新后会影响默认的
* order,所以在添加注册时会填充未标记的order。
*/
@Slf4j
@Component
public class DynamicRouteService implements ApplicationEventPublisherAware {
@Resource
private RouteDefinitionRepository routeDefinitionRepository;
private ApplicationEventPublisher publisher;
/**
* merge更新路由
*
* 保证刷新逻辑不存在线程安全问题,刷新路由并没有很高的性能需求,此处锁住整个refresh方法。
* @param definitions 路由详情集合
*/
public synchronized void refresh(List<RouteDefinition> definitions) {
// 填充生成order
fillTargetRouteOrder(definitions);
// 目标routes id集合
List<String> targetDefIds = definitions.stream().map(RouteDefinition::getId).collect(Collectors.toList());
// 获取现存所有路由map
Map<String, RouteDefinition> aliveRouteMap = getAliveRouteMap();
// 删除失效的routes
removeDefinitions(targetDefIds, aliveRouteMap);
// 更新definitions
updateDefinitions(definitions, aliveRouteMap);
// 添加definitions
createDefinitions(definitions, aliveRouteMap);
// 发布路由已更新时间
publishRouteChangedEvent();
}
/**
* 填充目标路由order
*
* @param definitions 路由详情集合
*/
private void fillTargetRouteOrder(List<RouteDefinition> definitions) {
int order = 1;
for (RouteDefinition route : definitions) {
if (route.getOrder() == 0) {
route.setOrder(order++);
}
}
}
/**
* 发布路由已更新消息
*/
private void publishRouteChangedEvent() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}
/**
* 添加routes
*
* @param definitions 目标routes
* @param aliveRouteMap 当前存活路由map
*/
private void createDefinitions(List<RouteDefinition> definitions, Map<String, RouteDefinition> aliveRouteMap) {
// 获取新添加的definitions
Set<String> aliveRouteIdSet = aliveRouteMap.keySet();
List<RouteDefinition> needCreateDefs =
definitions
.stream()
.filter(route -> !aliveRouteIdSet.contains(route.getId())) // 不存在与当前存活集合
.collect(Collectors.toList());
doCreateDefinitions(needCreateDefs);
}
/**
* 执行添加路由
*
* @param needCreateDefs 需要新增的路由集合
*/
private void doCreateDefinitions(List<RouteDefinition> needCreateDefs) {
needCreateDefs.forEach(createDef -> {
try {
this.routeDefinitionRepository.save(Mono.just(createDef)).subscribe();
log.info("created route: {}", createDef.getId());
} catch (Exception e) {
e.printStackTrace();
log.info("create route {} fail", createDef.getId());
}
});
}
/**
* 更新路由
*
* @param definitions 目标routes
* @param aliveRouteMap 当前存活路由map
*/
private void updateDefinitions(List<RouteDefinition> definitions, Map<String, RouteDefinition> aliveRouteMap) {
Set<String> aliveRouteIdSet = aliveRouteMap.keySet();
List<RouteDefinition> needUpdateDefs =
definitions
.stream()
.filter(route -> aliveRouteIdSet.contains(route.getId())
&& !route.equals(aliveRouteMap.get(route.getId()))) // 当前存活且与当前definition不同则为更新
.collect(Collectors.toList());
doUpdateDefinitions(needUpdateDefs);
}
/**
* 删除并重新创建路由实现更新
*
* route repo无updater的结局方法
* @param needUpdateDefs 需要更新route集合
*/
private void doUpdateDefinitions(List<RouteDefinition> needUpdateDefs) {
needUpdateDefs.forEach(updateDefinition -> {
try {
this.routeDefinitionRepository
.delete(Mono.just(updateDefinition.getId()))
.subscribe();
log.info("removed old route(will be recreate): {}", updateDefinition.getId());
} catch (Exception e) {
e.printStackTrace();
log.info("can't clean route(will be create): {}", updateDefinition.getId());
}
try {
this.routeDefinitionRepository.save(Mono.just(updateDefinition)).subscribe();
log.info("updated route: {}", updateDefinition.getId());
} catch (Exception e) {
e.printStackTrace();
log.info("updated route {} fail", updateDefinition.getId());
}
});
}
/**
* 获取当前存活的路由描述map
*
* @return 当前存活的路由描述map
*/
private Map<String, RouteDefinition> getAliveRouteMap() {
return routeDefinitionRepository
.getRouteDefinitions()
.toStream()
.collect(Collectors.toMap(RouteDefinition::getId, Function.identity()));
}
/**
* 删除剔除的routes
*
* @param targetDefIds 目标route id集合
* @param aliveRouteMap 当前存活的路由map
*/
private void removeDefinitions(List<String> targetDefIds, Map<String, RouteDefinition> aliveRouteMap) {
List<String> removedDefinitionIds =
aliveRouteMap
.keySet()
.stream()
.filter(routeId -> !targetDefIds.contains(routeId)) // 不存在于目标id集合判定为删除
.collect(Collectors.toList());
doRemoveDefinitions(removedDefinitionIds);
}
/**
* 删除剔除的routes
*
* @param removedDefinitionIds 需要被剔除的route id集合
*/
private void doRemoveDefinitions(List<String> removedDefinitionIds) {
removedDefinitionIds.forEach(removedId -> {
this.routeDefinitionRepository
.delete(Mono.just(removedId))
.subscribe();
log.info("removed route: {}", removedId);
});
}
/**
* 开启监听
*
* @param applicationEventPublisher publisher instance
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
}