得益于Spring Boot的封装,我们可以通过 java -jar 一行命令就可以启动一个web项目。再也不用搭建tomcat等相关容器。那么你是否关心过和探究过Spring Boot是如何封装达到这一效果的呢?本片文章带大家聊一聊,一探究竟。
我们都知道spring boot项目,当我们打包的时候会在pom文件中引入如下插件才能使用java -jar命令启动,这个插件帮我们生成了把我们所有依赖的jar包和我们的启动类打成jar包并指定Start-Class:启动类路径,难道指定这两个东西就能启动了吗?其实就算这样我们还是不能直接启动,因为Java没有提供任何标准的方式来加载嵌套的jar文件。在MANIFEAST文件中还执行了一个非常重要的配置就是Main-Class: org.springframework.boot.loader.JarLauncher(Spring Boot自定义出来的类加载器)来加载我们jar文件中的jar
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
先要弄清楚java -jar命令做了什么,在Oracle官网找到了这句描述。
If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.
//使用-jar参数时,后面的参数是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);该jar文件中包含的是class和资源文件;在manifest文件中有Main-Class的定义;Main-Class的源码中指定了整个应用的启动类;(in its source code)
小结一下:
java -jar 会找到jar中的manifest,再找到真正的启动类
这有个疑惑:在MANIFEST.MF文件中有这么一行内容
Start-Class: com.example.SpringbootMybatisApplication
前面Java官方文档中,只提到过main-class,并没有提到Start-class;
Start-Class的值 com.example.SpringbootMybatisApplication
,正是我们Java代码中的唯一类,所在的地址和类名称。
所以问题就来了::理论上执行java -jar 命令时Main-Class: org.springframework.boot.loader.JarLauncher
类会被执行,但是实际上Start-Class: com.example.SpringbootMybatisApplication
被执行了,这两个之间有什么联系?
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
执行maven clean package之后,会生成两个文件
spring-boot-maven-plugin项目存在于spring-boot-tools目录中。spring-boot-maven-plugin默认有5个:goals、repackage、run、start、build-info。在打包的时候默认使用的是repackage。
spring-boot-maven-plugin的repackage能够将mvn package生成的软件包,再次打包可执行的软件包,并将mvn package生成的软件包重命名为*.original。
spring-boot-maven-plugin在代码层调用了RepackageMojo的execute方法,而在该方法中用调用了repackage方法。repackage方法代码及操作解析如下:
private void repackage() throws MojoExecutionException {
// maven生成的jar,最终的命名将加上.original后缀
Artifact source = getSourceArtifact();
// 最终为可执行jar,即fat jar
File target = getTargetFile();
// 获取重新打包器,将maven生成的jar重新打包成可执行jar
Repackager repackager = getRepackager(source.getFile());
// 查找并过滤项目运行时依赖的jar
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
getFilters(getAdditionalFilters()));
// 将artifacts转换成libraries
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
getLog());
try {
// 获得Spring Boot启动脚本
LaunchScript launchScript = getLaunchScript();
// 执行重新打包,生成fat jar
repackager.repackage(target, libraries, launchScript);
}catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
// 将maven生成的jar更新成.original文件
updateArtifact(source, target, repackager.getBackupFile());
}
执行了以上命令之后,便生成了打包结果对应的两个文件。下面针对文件内容和结构进行一探究竟。
首先来看看jar的目录结构,都包含哪些目录和文件,解压jar包可以看到如下结构:
spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── 应用程序类
│ └── lib
│ └── 第三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── springboot启动程序
在上述目录结构中,META-INF记录了相关jar包的基础信息,包括入口程序等。
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springboot-mybatis
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.SpringbootMybatisApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.12.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
可以看到有Main-Class是org.springframework.boot.loader.JarLauncher ,这个是jar启动的Main函数。
还有一个Start-Class是com.example.SpringbootMybatisApplication,这个是我们应用自己的Main函数。
再继续了解底层和原理之前,我们先来了解一下Archive的概念:
public interface Archive extends Iterable<Archive.Entry> {
// 获取该归档的url
URL getUrl() throws MalformedURLException;
// 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
Manifest getManifest() throws IOException;
// 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}
SpringBoot定义了一个接口用于描述资源,也就是org.springframework.boot.loader.archive.Archive。该接口有两个实现,分别是org.springframework.boot.loader.archive.ExplodedArchive 和 org.springframework.boot.loader.archive.JarFileArchive。前者用于在文件夹目录下寻找资源,后者用于在jar包下寻找资源。而在Spring Boot打包的fatjar中,则是使用后者。
JarFile:对于jar包的封装,每个JarFileArchive都会对应一个Jar File。Jar File被构造的时候会解析内部结构,去获取jar包的各个文件夹或者文件,这些文件或文件夹被封装到Entry中,也存储在Jar File Archive中。如果Entry是个jar,会解析成Jar File Archive。
比如一个Jar FileArchive对应的URL为:
jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/
它对应的Jar File为:
/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar
这个JarFile有很多Entry,比如:
META-INF/
META-INF/MANIFEST.MF
spring/
spring/study/
....
spring/study/executablejar/ExecutableJarApplication.class
lib/spring-boot-starter-1.3.5.RELEASE.jar
lib/spring-boot-1.3.5.RELEASE.jar
...
JarFileArchive内部的一些依赖jar对应的URL(SpringBoot使用org.springframework,loader.jar.Handler处理器来处理这些URL);
jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/
jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class
我们看到如果有jar包中包含jar,或者jar包中包含jar包里面的class文件,会使用!/分隔开,这种方式只有org.springframework.boot.loader.jar.Handler能处理,它是Spring Boot内部扩展出来的一种URL协议。
在spring Boot项目依赖中加入如下依赖就可以找到JarLuancher类
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
从MANIFEST.MF可以看到Main函数是JarLauncher,下面来分析它的工作流程。JarLauncher类的继承结构是:
class JarLauncher extends ExecutableArchiveLauncher
class ExecutableArchiveLauncher extends Launcher
Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.
按照定义,JarLauncher可以加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class,其实JarLauncher实现很简单:
public class JarLauncher extends ExecutableArchiveLauncher {
public JarLauncher() {}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
其主入口新建了JarLuancher并调用父类Luncher中的Luanch方法启动程序,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive。
JarLuancher继承于org.springframework.boot.loader.ExecutableArchiveLauncher。该类的无参构造方法主要是构建了当前main方法所在的Fat Jar的Jar FileArchive对象。下面来看下launch方法,该主方法主要是做两件事情:
(1)以FatJar为file作为入参,构造JarFileArchive对象。获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader。
(2)以第一步构建的ClassLoader加载MANIFEST.MF文件中Start-Class指向的业务类,并且执行静态方法main。进而启动整个程序。
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
// 找到自己所在的jar,并创建Archive
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
public abstract class Launcher {
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
String path = (location == null ? null : location.getSchemeSpecificPart());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
}
}
在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用。
至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。
java中描述资源常使用URL。而URL有一个方法用于打开链接java.net.URL#openConnection()。由于URL用于表达各种各样的资源,打开资源的具体动作由java.net.URLStreamHandler这个类的子类来完成。根据不同的协议,会有不同的handler实现。而JDK内置了相当多的handler实现用于应对不同的协议。比如jar、file、http等等。URL内部有一个静态HashTable属性,用于保存已经被发现的协议和handler实例的映射。
获得URLStreamHandler有三种方法:
(1)实现URLStreamHandlerFactory接口,通过方法URL.setURLStreamHandlerFactory设置。该属性是一个静态属性,且只能被设置一次。
(2)直接提供URLStreamHandler的子类,作为URL的构造方法的入参之一。但是在JVM中有固定的规范要求:
子类的类名必须是Handler,同时最后一级的包名必须是协议的名称。比如自定义了Http的协议实现,则类名必然为xx.http.Handler;
JVM启动的时候,需要设置java.protocol.handler.pkgs系统属性,如果有多个实现类,那么中间用|隔开。因为JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler,使用Class.forName方法尝试初始化类,如果初始化成功,则会使用该类的实现作为协议实现。
为了实现这个目标,SpringBoot首先从支持jar in jar中内容读取做了定制,也就是支持多个!/分隔符的url路径。SpringBoot定制了以下两个方面:
(1)实现了一个java.net.URLStreamHandler的子类org.springframework.boot.loader.jar.Handler。该Handler支持识别多个!/分隔符,并且正确的打开URLConnection。打开的Connection是SpringBoot定制的org.springframework.boot.loader.jar.JarURLConnection实现。
(2)实现了一个java.net.JarURLConnection的子类org.springframework.boot.loader.jar.JarURLConnection。该链接支持多个!/分隔符,并且自己实现了在这种情况下获取InputStream的方法。而为了能够在org.springframework.boot.loader.jar.JarURLConnection正确获取输入流,SpringBoot自定义了一套读取ZipFile的工具类和方法。这部分和ZIP压缩算法规范紧密相连,就不拓展了。
总结SpringBoot应用启动流程
public URL findResource(String name)
public InputStream getResourceAsStream(String name)
SpringBoot构造LaunchedURLClassLoader时,传递了一个URL[]数组。数组里是lib目录下面的jar的URL。
对于一个URL,JDK或者ClassLoader如何知道怎么读取到里面的内容的?流程如下:
LaunchedURLClassLoader.loadClass
URL.getContent()
URL.openConnection()
Handler.openConnection(URL)
最终调用的是JarURLConnection的getInputStream()函数。
//org.springframework.boot.loader.jar.JarURLConnection
@Override
public InputStream getInputStream() throws IOException {
connect();
if (this.jarEntryName.isEmpty()) {
throw new IOException("no entry name specified");
}
return this.jarEntryData.getInputStream();
}
从一个URL,到最终读取到URL里的内容,整个过程是比较复杂的,总结下:
Resource getResource(final String name, boolean check) {
final URL url;
try {
url = new URL(base, ParseUtil.encodePath(name, false));
} catch (MalformedURLException e) {
throw new IllegalArgumentException("name");
}
final URLConnection uc;
try {
if (check) {
URLClassPath.check(url);
}
uc = url.openConnection();
InputStream in = uc.getInputStream();
if (uc instanceof JarURLConnection) {
/* Need to remember the jar file so it can be closed
* in a hurry.
*/
JarURLConnection juc = (JarURLConnection)uc;
jarfile = JarLoader.checkJar(juc.getJarFile());
}
} catch (Exception e) {
return null;
}
return new Resource() {
public String getName() { return name; }
public URL getURL() { return url; }
public URL getCodeSourceURL() { return base; }
public InputStream getInputStream() throws IOException {
return uc.getInputStream();
}
public int getContentLength() throws IOException {
return uc.getContentLength();
}
};
}
JarURLConnection juc = (JarURLConnection)uc;
从代码里可以看到,实际上是调用了url.openConnection()。这样完整的链条就可以连接起来了。
在上面只提到在一个fat jar里启动SpringBoot应用的过程,那么IDE里Spring boot是如何启动的呢?
在IDE里,直接运行的Main函数是应用的Main函数:
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
其实在IDE里启动SpringBoot应用是最简单的一种情况,因为依赖的Jar都让IDE放到classpath里了,所以Spring boot直接启动就完事了。
还有一种情况是在一个开放目录下启动SpringBoot启动。所谓的开放目录就是把fat jar解压,然后直接启动应用。
这时,Spring boot会判断当前是否在一个目录里,如果是的,则构造一个ExplodedArchive(前面在jar里时是JarFileArchive),后面的启动流程类似fat jar的。
总结
JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动。
SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载。
SpringBoot通过扩展URLClassLoader–LauncherURLClassLoader,实现了jar in jar中class文件的加载。
WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动。
1、调用SpringApplication.run方法启动spring boot应用
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); //调用run方法
}
}
2、 使用自定义SpringApplication进行启动
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
//将启动类放入primarySources,因为我们的启动类是一个配置类,配置了ComponentScan,在spring中启动也是传入一个配置类
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//根据class Path下的类,推算当前web应用类型(webFlux,servlet)
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//就是去spring.factroies中去获取所有key:org.springframework.context.ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//就是去spring.factories 中去获取所有key: org.springframework.context.ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 根据main方法推算出mainApplicationClass
this.mainApplicationClass = deduceMainApplicationClass();
}
总结:
在创建Spring Application之后调用run方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
run方法:
public ConfigurableApplicationContext run(String... args) {
//用来记录当前Spring Boot启动耗时
StopWatch stopWatch = new StopWatch();
//记录启动开始时间
stopWatch.start();
//它是任何spring上下文接口,所以可以接受任何ApplicationContext实现
ConfigurableApplicationContext context = null;
// 开启了Headless模式:
configureHeadlessProperty();
// 去spring.factroies中读取了SpringApplicationRunListener 的组件, 就是用来发布事件或者运行监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 发布1.ApplicationStartingEvent事件,在运行开始时发送
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 根据命令行参数 实例化一个ApplicationArguments
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 预初始化环境: 读取环境变量,读取配置文件信息(基于监听器)
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 忽略beaninfo的bean
configureIgnoreBeanInfo(environment);
// 打印Banner 横幅
Banner printedBanner = printBanner(environment);
// 根据webApplicationType创建Spring上下文
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
//预初始化spring上下文
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 加载spring ioc 容器 **相当重要 由于是使用AnnotationConfigServletWebServerApplicationContext 启动的spring 容器所以springboot对它做了扩展:
// 加载自动配置类:invokeBeanFactoryPostProcessors , 创建servlet容器onRefresh
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
总结:
1、在refreshContext()方法中调用refresh方法下的onRefresh方法
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();//启动servlet容器
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var10) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
}
this.destroyBeans();
this.cancelRefresh(var10);
throw var10;
} finally {
this.resetCommonCaches();
contextRefresh.end();
}
}
}
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer(); //创建servlet容器
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
3、进入创建servlet容器方法
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
this.webServer = factory.getWebServer(getSelfInitializer()); //创建对应的servlet容器
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat); //启动Tomcat
}
5、进入getTomcatWebServer()——>启动Tomcat方法
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();//进入
}
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
// Start the server to trigger initialization listeners
this.tomcat.start();//启动Tomcat方法
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startDaemonAwaitThread();//将Tomcatawait
}
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
private void startDaemonAwaitThread() {
Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
@Override
public void run() {
TomcatWebServer.this.tomcat.getServer().await(); //将Tomcatawait
}
};
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false);
awaitThread.start();
}
最后附上一张内置与外置servlet容器启动流程图