美文网首页
Spring Cloud Gateway 不小心换了个 Web

Spring Cloud Gateway 不小心换了个 Web

作者: 干货满满张哈希 | 来源:发表于2022-03-12 08:37 被阅读0次

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 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:

    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-cloud/spring-cloud-gateway/commit/68dcc355119e057af1e4f664c81f77714c5a8a16

    这其实也是为兼容所有的 Web 容器进行铺路。那么,究竟有计划兼容所有的 Web 容器么?是有计划的,还在做,已经做了快 4 年了,应该快做好了,相当于所有的单元测试要重新跑甚至重新设计,可以通过这个 ISSUE:Support running the gateway with other reactive containers besides netty #145
    来查看兼容的进度。

    相关文章

      网友评论

          本文标题:Spring Cloud Gateway 不小心换了个 Web

          本文链接:https://www.haomeiwen.com/subject/phjkdrtx.html