001.Spring Boot 整合 Dubbo 使用 war 包部署報錯

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章