问题现象
Dubbo
从低版本升级到2.6.5
版本后,启动失败,报错如下:
05-Mar-2019 16:02:25.204 ?? [RMI TCP Connection(2)-127.0.0.1] org.apache.catalina.core.StandardContext.listenerStart Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener
java.lang.IllegalStateException: Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml!
at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:296)
at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:107)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4727)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5189)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
解决方案
<b><font color='red'>上终极方案:使用2.6.2
以下版本或者2.7.0
以上版本的dubbo
;</font></b>
具体解决方式需要根据项目的情况解决,提供一些其他方案:
- 方案1和方案2:适合拥有
web.xml
的纯xml工程; - 方案3和方案4:适合没有
web.xml
的Spring Boot
工程;
拥有Web.xml的项目
方案1:删除自己配置的 ContextLoaderListener
删除 web.xml
中如下的配置:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
注意:如果有自定义的
Listener
继承自ContextLoaderListener
也需要删除;这么做的目的是不需要自己去配置初始化
Spring
框架,Dubbo
在2.6.3
之后可以“自动”
初始化Spring
框架;
方案2:关闭 Servlet 3.0
的可插性功能
- 将自己的
web.xml
的xsd
升级到3.0
; - 配置
metadata-complete
; - 将
Dubbo
的ContextInitializer
添加到描述文件中;
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1" metadata-complete="true">
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>org.apache.dubbo.config.spring.initializer.DubboApplicationContextInitializer</param-value>
</context-param>
</web-app>
没有Web.xml的Spring Boot工程
Spring Boot
工程没有特别好的解决方案,提供两个解决思路:
方案3:添加 web.xml
文件并按照传统配置web.xml
- 将
Spring Boot
工程改造下,创建webapp/WEB-INF
目录并创建web.xml
文件; - 按照
方案2
改造工程; - 主要关闭特性后,很多
Spring Boot
自动做的需要我们手动在web.xml
中配置;
NOTE:如果使用此方案来改造,需要注意自己的
Spring Boot
项目是否还有其他依赖Servelt 3.0
特性的地方,并手动配置到web.xml
中;
方案4:阻止dubbo
的Listener
运行
这个方案也没有绕过添加web.xml
的命运,做法如下:
- 创建
webapp/WEB-INF
目录并创建web.xml
; - 在
web.xml
中指定absolute-ordering
,仅允许Spring Web
的配置生效;
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>risk-etl</display-name>
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
原因和原理分析
观察报错日志,报错位置很明显是Spring
框架初始化时的报错,重点是:there is already a root application
。
这个错误抛出位置位于:Spring-web
包的ContextLoader
类的initWebApplicationContext
方法。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
…………
}
原因很明显,ContextLoader
被调用了至少两遍,第二遍报错导致项目初始化失败,其主要的“罪魁祸首”是dubbo
包下面的web-fragment.xml
。
Servlet 3.0的可插特性
Servlet 3.0
是随着Java EE 6
规范发布的,主要新增特性:
- 支持异步:
Servlet
可以支持异步; - 新增注解:可以使用注解来配置
Servlet
/Filter
/Listener
; - 可查特性:允许使用
web-fragment.xml
将一个web.xml
拆分到多个包中配置;
支持Servlet 3.0
规范的容器,在启动后会扫描工程的jar
包,找到符合规范的添加了相关注解的类
和web-fragment.xml
然后跟web.xml
的配置合并作为整个项目的初始化配置。
发生原因及解决原理
上述问题的发生原因很明显了:
-
dubbo
在2.6.3
版本为了实现优雅关机
(实际上并不好用)引入了web-fragment.xml
注册自己的ContextInitializer
; -
DubboApplicationContextInitializer
通过Spring
的消息广播机制
在Context
加载完成后调用addShutdownHook()
向JVM
注册一个钩子函数,以便JVM
关闭时可以释放一些资源防止内存泄露; -
dubbo
为了保证自己的ContextInitializer
被用到(利用的Spring
的机制)在自己的web-fragment.xml
顺手配置了一个listener
; - 容器启动时,如果我们自己在
web.xml
中配置了ContextLoaderListener
(或其子类),我们的Listener
一般会被优先调用,完成第一次的Spring Context
初始化; - 如果是
Spring Boot
项目,容器会先调用SpringServletContainerInitializer
类的onStartup
方法,这个方法内部会初始化Spring Context
; - 我们的或者
Spring Boot
的Listener
调用完成后会调用dubbo
的listener
,这个时候ContextLoader
类会检测到已经初始化了一个Context
从而报错,引发项目启动失败;
<web-fragment version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd">
<name>dubbo-fragment</name>
<ordering>
<before>
<others/>
</before>
</ordering>
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>org.apache.dubbo.config.spring.initializer.DubboApplicationContextInitializer</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-fragment>
Dubbo在 github 上说明了他为什么要这么做:https://github.com/apache/incubator-dubbo/pull/2126
metadata-complete
这个是Servlet 3.0
提供的一个属性,等同一个开关,设置为true
则表示web.xml
已经提供了全部的配置信息,不需要容器再去各个jar
包找配置了,换句话就是:关闭可插特性
;
absolute-ordering
这个属性是SpringServletContainerInitializer
注释里面提供的解决思路。这个属性可以理解为指定web-fragment.xml
的加载顺序,和ordering
标签的区别是,absolute-ordering
仅仅针对我们指定的web-fragment.xml
做排序。
总结
轻易升级一个基础框架不是一个好的做法,<b>升级基础框架还是应该关注下当前版本和目标升级版本,框架作者做了些什么事情,出现过什么BUG。</b>
当前的Spring Boot
的解决方案并不让人满意,毕竟Spring Boot
的无Xml的感觉还是很爽的,为了这个升级引入了web.xml
会有一点点不爽。