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 規範翻譯——註解和可插拔性