SpringBoot配置外部Tomcat项目启动流程源码分析(上)

Posted Java小叮当

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot配置外部Tomcat项目启动流程源码分析(上)相关的知识,希望对你有一定的参考价值。

前言

SpringBoot应用默认以Jar包方式并且使用内置Servlet容器(默认Tomcat),该种方式虽然简单但是默认不支持JSP并且优化容器比较复杂。故而我们可以使用习惯的外置Tomcat方式并将项目打War包。一键获取SpringBoot学习笔记

【1】创建项目并打War包

① 同样使用Spring Initializer方式创建项目

② 打包方式选择"war"

③ 选择添加的模块

④ 创建的项目图示

有三个地方需要注意:

  • pom中打包方式已经为war;
  • 对比默认为jar的项目多了ServletInitializer类;
  • 项目结构没有src/main/webapp,且没有WEB/INF web.xml。

ServletInitializer类如下:

public class ServletInitializer extends SpringBootServletInitializer 
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) 
return application.sources(SpringbootwebprojectApplication.class);


pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.web</groupId>
<artifactId>springbootwebproject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>springbootwebproject</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--这里修改了内置Tomcat的作用域-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

⑤ 补全项目结构

第一种方式,手动创建src/main/webapp, WEB/INF以及web.xml。

第二种方式,使用idea创建,步骤如下:

1.如下图所示,点击项目结构图标

2.创建src/main/webapp

3.创建web.xml

此时项目结构图如下:

【2】使用外部配置的Tomcat启动项目

① 点击"Edit Configurations…"添加Tomcat。

② 设置Tomcat、JDK和端口

③ 部署项目

④ 启动项目

此时如果webapp 下有index.html,index.jsp,则会默认访问index.html。

如果只有index.jsp,则会访问index.jsp;如果webapp下无index.html或index.jsp,则从静态资源文件夹寻找index.html;如果静态资源文件夹下找不到index.html且项目没有对"/"进行额外拦截处理,则将会返回默认错误页面。

index.html显示如下图:

【3】SpringBoot 使用外部Tomcat启动原理

① 首先看Servlet3.0中的规范

  • javax.servlet.ServletContainerInitializer(其是一个接口) 类是通过JAR服务API查找的。对于每个应用程序,ServletContainerInitializer的一个实例是由容器在应用程序启动时创建。
  • 提供servletcontainerinitializer实现的框架必须将名为javax.servlet的文件捆绑到jar文件的META-INF/services目录中。根据JAR服务API,找到指向ServletContainerInitializer的实现类。
  • 除了ServletContainerInitializer 之外,还有一个注解–@HandlesTypes。ServletContainerInitializer 实现上的handlesTypes注解用于寻找感兴趣的类–要么是@HandlesTypes注解指定的类,要么是其子类。
  • 不管元数据完成的设置如何,都将应用handlesTypes注解。
  • ServletContainerInitializer实例的onStartup 方法将在应用程序启动时且任何servlet侦听器事件被激发之前被调用。
  • ServletContainerInitializer 的onStartup 方法调用是伴随着一组类的(Set<Class<?>> webAppInitializerClasses),这些类要么是initializer的扩展类,要么是添加了@HandlesTypes注解的类。将会依次调用webAppInitializerClasses实例的onStartup方法。

总结以下几点:

1)服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面
ServletContainerInitializer实例;

2)jar包的META-INF/services文件夹下,有一个名为
javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名;

如下图所示:

3)还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类;

4)容器启动过程中首先调用
ServletContainerInitializer 实例的onStartup方法。

ServletContainerInitializer 接口如下:

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

② 步骤分析如下

第一步,Tomcat启动

第二步,根据Servlet3.0规范,找到
ServletContainerInitializer ,进行实例化

jar包路径:

org\\springframework\\spring-web\\4.3.14.RELEASE\\
spring-web-4.3.14.RELEASE.jar!\\METAINF\\services\\
javax.servlet.ServletContainerInitializer:

Spring的web模块里面有这个文件:

org.springframework.web.SpringServletContainerInitializer

第三步,创建实例

SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set集合,为这些WebApplicationInitializer类型的类创建实例并遍历调用其onStartup方法。

SpringServletContainerInitializer 源码如下(调用其onStartup方法):

//感兴趣的类为WebApplicationInitializer及其子类
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer 
//先调用onStartup方法,会传入一系列webAppInitializerClasses
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException 
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) 
//遍历感兴趣的类
for (Class<?> waiClass : webAppInitializerClasses) 
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
//判断是不是接口,是不是抽象类,是不是该类型
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) 
try 
//实例化每个initializer并添加到initializers中
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());

catch (Throwable ex) 
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);




if (initializers.isEmpty()) 
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;

servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
//依次调用initializer的onStartup方法。
for (WebApplicationInitializer initializer : initializers) 
initializer.onStartup(servletContext);


如上所示,在
SpringServletContainerInitializer方法中又调用每一个initializer的onStartup方法。即先调用SpringServletContainerInitializer实例的onStartup方法,在onStartup()方法内部又遍历每一个WebApplicationInitializer类型的实例,调用其onStartup()方法。

WebApplicationInitializer(Web应用初始化器)是什么?

在Servlet 3.0+环境中提供的一个接口,以便编程式配置ServletContext而非传统的xml配置。该接口的实例被
SpringServletContainerInitializer自动检测(@HandlesTypes(WebApplicationInitializer.class)这种方式)。而SpringServletContainerInitializer是Servlet 3.0+容器自动引导的。通过WebApplicationInitializer,以往在xml中配置的DispatcherServlet、Filter等都可以通过代码注入。你可以不用直接实现WebApplicationInitializer,而选择继承AbstractDispatcherServletInitializer。

