Spring Cloud Gateway 不小心换了个 Web 容器就不能用了,我 TM 人傻了
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~
本文是我 TM 人傻了的第多少期我忘了,每一期总结一个坑以及对于坑的一些发散性想法,往期精彩回顾:
最近组员修改微服务的一些公共依赖,在某个依赖中需要针对我们微服务使用的 Undertow 容器做一些订制,所以加入了 web 容器 Undertow 的依赖。但是,一般这种底层框架依赖,是要兼顾当前使用的这个项目的 web 容器是否是 Undertow,这位同学在配置类上写了 @Conditional:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Undertow.class })
public class CustomizedUndertowConfiguration {
.......
}
但是加的 undetow 依赖的 scope 没有设置为 provided,导致只要加入这个依赖就会加入 Undertow 的依赖。正好网关也用到了这个依赖,并且我们的网关使用的是 Spring-Cloud-Gateway。这就导致了 Spring-Cloud-Gateway 本身的 Netty 的 Reactive 的 web 容器被替换成了 Undertow 的 Reactive 的 web 容器,从而导致了一系列的 Spring-Cloud-Gateway 不兼容的问题。
为何引入 undetow 依赖就会使异步 web 容器从原来的基于 netty 变为基于 undertow
我们知道,Spring-Cloud-Gateway 其实底层也是基于 Spring Boot 的。首先来看下 Spring Boot 中初始化哪种 web 容器的选择原理:首先第一步是根据类是否存在确定是哪种 WebApplicationType:
public enum WebApplicationType {
/**
* 没有 web 服务,不需要 web 容器
*/
NONE,
/**
* 使用基于 servlet 的 web 容器
*/
SERVLET,
/**
* 使用响应式的 web 容器
*/
REACTIVE;
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
从源码中可以看出,当有 WEBFLUX_INDICATOR_CLASS 并且没有 WEBMVC_INDICATOR_CLASS 以及 JERSEY_INDICATOR_CLASS 的时候,判断为 REACTIVE 环境。如果所有 SERVLET_INDICATOR_CLASSES 就认为是 SERVLET 环境。其实这样也可以看出,如果又引入 spring-web 又引入 spring-webflux 的依赖,其实还是 SERVLET 环境。如果以上都没有,那么就是无 web 容器的环境。在 Spring-Cloud-Gateway 中,是 REACTIVE 环境。
如果是 REACTIVE 环境,就会使用 org.springframework.boot.web.reactive.server.ReactiveWebServerFactory 的实现 Bean 创建 web 容器。那么究竟是哪个实现呢?目前有四个实现(Spring-boot 2.7.x):
- TomcatReactiveWebServerFactory:基于 Tomcat 的响应式 web 容器 Factory
- JettyReactiveWebServerFactory:基于 Jetty 的响应式 web 容器 Factory
- UndertowReactiveWebServerFactory:基于 Undertow 的响应式 web 容器 Factory
- NettyReactiveWebServerFactory:基于 Netty 的响应式 web 容器 Factory
实际会用哪个,看到底哪个 Bean 会注册到 ApplicationContext 中:
ReactiveWebServerFactoryConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
@ConditionalOnClass({ HttpServer.class })
static class EmbeddedNetty {
@Bean
@ConditionalOnMissingBean
ReactorResourceFactory reactorServerResourceFactory() {
return new ReactorResourceFactory();
}
@Bean
NettyReactiveWebServerFactory nettyReactiveWebServerFactory(ReactorResourceFactory resourceFactory,
ObjectProvider<NettyRouteProvider> routes, ObjectProvider<NettyServerCustomizer> serverCustomizers) {
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory();
serverFactory.setResourceFactory(resourceFactory);
routes.orderedStream().forEach(serverFactory::addRouteProviders);
serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));
return serverFactory;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
@ConditionalOnClass({ org.apache.catalina.startup.Tomcat.class })
static class EmbeddedTomcat {
@Bean
TomcatReactiveWebServerFactory tomcatReactiveWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatReactiveWebServerFactory factory = new TomcatReactiveWebServerFactory();
factory.getTomcatConnectorCustomizers()
.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers()
.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers()
.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
@ConditionalOnClass({ org.eclipse.jetty.server.Server.class, ServletHolder.class })
static class EmbeddedJetty {
@Bean
@ConditionalOnMissingBean
JettyResourceFactory jettyServerResourceFactory() {
return new JettyResourceFactory();
}
@Bean
JettyReactiveWebServerFactory jettyReactiveWebServerFactory(JettyResourceFactory resourceFactory,
ObjectProvider<JettyServerCustomizer> serverCustomizers) {
JettyReactiveWebServerFactory serverFactory = new JettyReactiveWebServerFactory();
serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));
serverFactory.setResourceFactory(resourceFactory);
return serverFactory;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
@ConditionalOnClass({ Undertow.class })
static class EmbeddedUndertow {
@Bean
UndertowReactiveWebServerFactory undertowReactiveWebServerFactory(
ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) {
UndertowReactiveWebServerFactory factory = new UndertowReactiveWebServerFactory();
factory.getBuilderCustomizers().addAll(builderCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
}
从原码可以看出,每种配置上都有 @ConditionalOnMissingBean(ReactiveWebServerFactory.class) 以及判断是否有对应容器的 class 的条件,例如:@ConditionalOnClass({ Undertow.class }),@Configuration(proxyBeanMethods = false)是关闭这个配置中 Bean 之间的代理加快加载速度。
由于每个配置都有 @ConditionalOnMissingBean(ReactiveWebServerFactory.class),那么其实能保证就算满足多个配置的条件,最后也只有一个 ReactiveWebServerFactory,那么当满足多个条件时,哪个优先加载呢?这就要看这里的源码:
ReactiveWebServerFactoryAutoConfiguration
@Import({ ReactiveWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ReactiveWebServerFactoryConfiguration.EmbeddedTomcat.class,
ReactiveWebServerFactoryConfiguration.EmbeddedJetty.class,
ReactiveWebServerFactoryConfiguration.EmbeddedUndertow.class,
ReactiveWebServerFactoryConfiguration.EmbeddedNetty.class })
从这里可以看出,是按照 EmbeddedTomcat,EmbeddedJetty,EmbeddedUndertow,EmbeddedNetty 的顺序 Import 的,也就是:只要你的依赖中加入了任何 Web 容器(例如 Undertow),那么最后创建的就是基于那个 web 容器的异步容器,而不是基于 netty 的
为何 Web 容器换了就会有问题
首先, Spring Cloud Gateway 的官方文档中就说了:
Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It does not work in a traditional Servlet Container or when built as a WAR.
就是 Spring Cloud Gateway 只能在 Netty 的环境中运行。这是为什么呢。当初在设计的时候,就假定了容器只能是 Netty,后续开发各种 Spring Cloud Gateway 的内置 Filter 以及 Filter 插件的时候,有很多假设当前就是 Netty 的代码,例如缓存 Body 的 Filter 使用的工具类 ServerWebExchangeUtils:
private static <T> Mono<T> cacheRequestBody(ServerWebExchange exchange, boolean cacheDecoratedRequest,
Function<ServerHttpRequest, Mono<T>> function) {
ServerHttpResponse response = exchange.getResponse();
//在这里,强制转换了 bufferFactory 为 NettyDataBufferFactory
NettyDataBufferFactory factory = (NettyDataBufferFactory) response.bufferFactory();
// Join all the DataBuffers so we have a single DataBuffer for the body
return DataBufferUtils.join(exchange.getRequest().getBody())
.defaultIfEmpty(factory.wrap(new EmptyByteBuf(factory.getByteBufAllocator())))
.map(dataBuffer -> decorate(exchange, dataBuffer, cacheDecoratedRequest))
.switchIfEmpty(Mono.just(exchange.getRequest())).flatMap(function);
}
从源码中可以看到,代码直接认为 response 中的 BufferFactory 就是 NettyDataBufferFactory,其实在其他 Web 容器的情况下,目前应该是 DefaultDataBufferFactory,这样就会有异常。不过在 v3.0.5 之后的版本,已经修复了这个强转,参考:https://github.com/spring-cloud/spring-cloud-gateway/commit/68dcc355119e057af1e4f664c81f77714c5a8a16
这其实也是为兼容所有的 Web 容器进行铺路。那么,究竟有计划兼容所有的 Web 容器么?是有计划的,还在做,已经做了快 4 年了,应该快做好了,相当于所有的单元测试要重新跑甚至重新设计,可以通过这个 ISSUE:Support running the gateway with other reactive containers besides netty #145 来查看兼容的进度。
微信搜索“我的编程喵”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer:
我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:
Spring Cloud Gateway 不小心换了个 Web 容器就不能用了,我 TM 人傻了的更多相关文章
- Spring Cloud gateway 网关服务 一
之前我们介绍了 zuul网关服务,今天聊聊spring cloud gateway 作为spring cloud的亲儿子网关服务.很多的想法都是参照zuul,为了考虑zuul 迁移到gateway 提 ...
- Spring Cloud Gateway实战之一:初探
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于<Spring Cloud Gateway实 ...
- CVE-2022-22947 Spring Cloud Gateway SPEL RCE复现
目录 0 环境搭建 1 漏洞触发点 2 构建poc 3 总结 参考 0 环境搭建 影响范围: Spring Cloud Gateway 3.1.x < 3.1.1 Spring Cloud Ga ...
- 从0开始构建你的api网关--Spring Cloud Gateway网关实战及原理解析
API 网关 API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题 ...
- Spring cloud gateway
==================================为什么需要API gateway?==================================企业后台微服务互联互通, 因为 ...
- Spring Cloud Gateway Ribbon 自定义负载均衡
在微服务开发中,使用Spring Cloud Gateway做为服务的网关,网关后面启动N个业务服务.但是有这样一个需求,同一个用户的操作,有时候需要保证顺序性,如果使用默认负载均衡策略,同一个用户的 ...
- 简单尝试Spring Cloud Gateway
简单尝试Spring Cloud Gateway 简介 Spring Cloud Gateway是一个API网关,它是用于代替Zuul而出现的.Spring Cloud Gateway构建于Sprin ...
- spring cloud gateway的stripPrefix配置
序 本文主要研究下spring cloud gateway的stripPrefix配置 使用zuul的配置 zuul: routes: demo: sensitiveHeaders: Access-C ...
- Spring Cloud Gateway中异常处理
最近我们的项目在考虑使用Gateway,考虑使用Spring Cloud Gateway,发现网关的异常处理和spring boot 单体应用异常处理还是有很大区别的.让我们来回顾一下异常. 关于异常 ...
随机推荐
- ApacheCN Python 译文集 20211108 更新
Think Python 中文第二版 第一章 编程之路 第二章 变量,表达式,语句 第三章 函数 第四章 案例学习:交互设计 第五章 条件循环 第六章 有返回值的函数 第七章 迭代 第八章 字符串 第 ...
- java命令- (学习)jps
jps(Java Virtual Machine Process Status Tool) 是java提供的一个显示当前所有java进程pid的命令,适合在linux/unix平台上简单察看当前jav ...
- 自定义 RestTemplate 异常处理 (转)
转自:https://ethendev.github.io/2018/11/06/RestTemplate-error-handler/ 一些 API 的报错信息通过 Response 的 body返 ...
- 使用Reachability监测网络变化-陈鹏
在appdelegate里面添加观察者,并启动监测 // 使用通知中心监听kReachabilityChangedNotification通知 [[NSNotificationCenter defau ...
- 【C++】近期C++特性进阶学习总结(一)
前言 C++的特性多的数不胜数,语言标准也很多,所以不定期对近期所学的C++知识进行总结,是对自身知识体系检查的良好机会,顺便锻炼一下写博客的文笔 三/五/零之法则 三之法则:如果某个类需要用户定义的 ...
- C++输入多行数据
动机 编程题常用需求,比如输入两行数据. 解决思路:使用getline 程序 #include <iostream> #include <vector> #include &l ...
- Docker使用Dockerfile构建新的镜像
构建镜像步骤; 1.创建Dockerfile文件,该文件是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明. vim Dockerfile //每一个指令都会在镜像上创建一个新 ...
- 5、前端--js常量、变量、5种基本数据类型(number string boolean undefined object)、运算符、流程控制、三元运算符、函数、自定义对象、内置对象、BOM操作
变量与常量 在JS中声明变量需要使用关键字 老版本 var(全部都是全局变量) 新版本 let(可以声明局部变量) # 推荐使用let(其实问题不大) 在JS中声明常量也需要使用关键字 const # ...
- Spring Security配置个过滤器也这么卷
以前胖哥带大家用Spring Security过滤器实现了验证码认证,今天我们来改良一下验证码认证的配置方式,更符合Spring Security的设计风格,也更加内卷. CaptchaAuthent ...
- Welcom to my studyspace! 欢迎来到我的学习空间!
由于新手,博客才刚开始起步,用于记录我的学习,博客的建设后续需要将其完善 对我的博客进行diy 用于分享我的学习经历,一些笔记,还有一些小小的心得
