一文搞懂 Spring Cloud Gateway 源码

前言

哈喽,好久不见,鸽了几个月,我回来了!

最近项目上需要做架构设计的优化,某个业务模块发展比较快,我们打算把它拆分出单独的服务。

有关服务拆分的小年前面也分享过:每天学点架构,服务拆分迁移

小年方案设计阶段的时候碰到一个比较棘手的问题:如何迁移共用 API ?

共用 API 是什么?就是指多个业务模块共用的接口,一般是通过参数来区分业务类型。

一开始想到的方案是:由网关服务层支持自定的参数路由规则(当然,前提是你们微服务有网关层)

然而负责网关层同学告诉小年不支持这个能力。而且,共用的 API 不仅只有网关层上游调用,还有服务之间的调用,而这部分是不经过网关层!

所以,这个方案并不能解决后者的问题。

当然,如果在原业务服务一个一个对 API 做切流和转发也是能实现,But!关键是接口的数量不少,而且每个 API 的切流规则可能不尽相同,这样人工开发的工作量属实不少,不太现实。

既然是接口转发,那么业务层直接实现网关功能不就可以了?

业务系统使用的是 Spring Cloud 全家桶,直接可以接入 Spring Cloud Gateway 框架来实现网关层能力,并且它也支持自定义拦截器,通过拦截器来实现参数的分流和灰度。

今天主要分享一下 Spring Cloud Gateway 的基本概念和入门操作,通过阅读源码了解其背后的工作原理。

正文

基本介绍

Spring Cloud Gateway 是 Spring Cloud 体系中的一个 API 网关组件,它基于 Spring Framework 5 和 Spring Boot 2.0 开发,并且构建在Project Reactor(响应式编程模型)之上,因此它也是一种异步非阻塞的 API 网关,适用于高并发场景。

当然,它也具备网关所应有的能力:路由转发、过滤、限流、熔断等。

而在 Spring Boot 2.0 之前网关组件是使用 Netflix Zuul,为什么在 Spring Cloud 的 Finchley 版本之后被引入作为推荐的 API 网关替代 Zuul 呢?

这两者之间的有什么区别?小年就直接贴上 GPT 的回答

Spring Cloud Gateway 和 Zuul 都是微服务架构中常用的API网关,但它们在设计理念、性能、功能等方面存在一些差异。以下是两者的一些主要对比点:

  1. 架构和性能
    • Zuul 1.x 是基于Servlet 2.5,并且是同步阻塞的,不支持长连接,比如WebSockets。
    • Spring Cloud Gateway 基于 Spring WebFlux,使用非阻塞、异步请求处理,支持WebSockets,适合高并发场。
  2. 编程模型
    • Zuul 1.x 使用的是传统的Servlet模型。
    • Spring Cloud Gateway 支持响应式编程,利用了WebFlux框架,可以构建异步的、非阻塞的、事件驱动的服务。
  3. 集成和生态
    • Zuul 是Netflix的开源项目,虽然Spring Cloud有集成Zuul,但Zuul 2.x版本Spring Cloud并未集成,Zuul 2.x是基于Netty的,支持异步非阻塞。
    • Spring Cloud Gateway 是Spring Cloud的子项目,与Spring生态更加紧密集成,提供了更好的开发体验。
  4. 性能优化
    • Zuul 1.x 性能可以通过参数调优提高,但理论上Spring Cloud Gateway 由于其异步非阻塞的特性,不需要太多优化即可达到接近极限的性能。
  5. 功能和扩展性
    • Zuul 提供了基本的路由、过滤功能,但社区活跃度和扩展性相对较低。
    • Spring Cloud Gateway 提供了更丰富的路由、过滤器等扩展点,方便用户定制化配置。
  6. 社区和维护
    • Zuul 1.x 社区活跃度相对较低,且Netflix已经发布了Zuul 2.x,但Spring Cloud没有整合计划。
    • Spring Cloud Gateway 作为Spring Cloud生态的一部分,得到了Spring社区的积极维护和更新。
  7. 跨服务通信
    • Zuul 1.x 作为基于阻塞I/O的API Gateway,性能相对较差。
    • Spring Cloud Gateway 支持异步通信,能进一步提高系统的吞吐量和响应性能。

核心概念

Route(路由)

路由是网关的基本配置单元,它定义了请求如何被发送到后端服务。路由包含了一个或多个断言(Predicate)和一个过滤器(Filter)列表,用于匹配来自客户端的请求并将请求转发到指定的服务。

Predicate(断言)

用于检查请求是否与路由匹配。Spring Cloud Gateway 提供了一系列内置的断言工厂,例如,检查请求头、请求方法、请求路径等。

整理了这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka
面试专题

需要全套面试笔记【点击此处】即可免费获取

Filter(过滤器)

过滤器用于在路由期间对请求和响应进行处理,例如修改请求头、日志记录、认证等。Spring Cloud Gateway 允许开发者自定义过滤器或使用内置的过滤器。

代码示例

