API网关理解

项目背景介绍:

首先介绍一下项目背景,这个项目是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 实例。在这里,您可以在发送下游请求之前或之后修改请求和响应。

过滤器也很好理解,就是我们转发了地址,我们可以对这个请求进行一下自己的加工,比如在请求头上加一点标识,这也是后面流量染色的思路。

两种配置方式:

  1. 配置式(方便、规范)
    1. 简化版
    2. 全称版

这个配置式其实就是在配置文件中写配置项,这样写的好处就很明白了简单,固定,但是不灵活

  1. 编程式(灵活、相对麻烦)

这个相对麻烦,但是灵活,我们还可以利用装饰器的设计模式进行增强

讲完了三个核心概念和两种配置方式,下面就具体讲网关的作用:


 网关的作用:

路由:

进行一个转发,上面已经介绍过了

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 to 1.
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分析了一下这段代码:

  1. 方法签名:

    public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) { 

    这个方法返回一个 Mono<Void>,表示一个异步操作的结果。它接收 ServerWebExchange 和 GatewayFilterChain 作为参数。

  2. 获取原始响应:

    ServerHttpResponse originalResponse = exchange.getResponse(); DataBufferFactory bufferFactory = originalResponse.bufferFactory(); HttpStatus statusCode = originalResponse.getStatusCode(); 
    • 获取当前的 HTTP 响应对象和数据缓冲工厂。
    • 检查响应状态码,如果是 HttpStatus.OK,则继续进行装饰。
  3. 响应装饰器:

    ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { 
    • 创建一个 ServerHttpResponseDecorator 的实例,对原始响应进行装饰。
    • 重写 writeWith 方法,以便在响应体被写入时执行自定义逻辑。
  4. 处理响应体:

    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,则记录错误信息。
  5. 设置装饰过的响应:

    return chain.filter(exchange.mutate().response(decoratedResponse).build()); 
    将装饰过的响应设置回 ServerWebExchange 中,并继续调用过滤链。
  6. 异常处理:

    } catch (Exception e) { log.error("网关处理响应异常" + e); return chain.filter(exchange); } 
    捕获异常并记录错误,确保在出现异常时仍然返回响应。

我再举一个生活中的例子来解释到底为什么上面这个案例可以先让 chain.filter(exchange)这个方法执行完再接着执行后面的打印日志(这个不就是我们想要的嘛)

  1. 提交订单(发送请求)

    • 你在外卖应用上选择并提交了订单。这就像你发起了一个HTTP请求。
  2. 餐厅准备菜品(鉴权或其他操作)

    • 餐厅接到订单后开始准备菜品。在这个阶段,可能会涉及到各种操作,比如确认订单、准备食材等。这相当于在接口的处理过程中进行鉴权、验证请求等操作。
  3. 外卖小哥送餐(执行chain.filter(exchange)

    • 当餐厅准备好菜品后,外卖小哥会把餐送到你手中。这个过程就像Flux中的chain.filter(exchange),它负责将请求传递到下一个处理环节。
  4. 拿到外卖并享用(执行写日志的操作)

    • 最后,你拿到外卖并开始享用美食,这就相当于在接口处理完成后执行日志记录、返回响应等操作。

其实是利用到了Flux这个东西提前把请求给完成了,我们才能继续执行后面写日志的操作。

(先简单这样理解,Flux这个东西我还没学过,前端react也不是很了解

先留个todo) 

todo
最后还有一个知识点:就是上面的装饰器设计模式:

装饰器设计模式就是在原有的基础上增强。感觉蛮好理解这个东西

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值