SpringBoot jar包启动的原理
针对这个问题,之前一直不清楚,只知道jar可直接运行,war包要放到web容器中。
近期在做JPMS改造,当下在IDEA中已可以成功运行,但未找到jar部署的方式,以此为契机,正好把这个疑问解决一下。
感觉这篇文章还不错,就转了过来。因为当前站点主题对代码支持很不好,要结合源码来看。
Jar包结构
首先,先准备一个jar包,我这里准备了一个demo-0.0.1-SNAPSHOT.jar;先来看看jar包里面的目录结构:
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties
│ │ └── com
│ │ └── sf
│ │ └── demo
│ │ └── DemoApplication.class
│ └── lib
│ ├── spring-boot-2.1.3.RELEASE.jar
│ ├── spring-boot-autoconfigure-2.1.3.RELEASE.jar
│ ├── spring-boot-starter-2.1.3.RELEASE.jar
│ ├── 这里省略掉很多jar包
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.sf
│ └── demo
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── 省略class
├── archive
│ ├── Archive$Entry.class
│ ├── 省略class
├── data
│ ├── RandomAccessData.class
│ ├── 省略class
├── jar
│ ├── AsciiBytes.class
│ ├── 省略class
└── util
└── SystemPropertyUtils.class
这个文件目录分为BOOT-INF/classes、BOOT-INF/lib、META-INF、org:
BOOT-INF/classes:主要存放应用编译后的class文件
BOOT-INF/lib:主要存放应用依赖的jar包文件
META-INF:主要存放maven和MANIFEST.MF文件
org:主要存放springboot相关的class文件
当你使用命令java -jar demo-0.0.1-SNAPSHOT.jar
时,它会找到META-INF
下的MANIFEST.MF
文件,可以从文件中发现,其内容中的Main-Class
属性值为org.springframework.boot.loader.JarLauncher
,并且项目的引导类定义在Start-Class
属性中,值为com.sf.demo.DemoApplication,该属性是由springboot引导程序启动需要的,JarLauncher
就是对应的jar文件的启动器.
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.sf.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.0.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
启动类org.springframework.boot.loader.JarLauncher并非是项目中引入的类,而是spring-boot-maven-plugin插件repackage追加进去的.
探索JarLauncher的实现原理
当执行java -jar命令或执行解压后的org.springframework.boot.loader.JarLauncher
类时,JarLauncher
会将BOOT-INF/classes
下的类文件和BOOT-INF/lib
下依赖的jar加入到classpath
下,最后调用META-INF下
的MANIFEST.MF
文件的Start-Class
属性来完成应用程序的启动,也就是说它是springboot loader提供了一套标准用于执行springboot打包出来的JAR包.
JarLauncher重点类的介绍:
- java.util.jar.JarFile:JDK工具类,用于读取JAR文件的内容
- org.springframework.boot.loader.jar.JarFile:继承于JDK工具类JarFile类并扩展了一些嵌套功能
- java.util.jar.JarEntry:JDK工具类,此类用于表示JAR文件条目
- org.springframework.boot.loader.jar.JarEntry:也是继承于JDK工具类JarEntry类
- org.springframework.boot.loader.archive.Archive: spring boot loader抽象出来的统一访问资源的接口
- org.springframework.boot.loader.archive.JarFileArchive:JAR文件的实现
org.springframework.boot.loader.archive.ExplodedArchive:文件目录的实现
在项目里面添加一个依赖配置,就可以看JarLauncher的源码:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
org.springframework.boot.loader.ExecutableArchiveLauncher
public class JarLauncher extends ExecutableArchiveLauncher {}
org.springframework.boot.loader.Launcher
/**
*
* 启动程序的基类,该启动程序可以使用一个或多个支持的完全配置的类路径来启动应用程序
*
* @author Phillip Webb
* @author Dave Syer
* @since 1.0.0
*/
public abstract class Launcher {
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
/**
* 启动应用程序,此方法是子类方法{@code public static void main(String[] args)}调用的初始入口点
*
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
//①注册一个自定义URL的jar协议
JarFile.registerUrlProtocolHandler();
}
//②创建指定archive的类加载器
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
//③获取Start-Class属性对应的com.sf.demo.DemoApplication
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
//④利用反射调用Start-Class,执行main方法
launch(args, launchClass, classLoader);
}
}
①注册一个自定义URL的JAR协议 org.springframework.boot.loader.jar.JarFile#registerUrlProtocolHandler
spring boot loader
扩展了URL协议,将包名org.springframework.boot.loader
追加到java系统属性java.protocol.handler.pkgs
中,该包下存在协议对应的Handler类,即org.springframework.boot.loader.jar.Handler
其实现协议为JAR
.
/*
*注册一个'java.protocol.handler.pkgs'属性,让URLStreamHandler处理jar的URL
*/
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER,
("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
org.springframework.boot.loader.jar.JarFile#resetCachedUrlHandlers
/**
* 防止已经使用了jar协议,需要重置URLStreamHandlerFactory缓存的处理程序。
*/
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
} catch (Error ex) {
// Ignore
}
}
②创建指定archive的类加载器 org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchivesIterator
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
Archive.EntryFilter searchFilter = this::isSearchCandidate;
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
if (isPostProcessingClassPathArchives()) {
archives = applyClassPathArchivePostProcessing(archives);
}
return archives;
}
org.springframework.boot.loader.Launcher#createClassLoader(java.util.Iterator<org.springframework.boot.loader.archive.Archive>)
/**
* 创建一个指定的archives的类加载器
*/
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(50);
while (archives.hasNext()) {
Archive archive = archives.next();
urls.add(archive.getUrl());
archive.close();
}
return createClassLoader(urls.toArray(new URL[0]));
}
org.springframework.boot.loader.Launcher#createClassLoader(java.net.URL[])
/**
* 创建一个指定的自定义URL的类加载器
*/
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(isExploded(), urls, getClass().getClassLoader());
}
③获取Start-Class属性对应的com.sf.demo.DemoApplication org.springframework.boot.loader.ExecutableArchiveLauncher#getMainClass
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
//从配置文件获取Start-Class对应的com.sf.demo.DemoApplication
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
④利用反射调用Start-Class
,执行main
方法
/**
* 启动应用程序
*/
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
//将当前线程的上下文类加载器设置成LaunchedURLClassLoader
Thread.currentThread().setContextClassLoader(classLoader);
//启动应用程序
createMainMethodRunner(launchClass, args, classLoader).run();
}
/**
* 构造一个MainMethodRunner类,来启动应用程序
*/
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
org.springframework.boot.loader.MainMethodRunner
/**
* 用来调用main方法的工具类
*/
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
//利用反射启动应用程序
public void run() throws Exception {
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null, new Object[] { this.args });
}
}
我们先了解一下类加载机制:
我们知道双亲委派模型的原则,当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试加载任务。
由于demo-0.0.1-SNAPSHOT.jar中依赖的各个JDK包,并不在程序自己的classpath下,它是存放在JDK包里的BOOT-INF/lib目录下,如果我们采用双亲委派机制的话,根本获取不到我们JAR包的依赖,因此我们需要破坏双亲委派模型,使用自定义类加载机制。
在springboot2中,LaunchedURLClassLoader自定义类加载器继承URLClassLoader,重写了loadClass方法;在JDK里面,JAR的资源分隔符是!/
,但是JDK中只支持一个!/
,这无法满足spring boot loader的需求,so,springboot扩展了JarFile,从这里可以看到org.springframework.boot.loader.jar.JarFile#createJarFileFromEntry
,它支持了多个!/
,表示jar文件嵌套JAR文件、JAR文件嵌套Directory.
org.springframework.boot.loader.LaunchedURLClassLoader
public class LaunchedURLClassLoader extends URLClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
//........省略代码
try {
try {
//尝试根据类名去定义类所在的包,即java.lang.Package,确保jar文件嵌套jar包里匹配的manifest能够和package关联起来
definePackageIfNecessary(name);
} catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(name) == null) {
// This should never happen as the IllegalArgumentException indicates
// that the package has already been defined and, therefore,
// getPackage(name) should not return null.
throw new AssertionError("Package " + name + " has already been defined but it could not be found");
}
}
return super.loadClass(name, resolve);
} finally {
Handler.setUseFastConnectionExceptions(false);
}
}
}
org.springframework.boot.loader.LaunchedURLClassLoader#definePackageIfNecessary
/**
* 在进行调用findClass方法之前定义一个包,确保嵌套jar与包关联
* @param className the class name being found
*/
private void definePackageIfNecessary(String className) {
int lastDot = className.lastIndexOf('.');
if (lastDot >= 0) {
String packageName = className.substring(0, lastDot);
if (getPackage(packageName) == null) {
try {
definePackage(className, packageName);
} catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(packageName) == null) {
// This should never happen as the IllegalArgumentException
// indicates that the package has already been defined and,
// therefore, getPackage(name) should not have returned null.
throw new AssertionError(
"Package " + packageName + " has already been defined but it could not be found");
}
}
}
}
}
总结:
1、springboot
扩展了JDK
的URL
协议;
2、springboot
自定义了类加载器LaunchedURLClassLoader
;
3、Launcher
利用反射调用StartClass#main
方法(org.springframework.boot.loader.MainMethodRunner#run)
;
4、springboot1
和springboot2
主要区别是在启动应用程序时,springboot1
会启动一个线程去反射调用,springboot2
直接调用;