1. Spring Boot 项目打成 war 包
- 网上提供的思路一般都是如下方式,然后就可以成功启动了
1.1 修改 pom.xml
1. 修改 packaging
<packaging>war</packaging>
2. 修改 dependency
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 依赖改为 provided -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- 依赖改为 provided -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
1.2 修改启动类
@SpringBootApplication
public class HelloWorldMainApplication extends SpringBootServletInitializer {
/*public static void main(String[] args) {
SpringApplication.run(HelloWorldMainApplication.class, args);
}*/
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(HelloWorldMainApplication.class);
}
}
2. 报错详解
- 但是对于 Spring Boot + Dubbo 的项目,这样配置还不行,会报错
2.1 报错信息
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:4745)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5207)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
2.2 servlet 3.0 的可插拔特性
- 此处我们需要先了解一下 servlet 3.0 的可插拔特性,可以参考文章 Servlet 3.1 规范翻译——注解和可插拔性
1. spring 的扩展特性
-
在 spring 的
spring-web-xxx.RELEASE.jar
中有一个META-INF/services/javax.servlet.ServletContainerInitializer
文件,文件内容是org.springframework.web.SpringServletContainerInitializer
,在 servlet 容器启动时候会去寻找ServletContainerInitializer
实例,并调用onStartup
方法,此方法最终会调用org.springframework.context.support.AbstractApplicationContext#refresh
方法来初始化 spring 容器和 spring-mvc 子容器 -
调用栈关系如下:
onRefresh:152, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:540, AbstractApplicationContext (org.springframework.context.support)
refresh:142, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:775, SpringApplication (org.springframework.boot)
refreshContext:397, SpringApplication (org.springframework.boot)
run:316, SpringApplication (org.springframework.boot)
run:157, SpringBootServletInitializer (org.springframework.boot.web.servlet.support)
createRootApplicationContext:137, SpringBootServletInitializer (org.springframework.boot.web.servlet.support)
onStartup:91, SpringBootServletInitializer (org.springframework.boot.web.servlet.support)
onStartup:171, SpringServletContainerInitializer (org.springframework.web)
2. dubbo 的扩展特性
-
在 dubbo 的
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>
-
可以看到此处给容器中添加了 spring 的监听器
ContextLoaderListener
,根据 servlet 的监听器原理,会在 servlet 容器启动之后调用contextInitialized
方法,此方法调用了initWebApplicationContext(event.getServletContext())
方法,从字面意思上看也知道是在初始化 spring-mvc 容器public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { // 判断 servletContext 是否有此属性,如果有表示已经初始化过 web 容器 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. 解决方法
-
既然知道是基于 servlet 新特性做出的扩展,只要禁用
web-fragment.xml
即可 -
在项目中新建
webapp/WEB-INF/web.xml
文件,添加属性metadata-complete="true"
即可
<?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>