使用
这里以使用本地的 Tomcat 为例。
1、创建一个 SpringBoot 项目,打包方式为 war。
2、将嵌入式 Tomcat 依赖的 scope
指定为 provided
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
3、编写一个类继承 org.springframework.boot.web.support.SpringBootServletInitializer
,重写 configure
方法,例如:
package com.springboot.webdev3;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Webdev3Application.class);
}
}
4、创建 web 资源目录:
5、最终目录结构如下:
6、将其部署到本地 Tomcat 容器,启动 Tomcat,SpringBoot 项目会随之启动:
原理分析
之前我们启动打包方式为 Jar 的 SpringBoot 项目时,首先是执行 SpringBoot 入口类的 main 方法,随之启动了 IoC 容器,嵌入式 Servlet 容器也随之创建并启动了。
而我们现在是启动打包方式为 war 的Spring项目,直接启动服务器,SpringBoot 应用也随之启动了。
问题来了,为什么 SpringBoot 程序会随着外部 Servlet 容器启动而启动?
Servlet 3.0后有一个新规范:
- 服务器启动(Web 应用启动)时会当前 Web 应用中(包含所有依赖 jar 中)寻找目录
WEB-INF/services
下名为javax.servlet.ServletContainerInitializer
的文件。 - 在
javax.servlet.ServletContainerInitializer
文件中可指定全类名,对应类为javax.servlet.ServletContainerInitializer
的实现类,这些实现类会随服务器的启动而创建实例并会执行类中的onStartup
方法。 - 还可以通过
@HandlesTypes
注解加载我们需要的类,通过被标注类的构造方法注入。
在 SpringBoot 的 Web 场景启动器依赖中就有一个 javax.servlet.ServletContainerInitializer
文件:
# spring-web-4.3.22.RELEASE.jar!/META-INF/services/javax.servlet.ServletContainerInitializer
org.springframework.web.SpringServletContainerInitializer
查看该类:
// org.springframework.web.SpringServletContainerInitializer
package org.springframework.web;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// <1>
initializers.add((WebApplicationInitializer) waiClass.newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
AnnotationAwareOrderComparator.sort(initializers);
servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);
for (WebApplicationInitializer initializer : initializers) {
// <2>
initializer.onStartup(servletContext);
}
}
}
看到源码就很清晰了,应用在启动时会创建该类实例执行它的 onStartup
方法,而在该类上通过标注 @HandlesTypes(WebApplicationInitializer.class)
将当前程序中的所有 WebApplicationInitializer
的实现类的字节码对象通过构造方法注入,而 SpringBootServletInitializer
类就是 WebApplicationInitializer
的一个实现类,所以我们自己编写的 ServletInitializer
的字节码对象将会被注入,并且在 <1>
处创建实例,在 <2>
处执行了我们自己编写的 ServletInitializer
类对象的 onStartup
方法:
// org.springframework.boot.web.support.SpringBootServletInitializer#onStartup
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext);
if (rootAppContext != null) {
servletContext.addListener(new ContextLoaderListener(rootAppContext) {
@Override
public void contextInitialized(ServletContextEvent event) {
}
});
}
else {
this.logger.debug("No ContextLoaderListener registered, as "
+ "createRootApplicationContext() did not "
+ "return an application context");
}
}
接着执行 createRootApplicationContext(servletContext)
方法:
// org.springframework.boot.web.support.SpringBootServletInitializer#createRootApplicationContext
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
SpringApplicationBuilder builder = createSpringApplicationBuilder();
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
// <3>
builder = configure(builder);
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
// <4>
SpringApplication application = builder.build();
if (application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.getSources().add(getClass());
}
Assert.state(!application.getSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
if (this.registerErrorPageFilter) {
application.getSources().add(ErrorPageFilterConfiguration.class);
}
// <5>
return run(application);
}
接着在 <3>
处又执行了 configure
方法,而这个 configure
方法正是我们自己编写的继承 SpringBootServletInitializer
类重写的 configure
方法:
// com.springboot.webdev3.ServletInitializer#configure
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Webdev3Application.class);
}
在该方法中通过 application.sources(Webdev3Application.class)
传入了当前 SpringBoot 项目的入口类,返回一个 Spring 程序构建器。回到上一步,在 <3>
处拿到该构建器,在 <4> 处创建了 Spring 程序实例,最后在 <5>
处执行了 Spring 程序实例的 run
方法,即 SpringBoot 程序随服务器的启动而启动了。
评论区