1. 场景前提
假设有这样一个场景:一个 Spring Boot 应用使用了 Dubbo(2.6.5) 作为 RPC 组件,现在想利用 war 包的方式部署在 tomcat 上,利用 tomcat 来启动应用
默认 Spring Boot 启动是依靠 java -jar xxx.jar
的方式来启动,这种启动方式只会带动一个 Spring Context 容器,如果是用 war 包方式来启动,那么就会涉及到 Spring 的父子容器,在与 Dubbo 进行集成时,是否会出现问题呢
2. 环境搭建
代码已经上传至 https://github.com/masteryourself/diseases ,详见
diseases-dubbo/diseases-dubbo-war
工程
正常的 Spring Boot 应用打成 war 包,只需要按照如下两步即可生成 war 包,然后使用外置 Tomcat 容器部署 war 包即可
2.1 配置文件
1. pom.xml
把之前的 pom_back.xml
改成 pom.xml
<!-- 修改打包方式为 war -->
<packaging>war</packaging>
<dependencies>
<!-- 依赖改为 provided -->
<!-- 注意:这里不再需要引入 servlet-api jar 包了,因为 tomcat-embed-core jar 包已经内置了 javax.servlet 包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
2.2 代码
1. SpringBootServletInitializer
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
// 传入 Spring Boot 应用的主程序
return builder.sources(SpringBootDubboApplication.class);
}
}
3. 异常剖析
3.1 错误日志
对于 Spring Boot 整合 Dubbo 的应用,在使用 war 包部署时会抛出如下异常
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:262)
at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:103)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4701)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5167)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:743)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:719)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:705)
at org.apache.catalina.startup.HostConfig.manageApp(HostConfig.java:1720)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:287)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:479)
at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:428)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:287)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
at com.sun.jmx.remote.security.MBeanServerAccessController.invoke(MBeanServerAccessController.java:468)
at javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1466)
at javax.management.remote.rmi.RMIConnectionImpl.access$300(RMIConnectionImpl.java:76)
at javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1307)
at java.security.AccessController.doPrivileged(Native Method)
at javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1406)
at javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:828)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:323)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$254(TCPTransport.java:683)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$2/806523735.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
3.2 异常详解
为什么会抛出上面的异常呢?这是由于 Dubbo 的 jar 包里多了一个 META-INF/web-fragment.xml
文件,这个文件的内容如下
<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>
由此可知它会给容器中添加一个监听器 ContextLoaderListener
,同时向这个监听器的 contextInitializerClasses
属性设置了 DubboApplicationContextInitializer
上下文初始化器,而在 Spring Boot 启动流程中,已经不再需要 ContextLoaderListener
监听器去初始化 Spring 环境的上下文了
3.2.1 Spring Boot 中初始化 Spring 环境
1. org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#prepareWebApplicationContext
它的触发逻辑是: AbstractApplicationContext.refresh()
-> ServletWebServerApplicationContext.onRefresh()
-> ServletWebServerApplicationContext.createWebServer()
-> onStartup()
-> ServletWebServerApplicationContext.selfInitialize()
-> ServletWebServerApplicationContext.prepareWebApplicationContext()
protected void prepareWebApplicationContext(ServletContext servletContext) {
Object rootContext = servletContext.getAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
...
servletContext.log("Initializing Spring embedded WebApplicationContext");
try {
// 设置 Spring 环境初始化标识,表明已经初始化了上下文
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);
...
}
...
}
3.2.2 ContextLoaderListener 监听器初始化 Spring 环境
1. org.springframework.web.context.ContextLoader#initWebApplicationContext
public WebApplicationContext initWebApplicationContext(ServletContext 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!");
}
...
}
3.3 解决方案
3.3.1 修改版本号
更改 Dubbo 的版本,目前仅发现 Dubbo 2.6.3~2.6.5 会有这个文件导致报错,可以考虑修改 Dubbo 的版本号
3.3.2 禁用 Servlet3.0 特性
在无法更改 Dubbo 版本的情况下,可以考虑禁用 servlet3.0 新特性,即在项目中新建 webapp/WEB-INF/web.xml
文件,添加属性 metadata-complete="true"
,表示禁用 web-fragment.xml
<?xml version="1.0" encoding="UTF-8" ?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
metadata-complete="true">
</web-app>
关于 Servlet 3.0 的可插拔特性,可以参考文章 Servlet 3.1 规范翻译——注解和可插拔性