SpringBoot实战分析-Tomcat方式部署

265 阅读5分钟
原文链接: wrcj12138aaa.github.io

前言

Spring Boot 初体验一文中我们学习了以 JAR 形式快速启动一个Spring Boot程序,而 Spring Boot 也支持传统的部署方式: 将项目打包成 WAR,然后由 Web 服务器进行加载启动,这次以 Tomcat 为例,我们就快速学习下如何以 WAR 方式部署一个 Spring Boot 项目,代码托管于 Github, 并做一些简单的源码分析.

正文

利用Spring Initializr 工具下载基本的 Spring Boot 工程,选择 Maven 方式构建, 版本为正式版1.5.16, 只选择一个 Web 依赖. image-20181014094106403

继承 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 就说明打包成功了

image-20181014101039797

然后就可以项目的 target 目录下看到生成的 WAR.

image-20181014101142382

部署 Tomcat

springboot-tomcat-0.0.1-SNAPSHOT.war 放在 Tomcat程序的文件夹 **webapps** 下,然后运行Tomcat, 启动成功就可以在浏览器输入 http://localhost:8080/springboot-tomcat-0.0.1-SNAPSHOT/ ,请求这个简单 Web 程序了.

image-20181014101753493

到这里, WAR 方式部署的 Spring Boot 程序就完成了. 🎉🎉🎉

源码分析

完成到这里, 不禁有个疑问: 为何继承了 SpringBootServletInitializer 类,并覆写其 configure 方法就能以 war 方式去部署了呢 ? 带着问题,我们从源码的角度上去寻找答案.

在启动类 SpringbootTomcatApplication 覆写的方法进行断点,看下 Tomcat 运行项目时这个方法调用过程.

通过 Debug 方式运行项目,当运行到这行代码时,可以看到两个重要的类 SpringBootServletInitializerSpringServletContainerInitializer .

image-20181014131101858

从图可以看到 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 类就登场了.

image-20181014133828708

SpringServletContainerInitializer 类实现 Servlet 3.0 规范的 ServletContainerInitializer接口, 也就意味着当 Servlet 容器启动时,就以调用 ServletContainerInitializer 接口的 onStartup方法通知实现了这个接口的类.

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

现在我们来看下 SpringServletContainerInitializeronStarup 方法的具体实现如下, 关键代码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 实例的创建.

image-20181014135435492

看到这里,我们总结下这几个类调用流程,梳理下 Spring Boot 程序 WAR 方式启动过程:

SpringServletContainerInitializer#onStartup

​ => SpringBootServletInitializer#onStartup

​ => SpringBootServletInitializer#createRootApplicationContext​ =>SpringbootTomcatApplication#configure`

另外,我还收获了一点就是: 当执行 SpringBootServletInitializercreateRootApplicationContext 方法最后,调用了run(application).

这也说明了当 WAR方式部署 Spring Boot 项目时, 固定生成的 Main 方法不会再被执行到,是可以去掉.

//当项目以WAR方式部署时,这个方法就是无用代码
public static void main(String[] args) {
	SpringApplication.run(SpringbootTomcatApplication.class, args);
}

结语

本文主要实战学习如何让 Spring BootWAR 方式启动,并且进行简单的源码分析,帮助我们更好地理解 Spring Boot.希望有所帮助,后续仍会更多的实战和分析,敬请期待哈. 😁😁😁.

参考