从单体到微服务:Spring Cloud Gateway 断言的深度剖析

Spring Cloud Gateway框架的架构设计非常简单、有效,很多组件的设计都非常值得学习。本文主要对Spring Cloud Gateway三大核心中谓词进行剖析。

前言

在微服务架构还初兴之前,Java Web应用的开发仍然处于单体应用时代。此时的后端是一个完整的、紧密耦合的大系统。每次业务功能的开发,就像积木般的不断堆砌。

彼时的服务更像是一个功能齐全的大超市,所有功能都摆在这一个地方。前端就像顾客,直接和这个大超市打交道,要什么商品(数据)直接去对应的货架(接口)拿就行

单体应用时代,前后端交互方式也相对简单。前端只需将将调用路径的IP路径固定,从而进行接口间的调用即可。

但随着业务的不断发展,这个种单体应用的弊端也逐渐显露。因为一旦某个功能模块出问题,整个系统可能都得停业整顿。同时,扩展新的功能也很麻烦,需要重新规划整个系统的布局。

微服务架构的出现

为了解决单体应用的这些问题,微服务架构 理念应运而生。其实,微服务的架构思想很简单,通俗来看这就好比把大超市拆分成了一个个小便利店,每个便利店只专注于卖一类商品(实现一个特定的功能)。这样做的好处是,每个便利店可以独立管理、独立运营,出了问题也只影响这一个便利店,不会波及其他。而且,要扩展新的商品种类(功能),只需要再开一家新的便利店就行。

不过,这种微服务模式虽然解决了后端服务压力,但却给前端带来了调用艰难的问题。因为此时面对的是一堆微服务。由于不同的服务模块部署在不同的服务器,这导致每个服务的IP地址和端口存在差异,这就迫使前端在调用时,需要维护不同模块的地址信息。这显然与解耦的观念相悖。

要知道,在计算机的世界中,没有什么是加中间层解决不了的 。而此时加在前端后端间的中间层就是网关

网关的兴起

事实上,网关就像景区的导览车,将熟练的知道景区各个景点的位置,当有游客想去对应景点时,直接将其摆渡至对应位置。 当引入网关后 ,前端不用再记住每个后端微服务的地址,而只需要直接与网关交互即可。

换言之,网关就会根据请求地址将其路由至对应后端的微服务,并把响应数据拿回来交给前端。进而大大简化了前端和后端的交互过程,降低了前端开发和维护的难度。

目前主流的网关Spring Cloud Gateway接入后端应用后整个请求轮流转大致如下:

  • Route:代表网关中的一条路由,包含匹配条件和具体的转发目标。
  • Predicate:定义路由的匹配条件,如请求路径、HTTP方法等。
  • Filter:对请求和响应进行处理,可以进行修改、增强或拦截。

本次我们主要聚焦网关中的Predicate的内部原理。而在介绍Predicate内部原理之前,我们先对Java中的Predicate进行一个简单的介绍。

Predicate特性

PredicateJava 8java.util.function包下引入的一个用于处理处理条件判断函数式接口。具体来看Predicate 接口包含四个方法:

  • test(T t):该方法接受一个参数并返回一个布尔值
  • and(Predicate other):与另一个 Predicate 进行组合,实现逻辑与操作。
  • negate():与另一个 Predicate 进行组合,实现逻辑非操作。
  • or(Predicate other):与另一个 Predicate 进行组合,实现逻辑或操作。

test 方法

Predicate 接口通常用于测试一个条件是否成立。例如:

java 复制代码
// Predicate 接口,泛型参数是入参类型,返回布尔值
Predicate<String> predicate = s -> s.contains("utf-8");
boolean flag = predicate.test("utf-8能给你带来收获吗?");
// 打印:god23bin能给你带来收获吗?true
System.out.println("god23bin能给你带来收获吗?" + flag); 

and 方法

为了便于演示,这里准备两个 Predicate

java 复制代码
Predicate<String> startsWithA = (str) -> str.startsWith("A"); // 如果传入的字符串是A开头,则返回 true
Predicate<String> endsWithZ = (str) -> str.endsWith("Z"); // 如果传入的字符串是Z结尾,则返回 true

使用 and 进行组合,操作:

