前面在 WebMvc 的部分咱有解析过,打war包运行,使用外部Servlet容器启动 SpringBoot 应用时,需要一个 ServletInitializer
来引导启动 SpringBoot 应用。那在使用jar包启动时,咱只是知道会走主启动类的 main 方法,但那是在开发时直接指定走主启动类的 main 方法,在jar包启动时是另一种方式。咱这最后一篇就来看看jar包启动 SpringBoot 应用的原理。
翻开打好的jar包,会发现3个文件夹:
BOOT-INF
:存放自己编写并编译好的 .class 文件和静态资源文件、配置文件等
META-INF
:有一个 MANIFEST.MF
的文件
org
:spring-boot-loader
的一些 .class 文件
其中,org.springframework.boot.loader
里开始能找到 .class 文件了。
翻看 META-INF
下面的 MANIFEST.MF
文件,发现里面的内容如下:
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.9.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class:org.springframework.boot.loader.JarLauncher
这个文件中有两个值得关注的:
在 Start-Class
中注明了 SpringBoot 的主启动类
在 Main-Class
中注明了一个类: JarLauncher
如果能靠 SpringBoot 的主启动类完成应用的启动,那为什么还要标注下面的那个 JarLauncher
呢?
1. JarLauncher是什么东西
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);
}
}
发现有个 main 方法!而且上面定义了两个常量,恰好就是在jar包中 BOOT-INF
里面的两个部分:自己的源码,和第三方jar包。
2. 测试直接启动两个带main方法的类
2.1 SpringBootApplication
错误: 找不到或无法加载主类 **com.example.demo.DemoApplication**
发现启动失败,根本就找不到这个类。
2.2 JarLauncher
能正常启动,打印 Banner 等。
3. 【拓展】主启动类无法正常引导启动的原理
用正常的指令启动时,java 指令没有指定 classpath,而当前 SpringBoot 应用依赖的jar包均放在 BOOT-INF/lib
下,这部分无法被识别。
3.1 标准jar包的启动规范
在可执行jar包中,有一个规范:被标记为Main-Class的类必须连同自己的包,直接放在jar包的最外层(没有额外的文件夹包含)。
SpringBootApplication
的位置:"BOOT-INF/classes/com.example.demo.DemoApplication.class"
JarLauncher
的位置:"org.springframework.boot.loader.JarLauncher"
所以 JarLauncher
能引导成功,而直接运行主启动类却无法成功启动。
这也解释了另外一个现象:
SpringBoot 在打jar包时,没有直接将 spring-boot-loader
包直接依赖到lib目录,而是将这个包下面的所有 .class 文件都复制到要打的jar包中。
3.2 标准jar包的内嵌jar规范
可执行jar包中还有一个规范:jar包中原则上不允许嵌套jar包。
传统的打jar包的方式是将所有依赖的jar包都复制到一个新的jar包中。这样会出现一个致命问题:
如果两个不同的jar包中有一个全限定类名相同的文件,会出现覆盖现象。
SpringBoot 使用自定义的 ClassLoader
,可以解决这个问题,具体的部分要剖析源码才能看到实现机制。
4. JarLauncher的main方法都做了什么
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
只有这一句,而且从上面的源码中可以发现,调用的空参数构造方法没有任何实际作用,也没有调父类的构造方法。
那一切的功能都在 launch
方法中。launch
方法不在 JarLauncher
里,在父类的 Launcher
内有定义:
// 这个方法是一个入口点,且应该被一个public static void main(String[] args)调用
protected void launch(String[] args) throws Exception {
//注册URL协议并清除应用缓存
JarFile.registerUrlProtocolHandler();
//设置类加载路径
ClassLoader classLoader = createClassLoader(getClassPathArchives());
//执行main方法
launch(args, getMainClass(), classLoader);
}
从文档注释可以发现,这个方法必须被 main 方法调用,这跟上面的 JarLauncher
中 main 方法直接调用一致。
4.1 registerUrlProtocolHandler
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER,
("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
// 重置任何缓存的处理程序,以防万一已经使用了jar协议。
// 我们通过尝试设置null URLStreamHandlerFactory来重置处理程序,除了清除处理程序缓存之外,它应该没有任何效果。
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
}
catch (Error ex) {
// Ignore
}
}
先设置当前系统的一个变量 **java.protocol.handler.pkgs**
,而这个变量的作用,是设置 URLStreamHandler
实现类的包路径。
之后要重置缓存,目的是清除之前启动的残留(文档注释已标明)。
4.2 createClassLoader
它要来创建 ClassLoader
,而创建之前先调了 getClassPathArchives
方法来取一些 Archive
对象。
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
4.2.1 getClassPathArchives
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
从最后看起,isNestedArchive
方法在调用时要传入 Archive.Entry
,而这个参数的来源尚不明确,先搁置一边。
往前看,有一个 this.archive
,而这个 archive
的成员属性是在这个类创建时被调用的。
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
String path = (location != null) ? location.getSchemeSpecificPart() : null;
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));
}
Archive
对象最终创建在下面的 createArchive
方法。
到 File root = new File
之前的部分,这段代码都是在找当前类的所在jar包的绝对路径
之后下面把这个文件创建出来,并以此创建一个 JarFileArchive
对象。
而这个 JarFileArchive
是 Archive
的子类,这个 Archive
就可以被 Launcher
启动。文档注释和类定义:
/**
* An archive that can be launched by the Launcher
*/
public interface Archive extends Iterable<Archive.Entry>
恰巧从 Archive
中得到一个意外收获:Archive
里的 Archive.Entry
可以被迭代!
跟前面的那个方法引用刚好能对应上了。
isNestedArchive
方法传入的参数就是 archive
对象中的那一组 Entry
对象(一个 Entry
相当于一个 "File"
)。
4.2.2 getNestedArchives
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
archive
对象要执行 getNestedArchives
时,会传入一个 EntryFilter
,以此来获取一组被嵌套的 Archive
。
而这个 EntryFilter
的工作机制就是上面的 isNestedArchive
方法,在 JarLauncher
中也有定义:
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
很明显,看看是不是 BOOT-INF/lib
开头的jar包,如果不是,看看是不是 BOOT-INF/classes
文件夹。
这部分的意义正好跟前面测试主启动类与 JarLauncher
的启动相呼应:
位于 BOOT-INF/classes
的启动类需要后续被扫描到,才能被处理。
由此可见,得到的 archives
集合就是 **BOOT-INF/classes**
与 **BOOT-INF/lib**
下面的所有文件。
4.2.3 postProcessClassPathArchives
// 在使用之前调用后处理存档条目。实现可以添加和删除Entry。
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
这个后置处理的方法是空的,且没有子类重写,说明默认就是拿 **BOOT-INF/classes**
与 **BOOT-INF/lib**
下面的文件了。
4.3 createClassLoader
// 为指定的归档文件创建一个类加载器
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
上面部分的源码很容易可以看出是将每个 Archive
的绝对路径保存到一个 List
中,之后调用下面的 createClassLoader
方法。
下面直接创建了一个 LaunchedURLClassLoader
,传入的 ClassLoader
很明显是默认的,也就是 AppClassLoader
。
4.3.1 构造方法
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
很简单,直接调用父类的构造方法(指定 ClassLoader
是双亲委托机制)
而 LaunchedURLClassLoader
的父类是: java.net.URLClassLoader
,是jdk内部的 ClassLoader
,不再深入描述。
4.4 launch
launch(args, getMainClass(), classLoader);
调用 launch
之前会先调用 getMainClass
方法获取主启动类。
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
它要从 Launcher
类的成员 archive
中获取 Manifest
文件,这个文件就是之前在 META-INF
下面的 MANIFEST.MF
文件。
之后从这个文件中取出 Start-Class
的值,这个值就是主启动类的全限定类名。
之后调用 launch
方法:
// 根据一个Archive文件和一个完全配置好的ClassLoader启动应用。
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
先设置当前线程的上下文类加载器为新的类加载器,也就是 LaunchedURLClassLoader
(默认为 AppClassLoader
)。
之后要开始创建 main 方法的运行器,并运行。
4.5 mainMethodRunner.run
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
简单的创建了 MainMethodRunner
的对象,之后上面会调用 run 方法。
MainMethodRunner
的结构:
// 用于Launcher调用main方法的辅助类。使用当前线程上下文类加载器加载包含main方法的类。
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
public void run() throws Exception {
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
}
核心是 run 方法。
先拿到当前线程的上下文类加载器,就是 LaunchedURLClassLoader
。
之后用这个 ClassLoader
加载主启动类,之后运行 main 方法。
这也解释了为什么 SpringBoot
应用在开发期间只需要写 main 方法,引导启动即可。