WebApplicationInitializer类型的类如下图:

可以看到,将会创建我们的
com.web.ServletInitializer(继承自SpringBootServletInitializer)实例,并调用onStartup方法。

第四步:我们的
SpringBootServletInitializer的实例(com.web.ServletInitializer)会被创建对象,并执行onStartup方法(com.web.ServletInitializer继承自SpringBootServletInitializer,故而会调用SpringBootServletInitializer的onStartup方法)

SpringBootServletInitializer源码如下:

@Override
public void onStartup(ServletContext servletContext) throws ServletException 
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
//创建WebApplicationContext
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext);
if (rootAppContext != null) 
//如果根容器不为null,则添加监听--注意这里的ContextLoaderListener,
//contextInitialized方法为空,因为默认application context已经被初始化
servletContext.addListener(new ContextLoaderListener(rootAppContext) 
@Override
public void contextInitialized(ServletContextEvent event) 
// no-op because the application context is already initialized

);

else 
this.logger.debug("No ContextLoaderListener registered, as "
+ "createRootApplicationContext() did not "
+ "return an application context");


可以看到做了两件事:创建RootAppContext 和为容器添加监听。

创建WebApplicationContext 源码如下:

protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) 
//创建SpringApplicationBuilder --这一步很关键
SpringApplicationBuilder builder = createSpringApplicationBuilder();
//设置应用主启动类--本文这里为com.web.ServletInitializer
builder.main(getClass());

*/从servletContext中获取servletContext.getAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)作为parent。第一次获取肯定为null
*/
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) 
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
//以将ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE重置为null
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
//注册一个新的ParentContextApplicationContextInitializer--包含parent
builder.initializers(new ParentContextApplicationContextInitializer(parent));

//注册ServletContextApplicationContextInitializer--包含servletContext
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
//设置applicationContextClass为AnnotationConfigServletWebServerApplicationContext
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
builder = configure(builder);
//添加监听器
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
//返回一个准备好的SpringApplication ,准备run-很关键
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) 
application.addPrimarySources(Collections.singleton(getClass()));

Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) 
application.addPrimarySources(
Collections.singleton(ErrorPageFilterConfiguration.class));

//启动应用
return run(application);

【4】createRootApplicationContext详细流程源码分析

① createRootApplicationContext().createSpringApplicationBuilder()

跟踪代码到:

public SpringApplicationBuilder(Class<?>... sources) 
this.application = createSpringApplication(sources);

此时的Sources为空,继续跟踪代码:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) 
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//web应用类型--Servlet
this.webApplicationType = deduceWebApplicationType();
获取ApplicationContextInitializer,也是在这里开始首次加载spring.factories文件
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
这里是第二次加载spring.factories文件
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();

ApplicationContextInitializer是spring组件spring-context组件中的一个接口,主要是spring ioc容器刷新之前的一个回调接口,用于处于自定义逻辑。

ApplicationContextInitializer(应用上下文初始化器)是什么?


ConfigurableApplicationContext-Spring IOC容器称为“已经被刷新”状态前的一个回调接口去初始化ConfigurableApplicationContext。通常用于需要对应用程序上下文进行某些编程初始化的Web应用程序中。例如,与ConfigurableApplicationContext#getEnvironment() 对比,注册property sources或激活配置文件。另外ApplicationContextInitializer(和子类)相关处理器实例被鼓励使用去检测org.springframework.core.Ordered接口是否被实现或是否存在org.springframework.core.annotation.Order注解,如果存在,则在调用之前对实例进行相应排序。

spring.factories文件中的实现类:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\\
org.springframework.boot.env.PropertiesPropertySourceLoader,\\
org.springframework.boot.env.YamlPropertySourceLoader
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\\
org.springframework.boot.context.event.EventPublishingRunListener
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\\
org.springframework.boot.diagnostics.FailureAnalyzers
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# Application Listeners
org.springframework.context.ApplicationListener=\\
org.springframework.boot.ClearCachesApplicationListener,\\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\\
org.springframework.boot.context.FileEncodingApplicationListener,\\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\\
org.springframework.boot.context.config.ConfigFileApplicationListener,\\
org.springframework.boot.context.config.DelegatingApplicationListener,\\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\\
org.springframework.boot.context.logging.LoggingApplicationListener,\\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer
# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

设置WebApplicationType

private WebApplicationType deduceWebApplicationType() 
if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
&& !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) 
return WebApplicationType.REACTIVE;

for (String className : WEB_ENVIRONMENT_CLASSES) 
if (!ClassUtils.isPresent(className, null)) 
return WebApplicationType.NONE;


return WebApplicationType.SERVLET;

这里主要是通过判断REACTIVE相关的字节码是否存在,如果不存在,则web环境即为SERVLET类型。这里设置好web环境类型,在后面会根据类型初始化对应环境。

设置Initializer–
ApplicationContextInitializer类型

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) 
//线程上下文类加载器
ClassLoader classLoader = ThreadSpringBoot配置外部Tomcat项目启动流程源码分析(下)

SpringBoot配置外部Tomcat项目启动流程源码分析(下)

SpringBoot配置外部Tomcat项目启动流程源码分析

SpringBoot中如何使用外置tomcat服务器

springboot使用JNDI指定数据源配置由外部tomcat配置

SpringBoot项目部署到外部Tomcat的相关配置