徹底找到Spring Webflux與WebMVC 衝突原因

一、問題(

Spring Cloud Gateway Webflux啟動報錯

最近執行一年的閘道器突然報錯,無法啟動,報錯內容如下:

org。springframework。context。ApplicationContextException: Unable to start web server; nested exception is org。springframework。context。ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean。 at org。springframework。boot。web。servlet。context。ServletWebServerApplicationContext。onRefresh(ServletWebServerApplicationContext。java:156) ~[spring-boot-2。2。5。RELEASE。jar:2。2。5。RELEASE] at org。springframework。context。support。AbstractApplicationContext。refresh(AbstractApplicationContext。java:544) ~[spring-context-5。2。4。RELEASE。jar:5。2。4。RELEASE] at org。springframework。boot。web。servlet。context。ServletWebServerApplicationContext。refresh(ServletWebServerApplicationContext。java:141) ~[spring-boot-2。2。5。RELEASE。jar:2。2。5。RELEASE] at org。springframework。boot。SpringApplication。refresh(SpringApplication。java:747) [spring-boot-2。2。5。RELEASE。jar:2。2。5。RELEASE] at org。springframework。boot。SpringApplication。refreshContext(SpringApplication。java:397) [spring-boot-2。2。5。RELEASE。jar:2。2。5。RELEASE] at org。springframework。boot。SpringApplication。run(SpringApplication。java:315) [spring-boot-2。2。5。RELEASE。jar:2。2。5。RELEASE] at org。springframework。boot。SpringApplication。run(SpringApplication。java:1226) [spring-boot-2。2。5。RELEASE。jar:2。2。5。RELEASE] at org。springframework。boot。SpringApplication。run(SpringApplication。java:1215) [spring-boot-2。2。5。RELEASE。jar:2。2。5。RELEASE] at com。ncmed。eos。gateway。GatewayApplication。main(GatewayApplication。java:22) [classes/:na]Caused by: org。springframework。context。ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean。

二、問題分析

從報錯內容上來看是找不到ServletWebServerFactory這個bean導致的錯誤。從

Spring Framework 5。0開始,引入的新的響應式Web框架(Spring WebFlux),與Spring MVC不同,它不需要Servlet API,完全非同步和非阻塞。Spring Cloud Gateway 運用了響應式程式設計(WebFlux),因此它需要依賴於Servlet API,但是啟動的時候為什麼還是去找Servlet呢?百思不得其解。

1、相關程式碼如下:

Spring Cloud 版本:Hoxton。RELEASE

Nacos版本:2。1。0。RELEASE

pom檔案 org。springframework。cloud spring-cloud-starter-gateway <!——2。 nacos-服務發現功能依賴——> com。alibaba。cloud spring-cloud-starter-alibaba-nacos-discovery <!——alibaba nacos config——> com。alibaba。cloud spring-cloud-starter-alibaba-nacos-config org。springframework。cloud spring-cloud-starter-netflix-ribbon org。springframework。cloud spring-cloud-starter-openfeign com。ncmed。eos ncmed-common 1。0-SNAPSHOT com。ncmed。eos ncmed-auth-client 1。0-SNAPSHOT com。ncmed。eos ncmed-system-feign 1。0-SNAPSHOT

package com。ncmed。eos。gateway;import org。springframework。boot。SpringApplication;import org。springframework。boot。autoconfigure。SpringBootApplication;import org。springframework。boot。autoconfigure。jdbc。DataSourceAutoConfiguration;import org。springframework。cloud。client。discovery。EnableDiscoveryClient;import org。springframework。cloud。openfeign。EnableFeignClients;/** * @author 努力的碼農(Liiy) * @email manliyi@163。com * @date 2019/9/23 14:47 */@EnableFeignClients({“com。ncmed。eos。system。feign”,“com。ncmed。eos。auth。client。feign”})@EnableDiscoveryClient// 阻止注入資料庫連線@SpringBootApplication(exclude={DataSourceAutoConfiguration。class})public class GatewayApplication { public static void main(String[] args) { SpringApplication。run(GatewayApplication。class, args); }}

2、原始碼追蹤

由於不清楚工程啟動時為什麼會呼叫Servlet API,只好去追蹤原始碼,瞭解Spring內部真相。由SpringApplication。run(GatewayApplication。class, args)進入追蹤原始碼。

SpringApplication部分原始碼

(按執行順序貼出了部分呼叫關鍵程式碼)

/** * Static helper that can be used to run a {@link SpringApplication} from the * specified source using default settings。 * @param primarySource the primary source to load * @param args the application arguments (usually passed from a Java main method) * @return the running {@link ApplicationContext} */public static ConfigurableApplicationContext run(Class<?> primarySource, String。。。 args) { return run(new Class<?>[] { primarySource }, args);}/** * Static helper that can be used to run a {@link SpringApplication} from the * specified sources using default settings and user supplied arguments。 * @param primarySources the primary sources to load * @param args the application arguments (usually passed from a Java main method) * @return the running {@link ApplicationContext} */public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources)。run(args);}/** * Run the Spring application, creating and refreshing a new * {@link ApplicationContext}。 * @param args the application arguments (usually passed from a Java main method) * @return a running {@link ApplicationContext} */public ConfigurableApplicationContext run(String。。。 args) { StopWatch stopWatch = new StopWatch(); stopWatch。start(); ConfigurableApplicationContext context = null; Collection exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners。starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter。class, new Class[] { ConfigurableApplicationContext。class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch。stop(); if (this。logStartupInfo) { new StartupInfoLogger(this。mainApplicationClass)。logStarted(getApplicationLog(), stopWatch); } listeners。started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners。running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context;}private void refreshContext(ConfigurableApplicationContext context) { refresh(context); if (this。registerShutdownHook) { try { context。registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments。 } }}/** * Refresh the underlying {@link ApplicationContext}。 * @param applicationContext the application context to refresh */protected void refresh(ApplicationContext applicationContext) { Assert。isInstanceOf(AbstractApplicationContext。class, applicationContext); ((AbstractApplicationContext) applicationContext)。refresh();}

以上是Spring Boot啟動相關主要程式碼,重點關注以上程式碼第88行((AbstractApplicationContext) applicationContext)。refresh(),這行程式碼是Spring Boot啟動核心,繼續跟進去。

AbstractApplicationContext部分原始碼

@Overridepublic void refresh() throws BeansException, IllegalStateException { synchronized (this。startupShutdownMonitor) { // Prepare this context for refreshing。 prepareRefresh(); // Tell the subclass to refresh the internal bean factory。 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context。 prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses。 postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context。 invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation。 registerBeanPostProcessors(beanFactory); // Initialize message source for this context。 initMessageSource(); // Initialize event multicaster for this context。 initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses。 onRefresh(); // Check for listener beans and register them。 registerListeners(); // Instantiate all remaining (non-lazy-init) singletons。 finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event。 finishRefresh(); } catch (BeansException ex) { if (logger。isWarnEnabled()) { logger。warn(“Exception encountered during context initialization - ” + “cancelling refresh attempt: ” + ex); } // Destroy already created singletons to avoid dangling resources。 destroyBeans(); // Reset ‘active’ flag。 cancelRefresh(ex); // Propagate exception to caller。 throw ex; } finally { // Reset common introspection caches in Spring‘s core, since we // might not ever need metadata for singleton beans anymore。。。 resetCommonCaches(); } }}/** * Template method which can be overridden to add context-specific refresh work。 * Called on initialization of special beans, before instantiation of singletons。 *

This implementation is empty。 * @throws BeansException in case of errors * @see #refresh() */protected void onRefresh() throws BeansException { // For subclasses: do nothing by default。}

重點關注上面第30行程式碼onRefresh(),Spring Boot在這裡對一些特殊化bean進行初始化,繼續追蹤進去,發現該方法為抽象方法,由其子類進行實現,AbstractApplicationContext 在spring boot中有兩個實現類ReactiveWebServerApplicationContext和ServletWebServerApplicationContext。

從報錯的第二行內容來看(ServletWebServerApplicationContext。onRefresh),啟動的時候是呼叫了ServletWebServerApplicationContext的onRefresh方法,繼續進入ServletWebServerApplicationContext進行追蹤。

ServletWebServerApplicationContext部分原始碼

@Overrideprotected void onRefresh() { super。onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException(“Unable to start web server”, ex); }}private void createWebServer() { WebServer webServer = this。webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this。webServer = factory。getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer()。onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException(“Cannot initialize servlet context”, ex); } } initPropertySources();}/** * Returns the {@link ServletWebServerFactory} that should be used to create the * embedded {@link WebServer}。 By default this method searches for a suitable bean in * the context itself。 * @return a {@link ServletWebServerFactory} (never {@code null}) */protected ServletWebServerFactory getWebServerFactory() { // Use bean names so that we don’t consider the hierarchy String[] beanNames = getBeanFactory()。getBeanNamesForType(ServletWebServerFactory。class); if (beanNames。length == 0) { throw new ApplicationContextException(“Unable to start ServletWebServerApplicationContext due to missing ” + “ServletWebServerFactory bean。”); } if (beanNames。length > 1) { throw new ApplicationContextException(“Unable to start ServletWebServerApplicationContext due to multiple ” + “ServletWebServerFactory beans : ” + StringUtils。arrayToCommaDelimitedString(beanNames)); } return getBeanFactory()。getBean(beanNames[0], ServletWebServerFactory。class);}

重點關注以上38行到40行程式碼,找到了最終丟擲異常的地方。這裡會去找ServletWebServerFactory bean,因為找不到該Bean導致報錯。

深入思考:該類為Spring MVC的上下文啟動類,WebFlux不依賴於Servlet API,為何會呼叫到該類,按道理應該呼叫ReactiveWebServerApplicationContext類。

知識點補充:

ServletWebServerApplicationContext:Servlet Web服務

ReactiveWebServerApplicationContext:響應式Web服務

此刻需要對applicationContext進行追蹤,追蹤其在哪裡進行初始化,返回到SpringApplication原始碼

/** * Run the Spring application, creating and refreshing a new * {@link ApplicationContext}。 * @param args the application arguments (usually passed from a Java main method) * @return a running {@link ApplicationContext} */public ConfigurableApplicationContext run(String。。。 args) { StopWatch stopWatch = new StopWatch(); stopWatch。start(); ConfigurableApplicationContext context = null; Collection exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners。starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter。class, new Class[] { ConfigurableApplicationContext。class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch。stop(); if (this。logStartupInfo) { new StartupInfoLogger(this。mainApplicationClass)。logStarted(getApplicationLog(), stopWatch); } listeners。started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners。running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context;}/** * Strategy method used to create the {@link ApplicationContext}。 By default this * method will respect any explicitly set application context or application context * class before falling back to a suitable default。 * @return the application context (not yet refreshed) * @see #setApplicationContextClass(Class) */protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this。applicationContextClass; if (contextClass == null) { try { switch (this。webApplicationType) { case SERVLET: contextClass = Class。forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class。forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: contextClass = Class。forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { throw new IllegalStateException( “Unable create a default ApplicationContext, please specify an ApplicationContextClass”, ex); } } return (ConfigurableApplicationContext) BeanUtils。instantiateClass(contextClass);}

重點關注第20行程式碼context = createApplicationContext()繼續追蹤,在createApplicationContext方法找到了ApplicationContext初始化程式碼,透過屬性webApplicationType來決定初始化上下文物件是ReactiveWebServerApplicationContext還是ServletWebServerApplicationContext,如果webApplicationType屬性值為SERVLET則初始化ServletWebServerApplicationContext,如果為REACTIVE則初始化ReactiveWebServerApplicationContext,很顯然這裡的屬性值為SERVLET。繼續追蹤webApplicationType在哪裡賦值?

/** * Create a new {@link SpringApplication} instance。 The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} * documentation for details。 The instance can be customized before calling * {@link #run(String。。。)}。 * @param primarySources the primary bean sources * @see #run(Class, String[]) * @see #SpringApplication(ResourceLoader, Class。。。) * @see #setSources(Set) */public SpringApplication(Class<?>。。。 primarySources) { this(null, primarySources);}/** * Create a new {@link SpringApplication} instance。 The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} * documentation for details。 The instance can be customized before calling * {@link #run(String。。。)}。 * @param resourceLoader the resource loader to use * @param primarySources the primary bean sources * @see #run(Class, String[]) * @see #setSources(Set) */@SuppressWarnings({ “unchecked”, “rawtypes” })public SpringApplication(ResourceLoader resourceLoader, Class<?>。。。 primarySources) { this。resourceLoader = resourceLoader; Assert。notNull(primarySources, “PrimarySources must not be null”); this。primarySources = new LinkedHashSet<>(Arrays。asList(primarySources)); this。webApplicationType = WebApplicationType。deduceFromClasspath(); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer。class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener。class)); this。mainApplicationClass = deduceMainApplicationClass();}

繼續關注SpringApplication原始碼,發現SpringApplication初始化時對webApplicationType進行了賦值(上面第30行程式碼),繼續追蹤。

WebApplicationType部分原始碼

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

從原始碼可以看出,預設是SERVLET,當org。springframework。web。reactive。DispatcherHandler能載入,org。springframework。web。servlet。DispatcherServlet和org。glassfish。jersey。servlet。ServletContainer不能載入的時候才是REACTIVE,定位到這裡的時候,發現相關jar包中引入了DispatcherServlet類,導致無法正常使用響應式上下文(ReactiveWebServerApplicationContext)。DispatcherServlet類位於Spring WebMVC的jar包中,在pom檔案中剔除Spring MVC jar包即可。

ClassUtils部分原始碼

/** * Resolve the given class name into a Class instance。 Supports * primitives (like “int”) and array class names (like “String[]”)。 *

This is effectively equivalent to the {@code forName} * method with the same arguments, with the only difference being * the exceptions thrown in case of class loading failure。 * @param className the name of the Class * @param classLoader the class loader to use * (may be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws IllegalArgumentException if the class name was not resolvable * (that is, the class could not be found or the class file could not be loaded) * @throws IllegalStateException if the corresponding class is resolvable but * there was a readability mismatch in the inheritance hierarchy of the class * (typically a missing dependency declaration in a Jigsaw module definition * for a superclass or interface implemented by the class to be loaded here) * @see #forName(String, ClassLoader) */public static Class<?> resolveClassName(String className, @Nullable ClassLoader classLoader) throws IllegalArgumentException { try { return forName(className, classLoader); } catch (IllegalAccessError err) { throw new IllegalStateException(“Readability mismatch in inheritance hierarchy of class [” + className + “]: ” + err。getMessage(), err); } catch (LinkageError err) { throw new IllegalArgumentException(“Unresolvable class definition for class [” + className + “]”, err); } catch (ClassNotFoundException ex) { throw new IllegalArgumentException(“Could not find class [” + className + “]”, ex); }}

如果沒有排除Spring WebMVC相關jar包,還可以採用另外一種方式去指定webApplicationType為REACTIVE

package com。ncmed。eos。gateway;import org。springframework。boot。SpringApplication;import org。springframework。boot。WebApplicationType;import org。springframework。boot。autoconfigure。SpringBootApplication;import org。springframework。boot。autoconfigure。jdbc。DataSourceAutoConfiguration;import org。springframework。cloud。client。discovery。EnableDiscoveryClient;import org。springframework。cloud。openfeign。EnableFeignClients;/** * @author 努力的碼農(Liiy) * @email manliyi@163。com * @date 2019/9/23 14:47 */@EnableFeignClients({“com。ncmed。eos。system。feign”,“com。ncmed。eos。auth。client。feign”})@EnableDiscoveryClient// 阻止注入資料庫連線@SpringBootApplication(exclude={DataSourceAutoConfiguration。class})public class GatewayApplication { public static void main(String[] args) {// SpringApplication。run(GatewayApplication。class, args); SpringApplication application = new SpringApplication(GatewayApplication。class); // 該設定方式 application。setWebApplicationType(WebApplicationType。REACTIVE); application。run(args); }}

總結:名面上來看,我並沒有引入Spring WebMVC相關jar包,但是其他的包中引入了,因此要特別注意pom檔案中的引入,包與包之間存在許多依賴關係,需要仔細檢查其依賴。