java 复制代码
Predicate<String> startsWithAAndEndsWithZ = startsWithA.and(endsWithZ);
System.out.println(startsWithAAndEndsWithZ.test("ABCDEFZ")); // true
System.out.println(startsWithAAndEndsWithZ.test("BCDEFGH")); // false

negate 方法

使用 negate 进行组合,操作:

java 复制代码
Predicate<String> notStartsWithA = startsWithA.negate();
System.out.println(notStartsWithA.test("ABCDEF")); // false
System.out.println(notStartsWithA.test("BCDEFGH")); // true

or 方法

使用 or 进行组合,操作:

java 复制代码
Predicate<String> startsWithAOrEndsWithZ = startsWithA.or(endsWithZ);
System.out.println(startsWithAOrEndsWithZ.test("ABCDEF")); // true
System.out.println(startsWithAOrEndsWithZ.test("BCDEFGH")); // false

GateWay断言的原理

在网关处理请求的过程中,需要根据前端发来的请求信息,判断该将请求转发到哪个后端服务路径上。而这个进行判断的过程也正是Predicate的最佳应用场景。

为什么在网关中要使用断言呢?当请求进入网关时,会携带路径信息。我们可以根据配置文件中的规则,将这些路径信息作为断言判断的依据。比如使用正则表达式来定义匹配规则,当请求路径符合设定的正则表达式时,断言返回true,否则返回false网关会根据断言结果决定是否放行该请求,这也是网关中过滤器的核心判定逻辑。

GateWay断言配置

Spring Cloud gataWay网关Path的规则大致如下:

