前言
在Spring Boot 初体验一文中我们学习了以 JAR 形式快速启动一个Spring Boot
程序,而 Spring Boot
也支持传统的部署方式: 将项目打包成 WAR
,然后由 Web
服务器进行加载启动,这次以 Tomcat
为例,我们就快速学习下如何以 WAR
方式部署一个 Spring Boot
项目,代码托管于 Github, 并做一些简单的源码分析.
正文
利用Spring Initializr 工具下载基本的Spring Boot
工程,选择 Maven
方式构建, 版本为正式版1.5.16, 只选择一个 Web
依赖.
继承 SpringBootServletInitializer
加载
打开下载的工程后,对启动类 SpringbootTomcatApplication
进行修改, 继承 SpringBootServletInitializer
这个抽象类,并且重写父类方法 SpringApplicationBuilder configure(SpringApplicationBuilder builder)
.
@SpringBootApplication
public class SpringbootTomcatApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringbootTomcatApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootTomcatApplication.class, args);
}
}
SpringBootServletInitializer
类将在 Servlet
容器启动程序时允许我们对程序自定义配置,而这里我们将需要让 Servlet
容器启动程序时加载这个类.
修改打包方式为 WAR
接下来在pom.xml
文件中,修改打包方式为 WAR
,让 Maven
构建时以 WAR
方式生成.
<groupId>com.one</groupId>
<artifactId>springboot-tomcat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
另外要注意的是:为了确保嵌入式 servlet
容器不会影响部署war文件的servlet容器,此处为 Tomcat
。我们还需要将嵌入式 servlet
容器的依赖项标记为 provided
。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
实现 Rest 请求处理
为了验证WAR
部署是否成功,我们实现一个最基础的处理 Web
请求的功能,在启动类添加一些 Spring MVC
的代码
@SpringBootApplication
@RestController
public class SpringbootTomcatApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringbootTomcatApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootTomcatApplication.class, args);
}
@RequestMapping(value = "/")
public String hello() {
return "hello tomcat";
}
}
项目打包
现在就可以打包Spring Boot
程序成 WAR
, 然后让 Tomcat
服务器加载了,在当前项目路径下使用构建命令
mvn clean package
出现 BUILD SUCCESS
就说明打包成功了
然后就可以项目的 target
目录下看到生成的 WAR
.
部署 Tomcat
将 springboot-tomcat-0.0.1-SNAPSHOT.war
放在 Tomcat程序的文件夹 **webapps**
下,然后运行Tomcat
, 启动成功就可以在浏览器输入 http://localhost:8080/springboot-tomcat-0.0.1-SNAPSHOT/ ,请求这个简单 Web
程序了.
到这里, WAR
方式部署的 Spring Boot
程序就完成了. 🎉🎉🎉
源码分析
完成到这里, 不禁有个疑问: 为何继承了 SpringBootServletInitializer
类,并覆写其 configure 方法就能以 war 方式去部署了呢 ? 带着问题,我们从源码的角度上去寻找答案.
在启动类 SpringbootTomcatApplication 覆写的方法进行断点,看下 Tomcat 运行项目时这个方法调用过程.
通过 Debug 方式运行项目,当运行到这行代码时,可以看到两个重要的类 SpringBootServletInitializer
和 SpringServletContainerInitializer
.
从图可以看到 configure 方法调用是在父类的 createRootApplicationContext
,具体代码如下,非关键部分已省略,重要的已注释出来.
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
SpringApplicationBuilder builder = createSpringApplicationBuilder(); // 新建用于构建SpringApplication 实例的 builder
builder.main(getClass());
// ....
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
builder = configure(builder); // 调用子类方法,配置当前 builder
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
SpringApplication application = builder.build(); // 构建 SpringApplication 实例
if (application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.getSources().add(getClass());
}
//...
return run(application); // 运行 SpringApplication 实例
}
SpringApplicationBuilder
实例, 应该是遵循建造者设计模式,来完成SpringApplication
的构建组装.
而 createRootApplicationContext
方法的调用还是在这个类内完成的,这个就比较熟悉, 因为传统的 Spring Web
项目启动也会创建一个 WebApplicationContext
实例.
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case a ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext); // 创建一个 WebApplicationContext 实例.
// ...
}
问题又来了,这里的 onStartup
方法又是如何执行到的呢? SpringServletContainerInitializer
类就登场了.
SpringServletContainerInitializer
类实现 Servlet 3.0
规范的 ServletContainerInitializer
接口, 也就意味着当 Servlet
容器启动时,就以调用 ServletContainerInitializer
接口的 onStartup
方法通知实现了这个接口的类.
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
现在我们来看下 SpringServletContainerInitializer
的 onStarup
方法的具体实现如下, 关键代码23~24行里 initializers
是一个 LinkedList
集合,有着所有实现 WebApplicationInitializer
接口的实例,这里进行循环遍历将调用各自的 onStartup
方法传递
ServletContext
实例,以此来完成 Web
服务器的启动通知.
@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 {
// 提取webAppInitializerClasses集合中 实现 WebApplicationInitializer 接口的实例
initializers.add((WebApplicationInitializer) waiClass.newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
// ...
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext); // 调用所有实现 WebApplicationInitializer 实例的onStartup 方法
}
}
追踪执行到SpringServletContainerInitializer
类的22行, 我们可以看到集合里就包含了我们的启动类,因此最后调用了其父类的 onStartup
方法完成了 WebApplicationContext
实例的创建.
看到这里,我们总结下这几个类调用流程,梳理下 Spring Boot
程序 WAR
方式启动过程:
SpringServletContainerInitializer#onStartup
=> SpringBootServletInitializer#onStartup
=> SpringBootServletInitializer#createRootApplicationContext
=>
SpringbootTomcatApplication#configure`
另外,我还收获了一点就是: 当执行 SpringBootServletInitializer
的 createRootApplicationContext
方法最后,调用了run(application)
.
这也说明了当 WAR
方式部署 Spring Boot
项目时, 固定生成的 Main
方法不会再被执行到,是可以去掉.
//当项目以WAR方式部署时,这个方法就是无用代码
public static void main(String[] args) {
SpringApplication.run(SpringbootTomcatApplication.class, args);
}
结语
本文主要实战学习如何让 Spring Boot
以 WAR
方式启动,并且进行简单的源码分析,帮助我们更好地理解 Spring Boot
.希望有所帮助,后续仍会更多的实战和分析,敬请期待哈. 😁😁😁.