Spring|Spring Boot Jar 是怎么启动的--FatJar启动流程分析
欢迎访问我的博客,同步更新: 枫山别院
首先搭建一个Spring Boot工程,非常简单,在Spring Initializr
上快速生成一个项目就可以了,使用Java语言,其他的可以根据需要自己选择。
如果你没有什么特殊需要,可以参照我下面的设置。
文章图片
image.png
点击Generate之后,下载生成的工程。
在工程目录下,执行
mvn package -Dmaven.test.skip=true
命令,打包。
在target目录下,会生成一个jar,名字是demo-0.0.1-SNAPSHOT.jar,这就是我们要分析的jar,如下图:
文章图片
image.png 【Spring|Spring Boot Jar 是怎么启动的--FatJar启动流程分析】然后我们解压这个jar,看看跟普通的jar有什么区别。
解压之后,目录是这样的,只展开了2级目录(详细的请看附录):
├── BOOT-INF
│├── classes
│└── lib
├── META-INF
│├── MANIFEST.MF
│└── maven
└── org
└── springframework
再看一个普通的jar,也就是非Spring Boot的jar是什么样子的
├── META-INF
│├── MANIFEST.MF
│└── maven
└── com
└── guofeng
大家对比一下,Spring Boot的jar只比普通的jar多出了一个BOOT-INF目录,其他的东西在结构上是一样的。也就是说,首先Spring Boot的jar肯定是兼容普通的jar,普通jar有的东西,它都有,这样,它才能用
java -jar
运行。普通的jar,依赖都是在外部的,Spring Boot的fatjar,依赖都在Jar包内,还有就是把工程代码换了下位置。我们知道在jar运行之前,会首先从MANIFEST.MF文件中,查找jar的相关信息。我们看看这个文件里的内容
Manifest-Version: 1.0
Created-By: Maven Archiver 3.4.0
Build-Jdk-Spec: 12
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Version: 2.2.0.M6
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
上面有几个参数是需要我们关注的:
- Main-Class
这个是jar的入口函数main方法的类路径,这里可以看到,这个是spring的方法,不是我们工程中写的方法。 - Start-Class
这个参数是 Spring Boot定义的,可以看到,这个类是我们工程里的了。这个是我们工程的main方法的类路径。 - Spring-Boot-Classes
这个很明显,是工程代码在jar中的路径 - Spring-Boot-Lib
这个是工程中引入的依赖jar。Spring Boot把工程依赖的所有jar都打包在了项目的jar中,这就是为什么它是个fatjar。
这个类在工程中是找不到的,因为是jar启动需要的,在打包阶段由maven打进去的,工程是不需要的。
它在包spring-boot-loader中,GAV是:
org.springframework.boot
spring-boot-loader
2.0.2.RELEASE
provided
你可以在JarLauncher中找到这个类的源代码。
我们看下它的main方法:
public static void main(String[] args) throws Exception{
new JarLauncher().launch(args);
}
非常的简单,它调用了
JarLauncher
的launch
方法,并且把参数都传递进去了。但是在JarLauncher
中并没有launch
这个方法,这个方法是JarLauncher
的祖父类Launcher
的方法,继承关系是这样的JarLauncher--->ExecutableArchiveLauncher--->Launcher
。我们看一下
launch
方法的实现:protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
这是一个
protected
方法,只能在它的子类中使用。首先看第一句代码:
JarFile.registerUrlProtocolHandler();
,它的实现是:public static void registerUrlProtocolHandler() {
//PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
//HANDLERS_PACKAGE = "org.springframework.boot.loader"
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
这几句代码非常简单,就是重新设置了一个属性值,叫做
java.protocol.handler.pkgs
。那么这个属性值是干什么用的呢,从属性名字上我们能大体猜测一下,跟protocol有关,也就是协议。什么是协议?http,https这是最常见的协议吧?对的,这个属性值是存放的一个路径,这个路径下面对应的是各种协议的处理逻辑。除了http和https,还有jar,file,ftp等等协议。这个属性值默认是
sun.net.www.protocol.jar.Handler
,这是Java的默认实现,大家可以去这个路径下看一下。Spring Boot重新将这个路径设置了一下,添加了
org.springframework.boot.loader
路径,这个路径下有Spring Boot提供的Jar协议处理逻辑,也就是覆盖了原来的Jar处理逻辑。Spring Boot的Jar跟普通的Jar是有区别的,依赖是打包在Jar中的,引导类是Spring提供的实现,但是我们最后的目的肯定是启动我们自己的工程。所以在这个地方覆盖了默认的Jar启动逻辑,按照Spring的Jar启动逻辑来走,其目的就是自定义Jar包依赖的查找逻辑,从Jar内部找依赖,最终启动用户的工程。我们再看第二句代码:
ClassLoader classLoader = createClassLoader(getClassPathArchives());
简略一看,大家都能懂的,是创建了一个ClassLoader。我们仔细研究下
getClassPathArchives
方法,它的实现是:@Override
protected List getClassPathArchives() throws Exception {
List archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
最主要就是方法的第一句代码,它是获取用户工程代码和依赖的。拆分一下,我们先看
isNestedArchive
方法,它在JarLauncher和WarLauncher中,一共两种实现,我们看下JarLauncher中的实现:@Override
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/classes/
和BOOT-INF/lib/
中的类和jar过滤了出来。然后再回到getClassPathArchives
方法中,在getNestedArchives
方法中:@Override
public List getNestedArchives(EntryFilter filter) throws IOException {
List nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
在这里,把所有符合过滤条件的文件,都放到了nestedArchives中,然后返回给调用者。
现在,在代码
ClassLoader classLoader = createClassLoader(getClassPathArchives());
中,我们就好理解了,getClassPathArchives
方法获取到了工程代码和工程的依赖jar,然后根据这些东西,创建了ClassLoader
。好的,还有最后一句代码:
launch(args, getMainClass(), classLoader);
这个毫无疑问,就是最后的启动过程了。同样的,先分析参数
getMainClass
方法:@Override
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;
}
首先代码上来是获取Manifest,这个文件对应的就是Jar包中的MANIFEST.MF文件,我们说过,这个文件中定义了Jar包的信息。然后,注意后面的代码,从文件中获取了
Start-Class
的值。我们在看这个文件的时候,Start-Class
是定义了我们工程的启动类,对吧?终于,这个地方要运行我们自己的main方法了,现在我们拿到了用户的main方法的所在类。然后看launch方法,是怎么启动的:
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
首先把我们刚刚创建的classLoader放到了当前线程中。这个classLoader中带着我们工程代码和工程依赖,没忘吧?OK。
继续继续,敲黑板,重点来了,createMainMethodRunner方法的实现如下:
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 });
}
通过我们创建的classLoader,去找
Start-Class
定义的class类文件。在这个class文件中,找main方法,然后调用这个main方法。咚咚咚咚,Jar启动流程到这里就结束了,后面就是Spring Boot应用的启动过程了。
到这里,Spring Boot Jar启动流程就分析完了,是不是非常简洁和条理清晰,很巧妙的方法,佩服这些大佬。
大家可以下载Spring Boot的源代码,然后再仔细的回味一下。
附录:
- Spring Boot Jar解压之后的文件目录树
├── BOOT-INF
│├── classes
││├── application.properties
││└── com
││└── example
││└── demo
││└── DemoApplication.class
│└── lib
│├── classmate-1.5.0.jar
│├── hibernate-validator-6.0.17.Final.jar
│├── jackson-annotations-2.9.0.jar
│├── jackson-core-2.9.9.jar
│├── jackson-databind-2.9.9.3.jar
│├── jackson-datatype-jdk8-2.9.9.jar
│├── jackson-datatype-jsr310-2.9.9.jar
│├── jackson-module-parameter-names-2.9.9.jar
│├── jakarta.annotation-api-1.3.5.jar
│├── jakarta.validation-api-2.0.1.jar
│├── jboss-logging-3.4.1.Final.jar
│├── jul-to-slf4j-1.7.28.jar
│├── log4j-api-2.12.1.jar
│├── log4j-to-slf4j-2.12.1.jar
│├── logback-classic-1.2.3.jar
│├── logback-core-1.2.3.jar
│├── slf4j-api-1.7.28.jar
│├── snakeyaml-1.25.jar
│├── spring-aop-5.2.0.RC2.jar
│├── spring-beans-5.2.0.RC2.jar
│├── spring-boot-2.2.0.M6.jar
│├── spring-boot-autoconfigure-2.2.0.M6.jar
│├── spring-boot-starter-2.2.0.M6.jar
│├── spring-boot-starter-json-2.2.0.M6.jar
│├── spring-boot-starter-logging-2.2.0.M6.jar
│├── spring-boot-starter-tomcat-2.2.0.M6.jar
│├── spring-boot-starter-validation-2.2.0.M6.jar
│├── spring-boot-starter-web-2.2.0.M6.jar
│├── spring-context-5.2.0.RC2.jar
│├── spring-core-5.2.0.RC2.jar
│├── spring-expression-5.2.0.RC2.jar
│├── spring-jcl-5.2.0.RC2.jar
│├── spring-test-5.2.0.RC2.jar
│├── spring-web-5.2.0.RC2.jar
│├── spring-webmvc-5.2.0.RC2.jar
│├── tomcat-embed-core-9.0.24.jar
│├── tomcat-embed-el-9.0.24.jar
│└── tomcat-embed-websocket-9.0.24.jar
├── META-INF
│├── MANIFEST.MF
│└── maven
│└── com.example
│└── demo
│├── pom.properties
│└── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
├── archive
│├── Archive$Entry.class
│├── Archive$EntryFilter.class
│├── Archive.class
│├── ExplodedArchive$1.class
│├── ExplodedArchive$FileEntry.class
│├── ExplodedArchive$FileEntryIterator$EntryComparator.class
│├── ExplodedArchive$FileEntryIterator.class
│├── ExplodedArchive.class
│├── JarFileArchive$EntryIterator.class
│├── JarFileArchive$JarFileEntry.class
│└── JarFileArchive.class
├── data
│├── RandomAccessData.class
│├── RandomAccessDataFile$1.class
│├── RandomAccessDataFile$DataInputStream.class
│├── RandomAccessDataFile$FileAccess.class
│└── RandomAccessDataFile.class
├── jar
│├── AsciiBytes.class
│├── Bytes.class
│├── CentralDirectoryEndRecord.class
│├── CentralDirectoryFileHeader.class
│├── CentralDirectoryParser.class
│├── CentralDirectoryVisitor.class
│├── FileHeader.class
│├── Handler.class
│├── JarEntry.class
│├── JarEntryFilter.class
│├── JarFile$1.class
│├── JarFile$2.class
│├── JarFile$JarFileType.class
│├── JarFile.class
│├── JarFileEntries$1.class
│├── JarFileEntries$EntryIterator.class
│├── JarFileEntries.class
│├── JarURLConnection$1.class
│├── JarURLConnection$2.class
│├── JarURLConnection$CloseAction.class
│├── JarURLConnection$JarEntryName.class
│├── JarURLConnection.class
│├── StringSequence.class
│└── ZipInflaterInputStream.class
└── util
└── SystemPropertyUtils.class
- 普通Jar解压之后的文件树:
├── META-INF
│├── MANIFEST.MF
│└── maven
│└── com.guofeng
│└── first-spring-boot
│├── pom.properties
│└── pom.xml
└── com
└── guofeng
└── think
└── App.class
推荐阅读
- Activiti(一)SpringBoot2集成Activiti6
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- 2018-07-09|2018-07-09 Spring 的DBCP,c3p0
- spring|spring boot项目启动websocket
- Spring|Spring Boot 整合 Activiti6.0.0
- Spring集成|Spring集成 Mina
- springboot使用redis缓存
- Spring|Spring 框架之 AOP 原理剖析已经出炉!!!预定的童鞋可以识别下发二维码去看了
- Spring|Spring Boot之ImportSelector