spring-boot 可执行jar启动原理

用spring-boot很长时间了,解放了对一堆tomcat的配置,一个可执行jar包,比以前弄一堆xml方便了很多,大大简化微服务开发,也为java容器化提供了更方便的方式。

一直没好好看怎么启动的。本文一步步来看,基于spring-boot 2.1.1.RELEASE

简单示例

按照官doc新建一个简单的web应用:

<?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.example</groupId>
	<artifactId>simple-demo</artifactId>
	<version>1.0.0-SNAPSHOT.jar </version>

	<!-- Inherit defaults from Spring Boot -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.1.RELEASE</version>
	</parent>

	<!-- Add typical dependencies for a web application -->
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
	</dependencies>

	<!-- Package as an executable jar -->
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

mvn 打包:

mvn clean package

在target目录生产两个包:

# 可执行fat jar
simple-demo-1.0.0-SNAPSHOT.jar 
# 本项目的jar包
simple-demo-1.0.0-SNAPSHOT.jar.original

启动:

java -jar simple-demo-1.0.0-SNAPSHOT.jar 

就这么简单。下面解压simple-demo-1.0.0-SNAPSHOT.jar ,看它的目录结构:

jar tvf simple-demo-1.0.0-SNAPSHOT.jar 
# 解压后的3个目录
# tree -L 2
├── BOOT-INF # 项目class和依赖的jar包
│   ├── classes
│   └── lib
├── META-INF # jarMANIFEST和maven信息
│   ├── MANIFEST.MF
│   └── maven
└── org # spring-boot启动相关的class
    └── springframework

一个jar能通过java -jar 执行需要jar指定了main方法,文档见

总结一句话,需要在META-INF/MANIFEST.MF中指定Main-Class。下面我们看看解压后的文件是否有相关配置:

➜  META-INF (master) ✗ cat MANIFEST.MF
Manifest-Version: 1.0
Implementation-Title: simple-demo
Implementation-Version: 1.0-SNAPSHOT
Built-By: zhaozhou
Implementation-Vendor-Id: com.thoreauz.bootlearn
Spring-Boot-Version: 2.0.1.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.thoreauz.bootlearn.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_91
Implementation-URL: http://maven.apache.org

没错,Main-Class就是org.springframework.boot.loader.JarLauncher。所以最先启动的是它。

Archives

先撇开JarLauncher两分钟,认识下Archive,看代码:

// 可以被Launcher 启动的一个抽象
public interface Archive extends Iterable<Archive.Entry> {
    // 用于定位Archive
	URL getUrl() throws MalformedURLException;
   // 返回Archive的manifest
	Manifest getManifest() throws IOException;
	// 返回Archive所包装的Archive
	List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}

Archive,即归档,spring-boot把可以被启动器(Launcher)加载的抽象成一个Archive,目前有两个实现:

  • JarFileArchive:即一个jarFile(jar包或者jar目录)
  • ExplodedArchive:可以理解一个目录

再来看JarLauncher类,他是用来启动JarFileArchive类型的启动器,指定了从BOOT-INF/classes和BOOT-INF/lib/路径下加载。

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);
	}
	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方法中new JarLauncher()初始化JarLauncher,主要做的一件事就是把它属性archive实例化。这个archive正是之前提到的fatjar。获取方式如下:

protected final Archive createArchive() throws Exception {
    ProtectionDomain protectionDomain = this.getClass().getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URI location = codeSource == null ? null : codeSource.getLocation().toURI();
    String path = location == null ? null : location.getSchemeSpecificPart();
    // 省略了一些判断的代码
    File root = new File(path);
    return (Archive)(root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}

JarLauncher的archive就是JarFileArchive类型。

查找fat-jar中的类

上面提到JarLauncher的main方法调用了父类Launcherlaunch(String[] args)方法,方法如下:

JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = this.createClassLoader(this.getClassPathArchives());
this.launch(args, this.getMainClass(), classLoader);

它怎么判断从fat jar的哪个目录开始查找?通过上文中isNestedArchive()方法判断查找入口,查找类目录和jar,创建Archives。其实就是JarLauncher的archive(JarFileArchive)实现的getNestedArchives()方法的工作。返回的一个JarFileArchive的list,实际数据保存在JarFileArchive.JarFile,他的URL协议如下:

# NESTED_DIRECTORY类型
jar:file:/tmp/simple-demo/target/simple-demo-1.0-SNAPSHOT.jar!/BOOT-INF/classes!/
# NESTED_JAR类型
jar:file:/tmp/simple-demo/target/simple-demo-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-web-2.0.1.RELEASE.jar!/

LaunchedURLClassLoader

查找并创建的archives,最后转化为urls(List)。spring boot 通过LaunchedURLClassLoader,根据urls加载类,它继承jdk提供的URLClassLoader。URLClassLoader可以通过以下方式加载类:

  • 从文件目录加载
  • 从jar包中加载
  • 从网络加载 LaunchedURLClassLoader作了扩展,能根据JarFileArchive的urls,从fatjar中加载类。最后通过他反射调用项目的main方法:
//classLoader就是LaunchedURLClassLoader
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
    Thread.currentThread().setContextClassLoader(classLoader);
    this.createMainMethodRunner(mainClass, args, classLoader).run();
}
public void run() throws Exception {
    Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    mainMethod.invoke((Object)null, this.args);
}

main方法类来自MANIFEST.MF的Start-Class。

总结

至此,spring boot为什么能通过fat jar执行的基本原理就介绍完了,总结起来有几个关键点:

  1. maven插件按照一定规则组织jar包
  2. 把可以被spring boot加载的jar或者目录抽象为Archive。
  3. LaunchedURLClassLoader提供了从Archive加载class的能力。

当然,这是通过jar包运行的情况,如果在idea运行main方法,入口毫无疑问就是应用的main。 项目class在target/classes/目录,依赖jar包都被idea一股脑扔到classpath下了。


参考: [1]. Appendix E. The Executable Jar Format

CONTENTS