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 规范翻译——注解和可插拔性

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