示例代码:github.com/Zhang-BigSm…

  • spring-cloud-gateway-demo-eureka:注册中心,负责管理微服务;
  • spring-cloud-gateway-demo-api:网关服务,负责路由转发;
  • spring-cloud-gateway-demo-server: 提供网关转发的 API接口的服务;

重点我们看spring-cloud-gateway-demo-api 模块,引入核心依赖

 

xml

代码解读

复制代码

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>

application.yaml 配置接口的路由转发规则,比如下面配置中的 routeID=payment_route,当请求路径是 /test/**,并且是 GET 请求的,会将请求转发到 localhost:8080。

也就是说请求如果是 GET: localhost:8111/test/payment 会将请求转发到 localhost:8080/test/payment 。

 

yaml

代码解读

复制代码

eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ server: port: 8111 spring: main: web-application-type: reactive application: name: spring-cloud-gateway-demo-api cloud: gateway: routes: - id: payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: localhost:8080/ #匹配后提供服务的路由地址 predicates: - Path=/test/** # 断言,路径相匹配的进行路由 - Method=GET - id: payment_route2 uri: lb://spring-cloud-gateway-demo-server predicates: - Path=/payment/lb/** filters: - RewritePath=/test(?<segment>/?.*), $\{segment}

Spring Cloud Gateway 的入门使用还是比较简单和容易上手,当然它还有很多高级的用法,内置各种断言和过滤器,也可以自定义自己的断言和过滤器,更多的高级特性网上一搜都有。

源码解析

工作原理

下面是 Spring Cloud Gateway 官网的工作原理概述图👇

客户端向 Spring Cloud Gateway 发出请求,Gateway Handler Mapping 处理请求,确定请求与路由是否匹配,然后交给 Gateway Web Handler 处理。

而 Gateway Web Handler 实际上就是一组过滤器(Filter),按顺序执行完所有的 Filter 后,通过 Proxy Filter 转发请求,调用其他服务接口。

对客户端请求的前置处理,其实思想跟 SpringMVC 很相似,如果有了解过 SpringMVC 原理的同学应该知道, DispatchSerlvet 处理请求找到匹配的 HandlerMapping,从而找到相应的 Controller。

而 Spring Cloud Gateway 是通过 DispatcherHandler 遍历所有的 Mapping ,找到匹配路由的 Mapping,再执行对应的 Handler

RoutePredicateHandlerMapping 是对应上图中的 Gateway Handler Mapping,核心代码片段如下:

 

java

代码解读

复制代码

public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { ... @Override protected Mono<?> getHandlerInternal(ServerWebExchange exchange) { ... return Mono.deferContextual(contextView -> { exchange.getAttributes().put(GATEWAY_REACTOR_CONTEXT_ATTR, contextView); return lookupRoute(exchange) // .log("route-predicate-handler-mapping", Level.FINER) //name this .map((Function<Route, ?>) r -> { exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR); if (logger.isDebugEnabled()) { logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r); } exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); return webHandler; }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> { exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR); if (logger.isTraceEnabled()) { logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]"); } }))); }); } }

getHandlerInternal 是整个类的主要方法,判断是否匹配路由,并且返回相应的 webHandler。

再看到核心方法 lookupRoute ,这个是路由匹配的核心关键方法

 

java

代码解读

复制代码

protected Mono<Route> lookupRoute(ServerWebExchange exchange) { return this.routeLocator.getRoutes() // individually filter routes so that filterWhen error delaying is not a // problem .concatMap(route -> Mono.just(route).filterWhen(r -> { // add the current route we are testing exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId()); return r.getPredicate().apply(exchange); }) // instead of immediately stopping main flux due to error, log and // swallow it .doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e)) .onErrorResume(e -> Mono.empty())) // .defaultIfEmpty() put a static Route not found // or .switchIfEmpty() // .switchIfEmpty(Mono.<Route>empty().log("noroute")) .next() // TODO: error handling .map(route -> { if (logger.isDebugEnabled()) { logger.debug("Route matched: " + route.getId()); } validateRoute(route, exchange); return route; }); }

这里有几个关键的步骤:

  1. this.routeLocator.getRoutes() 是获取配置文件中的路由信息,也就是上面例子中在 yaml 中配置的路由规则
  2. concatMap 方法,顺序遍历每一个route,判断当前路由的断言(predicate)是否匹配。
    • 上面也有提过,Spring Cloud Gateway 内置了很多 Predicate,可以针对特殊的请求
    • 断言匹配这里的代码设计其实比较有意思,后面另外文章展开说说
  3. 然后 .next() 方法就会从过滤后的路由中选择第一个匹配的路由。

Predicate

在路由匹配方法的代码中 r.getPredicate().apply(exchange) 判断当前请求是否符合路由断言的规则。

代码示例 yaml 配置中的 routeID=payment_route,断言的条件是 Path=/test/**,Method=GET,使用到了 PathRoutePredicateFactory 和 MethodRoutePredicateFactory 。

Spring Cloud Gateway 内置了比较丰富的断言实现,开发者可以自由发挥。

Spring Cloud Gateway 断言部分的代码设计结构还是比较有意思的,下一篇小年展开讲一讲,这里就先不展开了。

Filter

RoutePredicateHandlerMapping getHandlerInternal 返回 FilteringWebHandler ,然后就执行 handler 中的过滤器

Filter 可以分成两种:GlobalFilterGatewayFilter

Global filters 会被应用到所有的路由上,而 Gateway filter 将应用到单个路由上或者一个分组的路由

Spring Cloud Gateway 内置很多 GatewayFilter,开发者可以根据实际场景使用,而且配置的方式很简单。

比如下面的 route 配置,会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 blue

 

yaml

代码解读

复制代码

spring: cloud: gateway: routes: - id: add_request_header_route uri: https://example.org filters: - AddRequestHeader=X-Request-red, blue

更多的 GatewayFilter 配置可以参考官网:cloud.spring.io/spring-clou…

全局路由 GlobalFilter 有下面几个

ForwardPathFilter

修改请求路径,就是将请求的路径转发到另一个请求路径。比如需要将 /v1/order 转发到 /v2/order

RouteToRequestUrlFilter

把客户端请求路径中的 uri 替换成目标转发的 uri

 

yaml

代码解读

复制代码

routes: - id: my-route uri: http://example.com predicates: - Path=/my-service/**

比如上面配置,客户端请求路径 http://gateway-service/my-service/resource,而 RouteToRequestUrlFilter ` 会解析出请求路径中的 uri (example.com) ,然后把 uri 替换成目标uri,替换成新的请求路径 example.com/my-service/…

ReactiveLoadBalancerClientFilter

Spring Cloud Gateway 作为微服务网关,必定也支持服务集群之间的调用,通过服务注册中心可以通过服务名转发目标服务的接口上。

像上面例子中的配置,uri: lb://spring-cloud-gateway-demo-server,以 lb:// 开头的就是标识微服务间的调用,而且会被 ReactiveLoadBalancerClientFilter 处理。

 

yaml

代码解读

复制代码

spring: main: web-application-type: reactive application: name: spring-cloud-gateway-demo-api cloud: gateway: routes: - id: payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: lb://spring-cloud-gateway-demo-server #匹配后提供服务的路由地址 predicates: - Path=/test/** # 断言,路径相匹配的进行路由 - Method=GET

这个过滤器的处理逻辑其实也比较简单,就是拉取转发的目标服务的注册地址列表,选择一个然后替换成真正的IP地址

这一块的代码是有一些研究的意思,SpringCloud 新版本的客户端负载均衡使用了LoadBalancer 替代了原先 NetFlix Ribbon

NettyRoutingFilter

这个是 Spring Cloud Gateway 最核心的过滤器,它负责与目标服务器之间的通信,简单理解就是,把请求转发到目标服务器的接口上。

当然有些与众不同的是,HTTP的通信协议是基于 Netty 框架实现。

Netty 作为一个高性能的网络框架,相信大家多少都有了解。而 Spring Cloud Gateway 选用 Netty 不仅是本身支持高并发、高吞吐、异步非阻塞的特性, Spring 5 开始全面接入了 Reactor 作为底层的响应式编程框架,并且 Netty 就是 Reactor 的默认网络层实现,所以 Spring Cloud Gateway 选择 Netty 也就成为顺理成章的事情。

NettyWriteResponseFilter

NettyWriteResponseFilter 是对响应信息的一些扩展操作,比如可以修改响应头,响应信息转换、加解密,缓存等。

这里比较有意思的是,可以看到上面图片 Filter 的排序,NettyWriteResponseFilter 排在比较前,但是执行的顺序是最后。这里是使用了 chain.filter(exchange).then 方式,所有 filter 执行完之后再执行 then

 

java

代码解读

复制代码

@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // NOTICE: nothing in "pre" filter stage as CLIENT_RESPONSE_CONN_ATTR is not added // until the NettyRoutingFilter is run // @formatter:off return chain.filter(exchange) .then(Mono.defer(() -> { Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR); ...... })).doOnCancel(() -> cleanup(exchange)) .doOnError(throwable -> cleanup(exchange)); // @formatter:on }

ForwardRoutingFilter

ForwardPathFilter 修改了请求路径之后,由 ForwardRoutingFilter 来转发。当然是直接将请求再交由 dispatcherHandler 进行处理��dispatcherHandler 会根据 path 前缀找到需要目标处理器执行逻辑。

小结

读到这里,相信大家对 Spring Cloud Gateway 的使用以及工作原理和架构设计都有更更进一步的理解。

通过阅读源码,我们学习到 Spring Cloud Gateway 的架构设计和原理,你会发现,其实实现一个网关并没有想象那么复杂,甚至可以说是有趣且有很多值得学习的地方。

简单来说,核心链路的实现其实就是过滤器链的执行框架。

当然Spring Cloud Gateway 集成了 Reactor 框架,如果没有了解过 Reactor 框架的同学,估计最难和最花时间的就在这一part上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值