写在前面
在之前的文章SpringBoot2.x嵌入式Servlet容器的配置和启动原理中,我们详细的介绍了SpringBoot启动时是如何通过jar包依赖的方式动态的创建不同类型的web容器的相关流程。其中主要针对嵌入式Servlet容器做了源码级别的解读,并通过源代码了解到如何自定义Servlet容器的相关属性。那是不是如果我们使用SpringBoot后就只能使用jar包的方式启动服务呢?SpringBoot使用的嵌入式Servlet容器有什么缺点呢?针对这些问题,今天我们就来介绍一下外置Servlet容器的配置和启动原理。
缺陷
之前的文章中我们说过,SpringBoot之所以能诞生,就是因为Spring官方意识到随着Spring生态的逐步完善,当项目高度依赖Spring并且集成大量其他类型的中间件时,手动配置会变得非常庞大和繁杂。为了解决这个问题,SpringBoot便通过自动配置的方式自动的帮我们装配了许多组件并针对每个组件都提供了基本的功能和默认的参数设置。与此同时,引入的嵌入式Web容器也可以帮助我们省掉自己去配置Web服务器的过程,大大提高开发和运维的效率。但任何事情都不会有绝对的好与坏,嵌入式Web容器也有自己的缺点,例如:
不支持JSP
无法深度优化和定制Web容器
...
配置
方式一:Maven项目,手动配置
步骤一:
这种方式步骤会稍微多一点,但是如果你是一个老司机,一定对在没有SpringBoot之前的项目结构非常熟悉。没错,我们就是要构建出原生的war项目。像下面这样:




步骤二:
引入maven依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><scope>provided</scope></dependency>
<scope>provided</scope>
即在编译和测试过程中有效,最后生成的war包时不会加入。因为servlet-api tomcat服务器已经存在了,如果再打包会冲突。
<packaging>war</packaging>
步骤三:
编写一个SpringBootServletInitializer的子类,并调用configure方法

这样就完成啦,剩下的工作就是我们熟悉的配方了。通过Maven打成war包,安装Tomcat服务,将war包放在指定目录下,配置Tomcat相关属性,最后启动Tomcat就可以啦。如果你使用IDEA在本地启动,也只需要配置一下IDEA中的Tomcat配置指向本地的Tomcat安装路径,如下图:

方式二:Spring Initializer配置
Spring Initializer是IDEA提供的Spring脚手架,方便我们可以快速的创建Spring工程。我们也可以利用它来快速的创建项目。

next之后就进入详情配置,这里没啥要说的,记得将打包方式修改成war


此时我们还缺少webapp和对应的web.xml,你可以通过手动的方式进行创建,同时也可以通过IDEA的配置自动的帮我们生成。这里我重点说一下第二种方式。
首选选择Product structure -> Modules -> web,这里就是构建web工程文件结构的地方,红色字那里证明我们现在还没有webapp文件,选择➕,设置好路径后点击OK,IDEA就会帮我们自动创建webapp文件夹。如下图:

创建好webapp后,点击上面的➕,选择web.xml,配置好路径后点击OK。web.xml也就创建成功了。

我个人比较推荐使用IDEA帮我们生成这种方式,全程点点鼠标就可以,轻松 + 愉快。
启动原理
配置完成后有的小伙伴一定会有疑问,尤其是针对
编写一个SpringBootServletInitializer的子类,并调用configure方法
这一步骤,那为什么要创建SpringBootServletInitializer的子类,并调用configure方法呢?外置Web容器启动和之前介绍过的嵌入式Web容器启动有什么区别呢?废话不多说,我们还是直接看源码。
Servlet 3.1规范
https://download.oracle.com/otn-pub/jcp/servlet-3_1-fr-eval-spec/servlet-3_1-final.pdf
文档中有的8.2.4 Shared libraries runtimes pluggability,有这样一段话

服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例。 ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为 javax.servlet.ServletContainerInitializer 的文件,内容就是ServletContainerInitializer的实现类的全类名 如果我们想使用某些类,可以在@HandlesTypes注解中标识
那么在@HandlesTypes注解中标识的类我们要如何获取到呢,再看下文档:

SpringServletContainerInitializer

在ServletContainerInitializer内部,首选会看到@HandlersTypes注解
启动Tomcat并在ServletContainerInitializer的onStartup()方法加入断点

通过截图中的红框对比可以看出,当onStartup()方法被调用时,参数webAppInitializerClass集合中的元素正是@HandlersTypes注解中标识的WebApplicationInitializer接口的实现类。
下面我们来看看onStartup()的内部实现

这里没什么难点,主要做了两件事
创建WebApplicationInitializer实现类的实例对象
通过实例对象调用各自的onStartUp()方法
SpringBootServletInitializer


这里最主要的目的还是为了创建并启动Spring应用,需要特别注意的是,这里会回调我们自定义并继承的configure()方法,从代码中看,重写configure的目的就是将我们的启动类提交到builder中。如下图:


区别
jar包:执行SpringBoot主类的main方法,启动IOC容器,创建嵌入式的Web容器。即先有IOC,再有Web容器 war包:启动服务器,服务器启动SpringBoot应用,启动ioc容器。即先有Web容器,再有IOC容器。
遗留问题
首先我们在引入pom依赖时就自动的规避了这类问题的出现,看一下pom依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><scope>provided</scope></dependency>
这就意味着,当应用程序被打包之后,war包中引入的lib是不会有tomcat的jar存在的。又因为


所以TomcatServletWebServerFactory压根儿就不会加载。也就不会出现同时加载两个Web容器的情况。
即便我们真的不小心引入了tomcat的包导致TomcatServletWebServerFactory加载到容器中,也不会出现问题。因为在CreateWebServer()方法中,只有当this.servletContext == null时才会执行getWebServer方法。


好啦~本文的所有内容就结束啦~谢谢阅读









