前言
在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.希望有所帮助,后续仍会更多的实战和分析,敬请期待哈. 😁😁😁.