yml 复制代码
spring:
  cloud:
    gateway:
      # 配置tote转发规则
      routes:
        - id: order-rote
          uri: lb://server-order
          predicates:
            - Path=/order/**
        - id: product-rote
          uri: lb://server-product
          predicates:
            - Path=/product/**
        - id: bing-rote
          uri: https://cn.bing.com
          predicates:
            - Path=/**

在上述配置文件中定义了三个路由规则,根据不同路径转发到不同的服务或地址。例如:

  1. order-rote:将路径以/order/开头的请求转发到server-order服务。

  2. product-rote:将路径以/product/开头的请求转发到server-product服务。

  3. bing-rote:将所有其他请求转发到https://cn.bing.com

Path断言原理

见识了Spring Cloud中路由断言规则配置后,我们接下来以PathRoutePredicateFactory为例,看看断言工厂类的内部具体实现。

PathRoutePredicateFactory

java 复制代码
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {

// .....省略其他无关代码

@Override
public Predicate<ServerWebExchange> apply(Config config) {
   final ArrayList<PathPattern> pathPatterns = new ArrayList<>();
   synchronized (this.pathPatternParser) {
      pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchTrailingSlash());
      config.getPatterns().forEach(pattern -> {
         PathPattern pathPattern = this.pathPatternParser.parse(pattern);
         pathPatterns.add(pathPattern);
      });
   }
   return new GatewayPredicate() {
      @Override
      public boolean test(ServerWebExchange exchange) {
         PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath());

         PathPattern match = null;
         for (int i = 0; i < pathPatterns.size(); i++) {
            PathPattern pathPattern = pathPatterns.get(i);
            if (pathPattern.matches(path)) {
               match = pathPattern;
               break;
            }
         }

         if (match != null) {
            traceMatch("Pattern", match.getPatternString(), path, true);
            PathMatchInfo pathMatchInfo = match.matchAndExtract(path);
            putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
            exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ATTR, match.getPatternString());
            String routeId = (String) exchange.getAttributes().get(GATEWAY_PREDICATE_ROUTE_ATTR);
            if (routeId != null) {
               // populated in RoutePredicateHandlerMapping
               exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR, routeId);
            }
            return true;
         }
         else {
            traceMatch("Pattern", config.getPatterns(), path, false);
            return false;
         }
      }

     
   };
}
}

PathRoutePredicateFactory这个工厂类中,apply方法是核心,它接收配置信息,返回一个断言对象。具体来看,apply方法会接收配置对象 Config对象,它通过 PathPatternParser 解析路径模式,并将匹配逻辑封装到返回的 GatewayPredicater

而在 GatewayPredicater在断言对象的test方法中,会从服务器端的exchange对象中取出请求(request),进而获取请求的路径(uri)。然后,通过循环迭代的方式,将获取到的路径与配置中的路径模式进行正则表达式匹配(matches方法)。一旦匹配成功,就会记录相关信息,提取匹配到的路径信息、路径 ID 等,并将路径 ID 存储到exchange中,最后返回true,表示该请求符合断言规则,可以放行。

除了路径断言,还有其他类型的断言,比如AfterRoutePredicateFactory。它用于判断请求发生的时间是否在给定时间之后,配置时通常会指定一个时间参数,只有在该时间之后发出的请求才会被放行。而处理我们介绍的,Spring Cloud gateWay还有如下断言工厂:

  1. After Route Predicate Factory:该断言通过接收一个日期时间参数,判断请求时间是否在指定时间之后,从而决定是否匹配路由。例如,可以配置让某个路由在特定日期之后才生效。
  2. Before Route Predicate Factory :与After相反,它判断请求时间是否在指定时间之前。比如,可用于设置某个路由在特定日期之前有效。
  3. Between Route Predicate Factory:用于判断请求时间是否在两个指定的日期时间之间。适用于设置路由在特定时间段内生效的场景。
  4. Cookie Route Predicate Factory:通过检查请求中的 Cookie 是否存在以及其值是否匹配指定模式来进行路由判断。例如,根据用户是否携带特定 Cookie 来决定是否将请求路由到特定服务。
  5. Header Route Predicate Factory :根据请求头中的信息进行路由决策。可以检查请求头是否存在某个字段,或者字段值是否匹配指定模式。如根据请求头中的User-Agent来决定路由。
  6. Host Route Predicate Factory:基于请求的主机名进行路由匹配。可以配置允许特定主机名的请求通过路由,常用于多租户或基于不同域名的路由场景。
  7. Method Route Predicate Factory:根据 HTTP 请求方法(GET、POST、PUT 等)来判断是否匹配路由。例如,只允许 POST 请求通过某个路由。
  8. Path Route Predicate Factory:通过匹配请求的路径来决定是否路由。可以使用 Ant 风格的路径模式进行匹配,灵活地根据不同路径将请求路由到不同服务。
  9. Query Route Predicate Factory:根据请求中的查询参数进行路由判断。可以检查是否存在特定参数,或者参数值是否符合指定条件。
  10. RemoteAddr Route Predicate Factory:根据请求的远程地址来进行路由决策。可用于限制某些 IP 地址段的访问,或者根据客户端的来源 IP 进行路由。
  11. Weight Route Predicate Factory:为不同的路由分配权重,根据权重来决定请求被路由到不同服务的概率。适用于负载均衡和流量分配的场景。

虽然断言类型众多,但核心原理都是类似的,掌握了一种,其他的也就触类旁通了,感兴趣的读者可私下自行对其他类型的断言工厂进行研究。

总结

本文从单体应用到微服务架构的演进历程切入,深入剖析了Spring Cloud Gateway诞生的必然性。在阐述网关三大核心组件时,着重介绍了断言的作用,并以 PathRoutePredicateFactory 为例,详细解析Spring Cloud Gateway中断言的实现机制。

事实上,虽然Spring Cloud gate Way中的断言类型丰富多样,但其核心原理一脉相承。

相关推荐
子玖10 分钟前
go实现通过ip解析城市
后端·go
Java不加班17 分钟前
Java 后端定时任务实现方案与工程化指南
后端
心在飞扬1 小时前
RAG 进阶检索学习笔记
后端
Moment1 小时前
想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍
前端·后端·github
Das1_1 小时前
【Golang 数据结构】Slice 底层机制
后端·go
得物技术1 小时前
深入剖析Spark UI界面:参数与界面详解|得物技术
大数据·后端·spark
古时的风筝1 小时前
花10 分钟时间,把终端改造成“生产力武器”:Ghostty + Yazi + Lazygit 配置全流程
前端·后端·程序员
Cache技术分享1 小时前
340. Java Stream API - 理解并行流的额外开销
前端·后端
初次攀爬者1 小时前
RocketMQ 消息可靠性保障与堆积处理
后端·消息队列·rocketmq
ygxb1 小时前
如何去创建一个规范化的Agent SKIll?
后端·ai编程·claude