- spring boot是如何通过Java -jar 启动的
- java -jar 做了什么
- Jar包的打包插件及核心方法
- Jar包目录结构
- meta-INF内容
- Archive的概念
- JarLauncher
- URLStreamHandler
- spring boot的jar应用启动流程总结
- 在IDE/开放目录启动Spring boot应用
- Spring Boot是如何启动spring容器源码
- 创建Spring Application
- 启动
- Spring Boot启动内嵌servlet容器
得益于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
java -jar 做了什么org.springframework.boot spring-boot-maven-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被执行了,这两个之间有什么联系?
- 因为Java没有提供任何标准的方式来加载嵌套jar文件(即,它们本身包含在jar中的jar文件)。
org.springframework.boot spring-boot-maven-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 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的目录结构,都包含哪些目录和文件,解压jar包可以看到如下结构:
spring-boot-learn-0.0.1-SNAPSHOT
├── meta-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── 应用程序类
│ └── lib
│ └── 第三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── springboot启动程序
meta-INF内容
在上述目录结构中,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的概念:
- archive即归档文件,这个概念在Linux下比较常见
- 通常就是一个tar/zip格式的压缩包
- jar是zip格式
Spring Boot抽象了Archive的概念,一个Archive可以是jar(Jar FileArchive),也可以是一个文件目录(ExplodeArchive),可以抽象为统一访问资源的逻辑是:关于Spring Boot中Archive的源码如下:
public interface Archive extends Iterable{ // 获取该归档的url URL getUrl() throws MalformedURLException; // 获取jar!/meta-INF/MANIFEST.MF或[ArchiveDir]/meta-INF/MANIFEST.MF Manifest getManifest() throws IOException; // 获取jar!/BOOT-INF/lib 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()。这样完整的链条就可以连接起来了。
在IDE/开放目录启动Spring boot应用在上面只提到在一个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);
}
创建Spring Application
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();
}
总结:
- 获取启动类:根据启动类加载ioc容器
- 获取web应用类型
- spring.factories读取了对外扩展的ApplicationContextInitializer,ApplicationListener主要为了:对外扩展,对内解耦(比如全局配置文件、热部署文件)
- 根据main推算出所在的类
在创建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;
}
总结:
- 初始化SpringApplication 从spring.factories 读取 listener ApplicationContextInitializer 。
2.运行run方法
3.读取 环境变量 配置信息… - 创建springApplication上下文:ServletWebServerApplicationContext
- 预初始化上下文 : 读取启动类
6.调用refresh 加载ioc容器
加载所有的自动配置类
创建servlet容器
ps.在这个过程中springboot会调用很多监听器对外进行扩展
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();
}
}
}
2、进入onRefresh方法
@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();
}
4、这里我们以Tomcat容器为例
@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容器启动流程图



