1. SpringCloud Gateway
先来了解一些概念。
1. 简介
SpringCloud Gateway
是一个建立在Spring
生态之上,基于Spring5
、Spring Boot 2
、Project Reactor
的API网关。目标是提供一个简单但是有效的方式把请求路由到API,并提供像是安全、监控/指标和弹性之类的值得关注的切面。
2. 几个术语
-
Route
:网关的基本构件。由一个ID
,一个目标URI
,一个predicates
的集合,一个filters
的集合组成。当predicates
总体判断是true
的时候,这个route
就算匹配成功了。 -
Predicate
:这是一个Java8 Function Predicate。输入类型是一个Spring FrameWork ServerWebExchange
。这允许您匹配来自HTTP请求的任何内容,比如通过请求头或者请求参数。 -
Filter
:是由特定的工厂类构建的GatewayFilter
的实例。在发送下游请求之前或者之后,你可以在其中对请求或者响应做修改。说明:如果在
route
的URI
里面没有定义端口的话,HTTP和HTTPS的端口默认分别会被解析为为80和443。
3. SpringCloud Gateway是如何工作的
下面的这张图片整体描述了SpringCloud Gateway
是如何工作的:
客户端向SpringCloud Gateway
发送请求,如果Gateway Handler Mapping
判定请求匹配到了一个route
,就把它发送给Gateway Web Handler
,它用这个请求匹配到的特定的过滤器链来处理这个请求。图中Filter
中间虚线的意思是它在代理的请求被发送之前和之后都可以执行逻辑。也就是,先执行所有的前置过滤器逻辑,然后发送代理的请求,请求完成后,再执行后置的过滤器逻辑。
2. 依赖和配置
1. pom.xml
除了spring-cloud-starter-gateway
本身的依赖之外,这里还引入了spring-boot-starter-actuator
来监控路由表,引入了hutool-all
工具包方便对URL进行解析处理。
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.makabaka</groupId>
<artifactId>apigateway</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.2.10.RELEASE</version>
</parent>
f
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.5</version>
</dependency>
</dependencies>
</project>
2. application.yaml
server:
port: 8080
spring:
cloud:
gateway:
globalcors: #跨域配置,这里全部放开,实际使用时再按照需要修改
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
management: #actuator配置
endpoint:
gateway:
enabled: true #启用SpringCloud Gateway监控端点
endpoints:
web:
exposure:
include: gateway #暴露SpringCloud Gateway监控端点
3. 路由
在做路由转发之前,我们先来找一个公共api接口作为被转发的路由,比如这个。用它提供的每日一言的接口来测试,地址是:https://v.api.aa1.cn/api/yiyan/index.php。
1. 先从静态路由开始
1. Predicate
SpringCloud Gateway
内置了多达十二种Predicate
匹配方式来对请求的不同属性进行路由匹配,而且可以混合使用,详细文档可以看这里。我们用path route
进行路由匹配。在application.yaml
里添加如下配置:
spring:
cloud:
gateway:
routes:
- id: one
uri: https://v.api.aa1.cn
predicates:
- Path=/api/yiyan/index.php
启动项目,打开浏览器,访问http://localhost:8080/api/yiyan/index.php
,可以看到一句优美的文字出现在了浏览器里,我这里看到的是
但使主人能醉客,不知何处是他乡。 ——李白
说明访问http://localhost:8080/api/yiyan/index.php
的请求已经被转发到了https://v.api.aa1.cn/api/yiyan/index.php
,我们的第一步成功了。
但是这里我们是直接使用了API接口的url,如果希望把请求http://localhost:8080/api/oneword
转发到https://v.api.aa1.cn/api/yiyan/index.php
,应该怎样配置呢?用Filter
。
2. Filter
Filter
可以按照指定的规则对请求和响应进行修改,SpringCloud Gateway
内置了三十多种Filter
,详细文档可以看这里,使用细节可以参考官方给出的单元测试例子。这里可以用RewritePath Filter
来重写path。修改配置文件:
spring:
cloud:
gateway:
routes:
- id: one
uri: https://v.api.aa1.cn
predicates:
- Path=/api/oneword
filters:
- RewritePath=/api/oneword,/api/yiyan/index.php
重启项目,打开浏览器,访问http://localhost:8080/api/oneword
,这次看到的是
世间无限丹青手,一片伤心画不成。——高蟾
说明访问http://localhost:8080/api/one
的请求被我们配置的RewritePath Filter
重写并转发到了https://v.api.aa1.cn/api/yiyan/index.php
。
2. 改成动态路由
简单掌握了静态路由的配置之后,我们来尝试把路由改为动态配置,第一步先把配置方式从yaml
配置改为使用Java代码进行配置。首先把application.yaml
中的路由配置注释掉,然后定义一个路由的实体类。
public class LocalRoute {
//路由id
private String id;
//api地址
private String url;
//转发的路径
private String path;
//省略get、set和构造方法
}
使用Java代码进行路由配置的关键在于创建一个类去实现RouteDefinitionRepository
接口,并在getRouteDefinitions
方法的实现里配置路由信息。
@Component
public class StaticRouteDefinitionRepositoryImpl implements RouteDefinitionRepository {
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<LocalRoute> localRouteList = new ArrayList<>();
localRouteList.add(new LocalRoute("1", "https://v.api.aa1.cn/api/yiyan/index.php", "/oneword"));
List<RouteDefinition> routeDefinitionList = new ArrayList<>();
for (LocalRoute localRoute : localRouteList) {
RouteDefinition routeDefinition = new RouteDefinition();
PredicateDefinition predicateDefinition = new PredicateDefinition();
//Route
routeDefinition.setId(localRoute.getId());
//处理api的url,拆分为uri和path两部分
URL url = URLUtil.url(localRoute.getUrl());
String uri = url.getProtocol() + "://" + url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
String rewritePath = url.getPath();
try {
routeDefinition.setUri(new URI(uri));
} catch (URISyntaxException e) {
//如果URI格式不正确,跳过这个路由配置
e.printStackTrace();
continue;
}
//Predicate
String localPath = "/api" + localRoute.getPath();//统一加上api前缀便于后续鉴权判断
predicateDefinition.setName("Path");
predicateDefinition.addArg("Path", localPath);
routeDefinition.setPredicates(Collections.singletonList(predicateDefinition));
//Filter
List<FilterDefinition> filterDefinitionList = new ArrayList<>();
//判断path使用通配符的情况,处理重写path配置
if (localRoute.getPath().endsWith("/**")) {
localPath = localPath.replace("/**", "/?(?<segment>.*)");
rewritePath = rewritePath + "/${segment}";
}
//RewritePath Filter
FilterDefinition rewritePathFilterDefinition = new FilterDefinition();
rewritePathFilterDefinition.setName("RewritePath");
rewritePathFilterDefinition.addArg("regexp", localPath);
rewritePathFilterDefinition.addArg("replacement", rewritePath);
filterDefinitionList.add(rewritePathFilterDefinition);
routeDefinition.setFilters(filterDefinitionList);
routeDefinitionList.add(routeDefinition);
}
return Flux.fromIterable(routeDefinitionList);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return null;
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return null;
}
}
保存,重启。这次我们用actuator api
来查看路由表,请求http://localhost:8080/actuator/gateway/routes
,响应如下:
[
{
"predicate": "Paths: [/api/oneword], match trailing slash: true",
"route_id": "1",
"filters": [
"[[RewritePath /api/oneword = '/api/yiyan/index.php'], order = 1]"
],
"uri": "https://v.api.aa1.cn:443",
"order": 0
}
]
打开浏览器,访问http://localhost:8080/api/oneword
,这次看到的是
直道相思了无益,未妨惆怅是清狂。——李商隐
说明我们使用Java代码配置的路由也生效了。
聪明的你肯定想到了,只需要把localRouteList
改为从你需要的地方获取(配置文件、数据库、Redis等等),就可以实现动态的获取路由配置。那么只需要当路由配置发生变化时,我们能刷新路由表,动态路由配置就完成了。
3. 刷新动态路由
SpringCloud Gateway
提供了刷新路由的事件,我们在需要时把这个事件发送给Spring
,就可以刷新路由了。
@Component
public class RefreshRouteService implements ApplicationEventPublisherAware {
@Autowired
ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
//调用这个方法就可以刷新路由了
public void refreshRoutes() {
System.out.println("refresh routes");
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}
测试一下,先增加一个controller
提供刷新路由的接口
@RestController
@RequestMapping("/route")
public class RefreshRouteController {
@Autowired
private RefreshRouteService refreshRouteService;
@GetMapping("/refresh")
public String refresh() {
refreshRouteService.refreshRoutes();
return "已刷新路由表";
}
}
然后把获取路由配置的方式改为从txt文件中读取,这里仅作测试用,所以写的简单粗暴一些
try {
Scanner scanner = new Scanner(new File("C:\Users\Makabaka\Desktop\route.txt"));
while (scanner.hasNextLine()) {
String[] localRouteArr = scanner.nextLine().split(",");
localRouteList.add(new LocalRoute(localRouteArr[0], localRouteArr[1], localRouteArr[2]));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
route.txt
的内容
1,https://v.api.aa1.cn/api/yiyan/index.php,/oneword
启动项目,查看路由表:
[
{
"predicate": "Paths: [/api/oneword], match trailing slash: true",
"route_id": "1",
"filters": [
"[[RewritePath /api/oneword = '/api/yiyan/index.php'], order = 1]"
],
"uri": "https://v.api.aa1.cn:443",
"order": 0
}
]
给route.txt
增加一行搞笑段子的路由
1,https://v.api.aa1.cn/api/yiyan/index.php,/oneword
2,https://v.api.aa1.cn/api/api-wenan-gaoxiao/index.php,/funny
调用http://localhost:8080/route/refresh
,看到返回"已刷新路由表",再来看路由表
[
{
"predicate": "Paths: [/api/oneword], match trailing slash: true",
"route_id": "1",
"filters": [
"[[RewritePath /api/oneword = '/api/yiyan/index.php'], order = 1]"
],
"uri": "https://v.api.aa1.cn:443",
"order": 0
},
{
"predicate": "Paths: [/api/funny], match trailing slash: true",
"route_id": "2",
"filters": [
"[[RewritePath /api/funny = '/api/api-wenan-gaoxiao/index.php'], order = 1]"
],
"uri": "https://v.api.aa1.cn:443",
"order": 0
}
]
请求http://localhost:8080/api/funny?aa1=text
,(注意这个接口需要传参数aa1=text
),返回结果
步步高打火机,哪里不会点哪里,妈妈以后再也不用担心我学习了。
emmm,虽然段子不好笑,但是说明转发成功了,刷新路由也成功了。
4. 功能扩展
我们基于SpringCloud Gateway
的Route
、Predicate
、Filter
实现了动态路由配置,刷新功能。但一个实际使用的API网关的需求可能还包括鉴权、请求记录、流量统计等等。这些都可以使用SpringCloud Gateway
的其它特性,比如GlobalFilters
、HttpHeadersFilters
来实现。