上次说到SpringBoot中嵌入式容器的配置和启动原理。那么SpringBoot是否可以使用外置的Servlet容器呢?答案是肯定的。接下来听我道来。
一、嵌入式容器和外置Servlet容器比较
- 优点:
- 嵌入式容器相比外置的容器来说比较简单和便捷,不需要额外的部署环境。一个jar包全部搞定。
- 缺点
- 嵌入式容器默认不支持JSP、优化定制比较复杂(通过ServerProperties、WebServerFactoryCustomizer两种方法)
二、如何配置?
-
创建maven项目时,选择打包方式为war包(这里以IDEA为例)
-
创建完成后,因为打包方式为war。所以SpringBoot在创建的时候,把pom文件中的spring-boot-starter-tomcat改成了provided
1
2
3
4
5<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency> -
这时的目录结构为
可以发现,在
in.zyzl
下除了有一个WebDemoApplication
的启动类还有一个ServletInitializer
类。 -
ServletInitializer
源码如下:1
2
3
4
5
6
7
8public class ServletInitializer extends SpringBootServletInitializer {
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//传入我们SpringBoot应用的主程序
return application.sources(WebDemoApplication.class);
}
}如果需要使用外部的servlet容器,那么这个类是必须得。类名无所谓,重要的是该类必须继承
SpringBootServletInitializer
方法,并重写configure
方法,把SpringBoot的启动类传进去。 -
因为在IDEA中,创建的war包,不会帮你自动创建webapp文件夹和WEB-INF以及web.xml,这里需要我们手动操作。操作如下:
-
右键项目名,选择
open module settings
-
点击后,打开如下窗口,按照步骤来即可创建webapp文件夹
-
接下来创建WEB-INF文件夹和web.xml。这两个文件,可以自己在webapp下面,手动创建。这里我们使用IDEA创建。按照下图步骤,即可创建。
-
-
配置好外部Servlet容器。这里使用tomcat,然后启动应用。配置步骤如下;
-
编辑Configuration
-
新加配置
-
选择你的本地tomcat
-
把SpringBoot应用部署到容器
-
部署后,启动即可。
-
三、原理
-
通过启动时候的观察,我们可以发现jar包版和war包版,启动servlet容器的顺序有些许不同。如下:
- jar包版,先启动SpringBoot应用,再选择IOC容器并创建,接下来再是初始化servlet容器。
- war包版,先启动servlet容器,再去启动SpringBoot应用,然后再创建IOC容器。
-
jar包版启动原理我们已经分析了,那么war包版的他是如何启动的呢?首先我们看看servlet3.1规范8.2.4章节。中文文档参考
http://jinnianshilongnian.iteye.com/blog/1750736
这里总结下:- 在容器启动时,会查找所有jar包下面的
ServletContainerInitializer
实例。 ServletContainerInitializer
的实现类的类名必须写在META-INF/services
目录下的javax.servlet.ServletContainerInitializer
文件中。- 除了
ServletContainerInitializer
外,还可以使用@HandlesTypes
注解实现同样的功能。
- 在容器启动时,会查找所有jar包下面的
-
既然servlet容器会在启动时扫描jar包下面的所有
ServletContainerInitializer
实例,而这个实例的全类名又是记录在META-INF/services/javax.servlet.ServletContainerInitializer
中,而spring-web.jar
中有这个文件,内容如下:1
org.springframework.web.SpringServletContainerInitializer
-
SpringServletContainerInitializer
源码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41.class}) ({WebApplicationInitializer
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}
//webAppInitializerClasses容器中的类型为WebApplicationInitializer
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList();
Iterator var4;
if(webAppInitializerClasses != null) {
var4 = webAppInitializerClasses.iterator();
//遍历所有实现了WebApplicationInitializer的类
while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
//如果当前类不是接口,也不是抽象类
if(!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
//加入到List中
initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance(new Object[0]));
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}
if(initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
//如果List不为空
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
var4 = initializers.iterator();
//遍历List,并执行每个实例的onStartup()方法
while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}
}
}
}也就是说Servlet容器启动时,会执行
ServletContainerInitializer
的onStartup
方法,SpringServletContainerInitializer
中的onStartup
方法做了如下几件事情- 遍历所有实现了WebApplicationInitializer的类,如果不是接口和抽象类,则加入到list
- 遍历list,执行里面每个实例的onStartup方法。
-
接下来我们来看看
WebApplicationInitializer
的实现类SpringBootServletInitializer
,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39public void onStartup(ServletContext servletContext) throws ServletException {
this.logger = LogFactory.getLog(this.getClass());
//创建WebApplicationContext实例 createRootApplicationContext()方法如下
WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
if(rootAppContext != null) {
servletContext.addListener(new ContextLoaderListener(rootAppContext) {
public void contextInitialized(ServletContextEvent event) {
}
});
} else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
}
}
//createRootApplicationContext方法
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
//创建SpringApplicationBuilder
SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
//初始化运行环境之类的
StandardServletEnvironment environment = new StandardServletEnvironment();
environment.initPropertySources(servletContext, (ServletConfig)null);
builder.environment(environment);
.....
//调用configure()方法。
builder = this.configure(builder);
//创建Spring应用
SpringApplication application = builder.build();
//调用run()方法,启动Spring应用
return this.run(application);
}
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder;
}
protected WebApplicationContext run(SpringApplication application) {
//SpringApplication的run方法就是我们昨天分析run方法,这里就不再说了
return (WebApplicationContext)application.run(new String[0]);
}因为
SpringBootServletInitializer
是一个抽象类。从上面的源码上看,configure肯定会被子类重写。回过头来,之前我们创建war工程的时候,SpringBoot除了创建了启动类,还帮我们创建了一个类ServletInitializer
他不就是继承了SpringBootServletInitializer
这个抽象类,并实现了configure方法么?源码如下:1
2
3
4
5
6
7
8
9
10//继承SpringBootServletInitializer类,所以在SpringBootServletInitializer的createRootApplicationContext方法中调用configure方法,实际上是调用该类的。
public class ServletInitializer extends SpringBootServletInitializer {
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//把SpringBoot应用启动类传进去
return application.sources(WebDemoApplication.class);
}
}到此,整套运行流程已经出来了。即:
-
Servlet容器启动,扫描所有的
ServletContainerInitializer
实例。 -
SpringServletContainerInitializer
实现了ServletContainerInitializer
,在onStartup方法中,遍历所有实现了WebApplicationInitializer
类的实例,并执行他的onStartup方法 -
SpringBootServletInitializer
实现了WebApplicationInitializer
,在OnStartup方法中,调用子类ServletInitializer
的configure(),初始化builder,然后创建SpringApplication,并启动它。
-