误区
开始学习及使用 Spring Boot
的时候,通常都编写过以下代码:
package com.example.myapplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
接着通过 Gradle
或者 Maven
构建工具打包得到文件app.jar
,然后输入以下命令启动:
java -jar app.jar
这里读者可能会误以为 JVM
是直接调用的 Application#main
函数,然后开始执行 SpringApplication.run(Application.class, args);
。
接下来就开始解释这为什么会是误解
。
分析
首先我们分析一下 java -jar
这个操作到底做了什么。
查询 java
命令手册之后,在描述区有这么一句:
By default, the first argument that is not an option of the java command is the fully qualified name of the class to be called.
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.
重点在于最后一句:启动类必须指定在 Manifest
清单中的 Main-Class
头中。
当然,直接查看 -jar
option 也能找打类似的描述:
-jar filename
Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains
a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that
serves as your application's starting point.
When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are
ignored.
至此我们已经了解到执行 java -jar app.jar
的第一步是寻找 Manifest
清单中的 Main-Class
头,找到启动类
之后才进行调用 #main
函数。
解压 app.jar
app.jar
是经过 Gradle
或者 Maven
的 spring boot plugin
插件特殊处理。解压结果如下:
app.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-BOOT-INF
+-com
| +-example
| +-myapplication
| +-Application.class
| +-Other classes...
+-lib
+-dependency1.jar
+-dependency2.jar
可以从以下链接找到
jar
文件的规格要求。
https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html
打开文件 META-INF/MANIFEST.MF
,内容如下:
Manifest-Version: 1.0
Implementation-Title: Example Application
Implementation-Version: 1.1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.myapplication.Application
Spring-Boot-Version: 2.2.1.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
该文件指定 org.springframework.boot.loader.JarLauncher
为 Main-Class
,也就意味着通过 java -jar app.jar
命令运行后首先执行的应该是 JarLauncher#main
。
JarLauncher
文件可以在以下链接找到。
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java
突破
JarLauncher
是 spring boot loader
子模块中的一个类,内容如下:
package org.springframework.boot.loader;
import org.springframework.boot.loader.archive.Archive;
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
* included inside a {@code /BOOT-INF/classes} directory.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
JarLauncher#main 中调用的 Launcher#launch(args) 实际上是调用父类 org.springframework.boot.loader.Launcher#launch(args)
方法。
org.springframework.boot.loader.Launcher
可以在以下链接中找到。
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java
Launcher
的主要的方法如下:
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
/**
* Launch the application given the archive file and a fully configured classloader.
* @param args the incoming arguments
* @param mainClass the main class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
大致的思路如下:
- 遍历 jar 包中的依赖(Nest Jar)和
classes
,并创建一个ClassLoader
; - 根据清单文件(META-INF/Manifest.MF)找到所指定的
Start-Class
:启动类; - 将刚刚创建的
ClassLoader
设置到当前线程中。 - 最后通过反射找到
启动类
的main
函数,并调用该函数。
直到最后一步执行完成,才开始真正执行 Application@main
函数,才正式进入 Spring Boot
的初始化。