项目背景介绍:
首先介绍一下项目背景,这个项目是API开发平台,需要完成的接口的功能是:统计谁调用了这个接口,并且将这个接口的调用次数+1,剩余次数-1。
首先看到这个需求第一反应:
得先建个表,建一张用户接口关系表
并且分析用户和接口时多对多关系,一个用户可以调用多个接口,一个接口也可以被多个用户调用。这里什么建表就简单提一句,不是这个文章的重点。
当我们建完表之后,我们再来思考,如果完成这个功能?
在我没有接触API网关之前,第一反应就是AOP,每个接口次数都要加一嘛,这不是很简单用AOP嘛,
后面看了鱼皮老师画的图:
如果单纯只在这个项目中用AOP,就会发现,不太行,因为我们调用的接口有可能来自不同的项目。
每个项目都写一次AOP嘛。这肯定不够优雅,并且也有可能出错。
所以就引入了这个API网关的概念。
给我第一感觉这更像是一个更大的拦截器,
原来写代码都是在一个单体项目里,并且都是一个一个接口为单位去思考问题
这个项目需要以一个更大的视角来看问题了。
介绍先到这里,下面开始API网关的介绍及使用:
API网关:
先贴一个官网:Spring Cloud 网关 --- Spring Cloud Gateway
该项目提供了一个库,用于在 Spring WebFlux 或 Spring WebMVC 之上构建 API 网关。Spring Cloud Gateway 旨在提供一种简单而有效的方式来路由到 API,并为它们提供跨领域关注点,例如:安全性、监控/指标和弹性。
Features 特征
Spring Cloud Gateway features:
Spring Cloud Gateway功能:
-
Built on Spring Framework and Spring Boot
基于 Spring Framework 和 Spring Boot 构建 -
Able to match routes on any request attribute.
能够在任何请求属性上匹配路由。 -
Predicates and filters are specific to routes.
谓词和筛选器特定于路由。 -
Circuit Breaker integration.
断路器集成。 -
Spring Cloud DiscoveryClient integration
Spring Cloud Discovery客户端集成 -
Easy to write Predicates and Filters
易于编写的谓词和过滤器 -
Request Rate Limiting 请求速率限制
-
Path Rewriting 路径重写
这些都是官网的介绍和特征。
大概理解下来就是这个网关可以帮我们转发路径,
就比如我后端的接口地址是:http://localhost:8123/api/name
但是我肯定不能直接把我这个暴露出去,一个因为不安全,一个就是如果我暴露出去,那我上面提到的需求不是做不了了。
这个API网关就可以帮我们转发,比如这个网关项目的地址是:http://localhost:8090
然后我们在配置文件中定于路由的匹配规则就可以定向到http://localhost:8123/api/name
并且呢,我们转发了这个路由,我们就可以做一些我们想要的操作在上面,这个后面会介绍。
介绍完网关的特征和基础概念之后,下一步就是解释一下网关的三个核心概念和两种配置方式:
网关的三个核心概念和两种配置方式:
路由:
网关的基本构建块。它由 ID、目标 URI、谓词集合和筛选器集合定义。如果聚合谓词为 true,则匹配路由。
这个很好理解,就和前端的路由是一样的,当你访问某个url的时候帮你进行跳转到指定的页面,
API网关的路由就是当你访问某个url的时候跳转到特定的url。
谓词:
这是 Java 8 函数谓词。输入类型是 Spring Framework ServerWebExchange。这使您可以匹配 HTTP 请求中的任何内容,例如标头或参数
谓词的概念我刚上来也懵了,后面看了一下,可以把这个当成if,我们上面说会匹配到特定的url,但是并不是所有的路径都需要获取,或者换句话说,我的配置文件中用了很多的路由规则,就是靠这个匹配,
routes:
- id: api_route
uri: http://localhost:8123
predicates:
- Path=/api/**
这是其中的一个路由,下面的predicates就是谓词,当你的这个是/api结尾并且后面是两个**说明任何路径都能匹配,当你匹配上这个路径之后就给你转发到localhost:8123这个地址去
过滤器:
这些是使用特定工厂构建的 GatewayFilter 实例。在这里,您可以在发送下游请求之前或之后修改请求和响应。
过滤器也很好理解,就是我们转发了地址,我们可以对这个请求进行一下自己的加工,比如在请求头上加一点标识,这也是后面流量染色的思路。
两种配置方式:
- 配置式(方便、规范)
- 简化版
- 全称版
这个配置式其实就是在配置文件中写配置项,这样写的好处就很明白了简单,固定,但是不灵活
- 编程式(灵活、相对麻烦)
这个相对麻烦,但是灵活,我们还可以利用装饰器的设计模式进行增强
讲完了三个核心概念和两种配置方式,下面就具体讲网关的作用:
网关的作用:
路由:
进行一个转发,上面已经介绍过了
Spring Cloud Gateway
里面这个路由谓词工厂有很多的谓词操作,可以进行使用。
负载均衡:
在nginx中也听过这个概念
就是一个更大的操作,对于一个集群来说,比如其中有一台服务器请求压力过大,就将一部分的请求转发到其它服务器上。
统一处理跨域
网关统一处理跨域,不用在每个项目里单独处理
这个在springbMVC中肯定有见过类似的表达式,就是配置跨域。
统一鉴权:
这里的统一鉴权应该要结合具体的实现逻辑,可以和AOP一起理解
这里可以和AOP一起理解是什么意思呢,就是你可以把AOP写的代码直接复制粘贴到这个网关的过滤器中
发布控制:
不同的接口分配不同的权值,比如要测试一个接口,就给这个接口分配较少的权值进行测试,等测试没问题再调回来或者像圆梦之星的图,新出的图有更多的机会被匹配到是一个道理
流量染色:
流量染色,听名字好唬人
我们来看这个的中文翻译:
标头添加到所有匹配请求的下游请求的标头中
其实就是给这个请求头打个标记
复述一个鱼皮老师讲的例子
比如有个人绕过了网关知道了我们后端服务器的地址
直接进行访问,那我们做的鉴权就没有用了。
我们可以怎么办呢
就是用这个流量染色,我们给正常经过我们网关的请求头里面打上标记
比如flag = 一串加密后的东西
我们后端服务器再进行一次校验,有这个标记,允许访问,没有,就直接拒绝
接口保护:
限制请求:
黑白名单,违规的用户不能访问
信息脱敏:
保护用户信息
降级:
如果你需要转化的页面访问出现问题,就让用户访问我们实现准备好的降级页面
限流:
限流简单理解就是当请求服务器的请求太多,稍微平衡一下。
限流这里的参数可能有点不理解:
首先想理解,需要先知道两个算法:漏桶算法和令牌桶算法
漏桶算法:很好理解,可以理解为一个漏斗,请求以匀速被处理,当这个请求很多的时候,这个漏斗就会溢出来,那溢出来的请求就不要了
令牌桶算法:也是一个桶,这个桶比较高级一点,
我们往这个桶里匀速的放入令牌,每个请求来拿走一些令牌(不一定是一个),然后如果令牌没有了,那么请求就等待
上面使用的算法就是令牌桶算法:
The
redis-rate-limiter.replenishRate
property defines how many requests per second to allow (without any dropped requests). This is the rate at which the token bucket is filled.redis-rate-limiter.replenishRate
属性定义每秒允许多少个请求(没有任何丢弃的请求)。这是令牌桶的填充率。(这个可以理解为多块的往里面放令牌)
The
redis-rate-limiter.burstCapacity
property is the maximum number of requests a user is allowed in a single second (without any dropped requests). This is the number of tokens the token bucket can hold. Setting this value to zero blocks all requests.redis-rate-limiter.burstCapacity
属性是用户在一秒钟内允许的最大请求数(没有任何丢弃的请求)。这是令牌存储桶可以容纳的令牌数。将此值设置为零将阻止所有请求。(这个就是桶最多能有多少令牌)
The
redis-rate-limiter.requestedTokens
property is how many tokens a request costs. This is the number of tokens taken from the bucket for each request and defaults to1
.redis-rate-limiter.requestedTokens
属性是请求花费的令牌数量。这是每个请求从存储桶中获取的令牌数,默认为1
。(这个就是每个请求需要几个令牌)
统一日志:
类似于我刚刚开始学习AOP的第一个案例:对数据库操作进行一个记录,比如记录什么时间点,谁,做了什么事
讲完了这个API网关的作用:
下面就是在具体项目中的应用了:
具体项目中的应用:
在项目中一般采取编程式和配置式向结合的方式来实现。
1:首先做一个大的路由配置:
server:
port: 8090
spring:
main:
web-application-type: reactive
cloud:
gateway:
routes:
- id: api_route
uri: http://localhost:8123
predicates:
- Path=/api/**
logging:
level:
org:
springframework:
cloud:
gateway: trace
这里就是上面看到的了,给用户的接口是localhost:8090/api
然后进行路由匹配到这个localhost:8123/api/**
后面就是真正的服务器接口地址。
这里还有一个配置logging:那个
这个就是降低日志级别,降到最低,把所有的日志信息都进行输出。
2:具体配置一个全局拦截器来执行我们想要的操作:
这个就是全局过滤器的编程式代码。直接复制
解释一下这个
ServerWebExchange exchange:
- 定义:
ServerWebExchange
是 Spring WebFlux 中的一个接口,表示一次 HTTP 请求的上下文。它包含了请求和响应的所有信息。- 内容:
- 请求信息:可以获取请求的 URI、请求头、请求体等信息。
- 响应信息:可以设置响应的状态码、响应头、响应体等。
- 会话信息:可以访问和修改会话属性。
通过
exchange
,你可以在过滤器中访问请求的详细信息,并根据需要对请求或响应进行修改。我还没学过这个WebFlux,我现在就简单的理解为一个HttpServletRequest
里面包含了请求的信息。这里的上下文,在操作系统的书里也有出现过就是信息的意思。
GatewayFilterChain chain:
- 定义:
GatewayFilterChain
是一个接口,表示一系列过滤器的链条。在 Spring Cloud Gateway 中,过滤器可以是全局的或特定于路由的。- 功能:
- 通过调用
chain.filter(exchange)
,你可以将请求传递给下一个过滤器或目标服务。- 这使得过滤器可以按顺序执行,并且在每个过滤器中,你可以在请求到达目标服务之前或响应返回之前执行逻辑。
这个直接和AOP里面那个切点一起理解就行,放行就是执行下一步这个意思。
具体的代码:
private static ArrayList<String> IP_WHITE_LIST = new ArrayList<>();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1:请求日志
final ServerHttpRequest request = exchange.getRequest();
log.info("请求唯一标识:"+request.getId());
final URI uri = request.getURI();
log.info("请求路径:"+uri);
final HttpMethod method = request.getMethod();
log.info("请求方法:"+method);
MultiValueMap<String, String> queryParams = request.getQueryParams();
log.info("请求参数:"+queryParams);
String sourceAddress = request.getLocalAddress().getHostString();
log.info("请求地址:"+sourceAddress);
ServerHttpResponse response = exchange.getResponse();
//2:访问控制 -黑白名单
IP_WHITE_LIST.add("127.0.0.1");
if(!IP_WHITE_LIST.contains(sourceAddress)){
handleNoAuth(response);
}
//3:用户鉴权
//4:从数据库中查询模拟接口是否存在
//5:请求转化,调用模拟接口
final Mono<Void> filter = chain.filter(exchange);
return handleResponse(exchange,chain);
}
这段代码的整体逻辑就是(其实单看这个项目完成用户调用接口次数+1根本不需要写这么多,这里作为一个学习的案例,调用一下里面的方法加深印象):
先请求日志,就是看一下请求的信息
再进行访问控制就是设置一个白名单
用户鉴权和查询接口是否存在后面好像鱼皮老师有其它办法,我这里先留一个todo
请求转化这一步就挺难的了
其实第五步后面还有两步就是调用接口次数+1,和打印日志
但是出现了一个问题:
在我和GPT大战500回合下,我总算是领悟到了一点皮毛。
首先描述一下这个问题:
先精简一下这个filter的代码:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
一系列鉴权操作
chain.filter(exchange);
打印日志和操作数据库
}
我的本意是想就是在执行了这个放行操作之后呢,再打印日志,这也是肯定的,这个操作都没执行打印啥日志,可是我碰到了这个问题,就是我这个代码好像是异步执行的,就是先打印了日志才执行完这个方法
反应式编程:
反应式编程和命令式编程是两种方式
命令式编程就是我们常见的,一行一行代码执行下来,最后输出结果
反应式编程就是这里Mono这个例子
chain.filter(exchange)
返回的是一个 Mono<Void>
,这意味着它会异步执行,而不是立即执行。因此,后面的日志打印和数据库操作会在 chain.filter(exchange)
被调用后立即执行,而不等待过滤器链的完成
换句话说就是执行chain.filter(exchange)这个方法太慢了,你这一个进程就一直堵在这里,后面的操作都没法执行了,然后程序就先执行下面的打印日志操作了。
再举一个例子:
你有过订阅报纸或者杂志的经历吗?互联网的确从传统的出版发行商那儿分得了一杯羹,但是在过去,订阅报纸确实是了解时事的最佳方式。那时,我们每天早上都会收到一份最新的报纸,并在早饭时间或上班路上阅读。
现在假设一下,在支付完订阅费用之后,几天的时间过去了,你却没有收到任何报纸。又过了几天,你打电话给报社的销售部门询问为什么还没有收到报纸,他们告诉你因为你支付的是一整年的订阅费用,而现在这一年还没有结束,当这一年结束时,你肯定可以一次性完整地收到它们,你会觉得他们有多么不可理喻。值得庆幸的是,这并非订阅的真正运作方式。报纸都具有一定的时效性。在出版后报纸需要及时投递,以确保读者阅读到的内容仍然是新鲜的。此外,你在阅读最新一期的报纸时,记者们正在为未来的某一期报纸撰写内容,同时印刷机正在满速运转,印刷下一期的内容————一切都是并行的。
在开发应用程序代码时,我们可以编写两种风格的代码一一命令式和反应式。
- 命令式(
imperative
)的代码非常像上文所提到的那个荒谬的、假想的报纸订阅方式。它由一组串行的任务组成,每次只运行一项任务,每个任务又都依赖于前面的任务。教据会按批次进行处理,在前一项任务还没有完成对当前数据批次的处理时,不能将这些数据递交给下一项处理任务。- 反应式(
reactive
) 的代码则很像真实的报纸订阅方式。它会定义一组用来处数据的任务,但是这些任务可以并行。每项任务处理数据的一个子集,并且在将结果交给处理流程中下一项任务的同时,继续处理数据的另一个子集。
其实到这里我也只是单纯理解了命令式编程和反应式编程的概念
我们具体来看鱼皮老师的解决方法:
/**
* 处理响应
*
* @param exchange
* @param chain
* @return
*/
public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpResponse originalResponse = exchange.getResponse();
// 缓存数据的工厂
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
// 拿到响应码
HttpStatus statusCode = originalResponse.getStatusCode();
if (statusCode == HttpStatus.OK) {
// 装饰,增强能力
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
// 等调用完转发的接口后才会执行
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
log.info("body instanceof Flux: {}", (body instanceof Flux));
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
// 往返回值里写数据
// 拼接字符串
return super.writeWith(
fluxBody.map(dataBuffer -> {
// 7. 调用成功,接口调用次数 + 1 invokeCount
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);//释放掉内存
// 构建日志
StringBuilder sb2 = new StringBuilder(200);
List<Object> rspArgs = new ArrayList<>();
rspArgs.add(originalResponse.getStatusCode());
String data = new String(content, StandardCharsets.UTF_8); //data
sb2.append(data);
// 打印日志
log.info("响应结果:" + data);
return bufferFactory.wrap(content);
}));
} else {
// 8. 调用失败,返回一个规范的错误码
log.error("<--- {} 响应code异常", getStatusCode());
}
return super.writeWith(body);
}
};
// 设置 response 对象为装饰过的
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
return chain.filter(exchange); // 降级处理返回数据
} catch (Exception e) {
log.error("网关处理响应异常" + e);
return chain.filter(exchange);
}
}
让GPT分析了一下这段代码:
方法签名:
public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
这个方法返回一个
Mono<Void>
,表示一个异步操作的结果。它接收ServerWebExchange
和GatewayFilterChain
作为参数。获取原始响应:
ServerHttpResponse originalResponse = exchange.getResponse(); DataBufferFactory bufferFactory = originalResponse.bufferFactory(); HttpStatus statusCode = originalResponse.getStatusCode();
- 获取当前的 HTTP 响应对象和数据缓冲工厂。
- 检查响应状态码,如果是
HttpStatus.OK
,则继续进行装饰。响应装饰器:
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
- 创建一个
ServerHttpResponseDecorator
的实例,对原始响应进行装饰。- 重写
writeWith
方法,以便在响应体被写入时执行自定义逻辑。处理响应体:
if (body instanceof Flux) { Flux<? extends DataBuffer> fluxBody = Flux.from(body); return super.writeWith( fluxBody.map(dataBuffer -> { byte[] content = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(content); DataBufferUtils.release(dataBuffer); String data = new String(content, StandardCharsets.UTF_8); log.info("响应结果:" + data); return bufferFactory.wrap(content); })); } else { log.error("<--- {} 响应code异常", getStatusCode()); }
- 如果响应体是
Flux
类型,表示它是一个异步流。使用map
操作符来处理每个DataBuffer
。- 读取数据内容,释放内存,并打印日志。
- 返回一个新的
DataBuffer
包装原始内容。- 如果不是
Flux
,则记录错误信息。设置装饰过的响应:
将装饰过的响应设置回return chain.filter(exchange.mutate().response(decoratedResponse).build());
ServerWebExchange
中,并继续调用过滤链。异常处理:
捕获异常并记录错误,确保在出现异常时仍然返回响应。} catch (Exception e) { log.error("网关处理响应异常" + e); return chain.filter(exchange); }
我再举一个生活中的例子来解释到底为什么上面这个案例可以先让 chain.filter(exchange)这个方法执行完再接着执行后面的打印日志(这个不就是我们想要的嘛)
提交订单(发送请求):
- 你在外卖应用上选择并提交了订单。这就像你发起了一个HTTP请求。
餐厅准备菜品(鉴权或其他操作):
- 餐厅接到订单后开始准备菜品。在这个阶段,可能会涉及到各种操作,比如确认订单、准备食材等。这相当于在接口的处理过程中进行鉴权、验证请求等操作。
外卖小哥送餐(执行
chain.filter(exchange)
):
- 当餐厅准备好菜品后,外卖小哥会把餐送到你手中。这个过程就像
Flux
中的chain.filter(exchange)
,它负责将请求传递到下一个处理环节。拿到外卖并享用(执行写日志的操作):
- 最后,你拿到外卖并开始享用美食,这就相当于在接口处理完成后执行日志记录、返回响应等操作。
其实是利用到了Flux这个东西提前把请求给完成了,我们才能继续执行后面写日志的操作。
(先简单这样理解,Flux这个东西我还没学过,前端react也不是很了解
先留个todo)
todo
最后还有一个知识点:就是上面的装饰器设计模式:
装饰器设计模式就是在原有的基础上增强。感觉蛮好理解这个东西