关于springcloud:Spring-Cloud-Gateway-不小心换了个-Web-容器就不能用了我-TM-人傻了

81次阅读

共计 8319 个字符,预计需要花费 21 分钟才能阅读完成。

集体创作公约:自己申明创作的所有文章皆为本人原创,如果有参考任何文章的中央,会标注进去,如果有疏漏,欢送大家批评。如果大家发现网上有剽窃本文章的,欢送举报,并且踊跃向这个 github 仓库 提交 issue,谢谢反对~

本文是我 TM 人傻了的第多少期我忘了,每一期总结一个坑以及对于坑的一些发散性想法,往期精彩回顾:

  • 降级到 Spring 5.3.x 之后,GC 次数急剧减少,我 TM 人傻了
  • 这个大表走索引字段查问的 SQL 怎么就成全扫描了,我 TM 人傻了
  • 获取异样信息里再出异样就找不到日志了,我 TM 人傻了
  • spring-data-redis 连贯透露,我 TM 人傻了
  • Spring Cloud Gateway 没有链路信息,我 TM 人傻了
  • Spring Cloud Gateway 雪崩了,我 TM 人傻了
  • 启用 Spring-Cloud-OpenFeign 配置可刷新,我的项目无奈启动,我 TM 人傻了
  • spring-data-redis 上百万的 QPS 压力太大连贯失败,我 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:

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 })

从这里能够看出,是依照 EmbeddedTomcatEmbeddedJettyEmbeddedUndertowEmbeddedNetty 的程序 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

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-clo…

这其实也是为兼容所有的 Web 容器进行铺路。那么, 到底有打算兼容所有的 Web 容器么 ?是有打算的,还在做,曾经做了快 4 年了,应该快做好了,相当于所有的单元测试要从新跑甚至从新设计,能够通过这个 ISSUE:[Support running the gateway with other reactive containers besides netty #145
](https://github.com/spring-clo…) 来查看兼容的进度。

微信搜寻“我的编程喵”关注公众号,加作者微信,每日一刷,轻松晋升技术,斩获各种 offer

我会常常发一些很好的各种框架的官网社区的新闻视频材料并加上集体翻译字幕到如下地址(也包含下面的公众号),欢送关注:

  • 知乎:https://www.zhihu.com/people/…
  • B 站:https://space.bilibili.com/31…

正文完
 0