Spring Cloud Gateway 不小心換了個 Web 容器就不能用了,我 TM 人傻了

乾貨滿滿張雜湊發表於2022-03-12

個人創作公約:本人宣告創作的所有文章皆為自己原創,如果有參考任何文章的地方,會標註出來,如果有疏漏,歡迎大家批判。如果大家發現網上有抄襲本文章的,歡迎舉報,並且積極向這個 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
來檢視相容的進度。

微信搜尋“我的程式設計喵”關注公眾號,加作者微信,每日一刷,輕鬆提升技術,斬獲各種offer
image
我會經常發一些很好的各種框架的官方社群的新聞視訊資料並加上個人翻譯字幕到如下地址(也包括上面的公眾號),歡迎關注:

相關文章