探究一下在调用我们写的main方法之前,SpringBoot框架为我们做了哪些事情。
Spring Boot Jar文件探究
初始化一个Spring 应用,添加如下依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fxipp.spring</groupId>
<artifactId>first-app-by-gui</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>first-app-by-gui</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
执行mvn package
命令打包,查看jar包的目录结构
.
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties
│ │ └── com
│ │ └── fxipp
│ │ └── spring
│ │ └── FirstAppByGuiApplication.class
│ └── lib
│ ├── classmate-1.4.0.jar
│ ├── hibernate-validator-6.0.17.Final.jar
│ ├── jackson-annotations-2.9.0.jar
│ ├── jackson-core-2.9.9.jar
│ ......
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.fxipp.spring
│ └── first-app-by-gui
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── ......
├── archive
│ ├── Archive$Entry.class
│ ├── Archive$EntryFilter.class
│ ├── Archive.class
│ ├── ......
├── data
│ ├── RandomAccessData.class
│ ├── RandomAccessDataFile$1.class
│ ├──......
├── jar
│ ├── AsciiBytes.class
│ ├── Bytes.class
│ ├── ......
└── util
└── SystemPropertyUtils.class
18 directories, 91 files
文件结构比较复杂,解释一下
BOOT-INF/classes
: 存放应用编译后的class文件;BOOT-INF/lib
:class path目录, 存放应用依赖的jar包;META-INF
: 存放应用的元信息,如MANIFEST.MF
文件;org
:存放Spring Boot自身的class文件;
Jar文件的执行器: Spring Boot Loader
我们先从MANIFEST.MF
文件查看
Manifest-Version: 1.0
Implementation-Title: first-app-by-gui
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.fxipp.spring.FirstAppByGuiApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.6.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
里面记录了应用的元信息,Spring的版本,应用的版本,Maven的版本,Main-Class等信息。不难发现,MainClass指向的是org.springframework.boot.loader.JarLauncher
(以下简称JarLauncher
),而不是我们自己编写的com.fxipp.spring.FirstAppByGuiApplication
。
JarLauncher
从名字看出是一个jar的执行器,他的class文件位于org.springframework.boot.loader
目录下,可见它是Spring自身的class文件。
JarLauncher的GAV org.springframework.boot:spring-boot-loader:2.1.6.RELEASE
通常情况下,他会在spring-boot-starter-parent
引入到应用中,既然main-class指向到是JarLauncher
,那我们也可以直接执行java org.springframework.boot.loader.JarLauncher
,也可以启动Spring项目的。
java org.springframework.boot.loader.JarLauncher
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.6.RELEASE)
2019-06-19 20:30:52.202 INFO 3094 --- [ main] c.fxipp.spring.FirstAppByGuiApplication : Starting FirstAppByGuiApplication on fangxideMacBook-Pro.local with PID 3094 (/Users/fangxi/Java/workspace/default/spring-boot/first-app-by-gui/target/temp/BOOT-INF/classes started by fangxi in /Users/fangxi/Java/workspace/default/spring-boot/first-app-by-gui/target/temp)
既然可以执行,那就说明了,JarLauncher
这个类才是Spring项目真正的入口。如果我们执行自己写的com.fxipp.spring.FirstAppByGuiApplication
会怎么样?
➜ classes java com.fxipp.spring.FirstAppByGuiApplication
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
at com.fxipp.spring.FirstAppByGuiApplication.main(FirstAppByGuiApplication.java:10)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
启动报错,原因是找不到org.springframework.boot.SpringApplication
这个类,说白了就是没有指定Class Path,Spring Boot应用的Class Path目录是BOOT-INF/lib
。
也就是说,JarLauncher
可以执行成功,是因为Spring Boot知道了Class Path的路径,说明JarLauncher
在启动调用com.fxipp.spring.FirstAppByGuiApplication
之前,指定了Class Path的位置。
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);
}
}
Archive.Entry
:这个类对对象,代编jar包中的资源文件。
isNestedArchive
方法判断entry对象是不是位于jar包内,如果在jar内部,返回true。如果不在jar包里面,也就是我们解压了jar包,返回false。
重点看launch(String[])
方法
protected void launch(String[] args) throws Exception {
// 1
JarFile.registerUrlProtocolHandler();
// 2
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 3
launch(args, getMainClass(), classLoader);
}
这个方法一共3步
- 扩展JAR协议
- JDK默认支持file、http、jar等协议,所以JDK内部有默认的实现,位于
sun.net.www.protocol
包下。 JarFile.registerUrlProtocolHandler();
这个方法将org.springframework.boot.loader
包下对应的JAR协议实现,覆盖原有的JAR实现。- 因为原有的JAR实现,ClassPath是我们自己配置环境变量的时候制定的,不是
BOOT-INF/lib
。
- JDK默认支持file、http、jar等协议,所以JDK内部有默认的实现,位于
- 创建一个classloader,用于加载
JarLauncher
类,因为jar包可能会被解压,解压前和解压后的的ClassLoader是不同的。 - 调用
launch
方法,将参数传递。- args是我们自己指定的参数。
- getMainClass()是获取
MANIFEST.MF
文件里面Statr-Class属性,也就是获取我们自定义主类的Class 文件地址。 - 传递推出的类加载器
launch方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
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 });
}
}
launch方法分析:
- 将ClassLoader放入当前线程里面的ClassLoader里面
- 创建
MainMethodRunner
对象,调用里面的run()
方法。run()
方法先获取到之前设定的ClassLoader。- 利用ClassLoader加载Start-Class之类的类,也就是我们自己的主类。
- 获取主类里面的
main
方法,通过反射执行。
总结
通过分析,我们可以看出,Spring Boot Loader在调用我们自己的主类之前,主要做了三件事
- 扩展JDK默认的支持JAR对应的协议,因为Spring Boot启动不仅仅需要JDK半身的JAR文件,还需要
BOOT-INF/lib
这个目录下的文件。默认实现无法将BOOT-INF/lib
这个目录当作ClassPath,故需要替换实现。 - 判断当前的介质,是
java -jar
启动,还是java org.springframework.boot.loader.JarLauncher
启动。以便获取对应的ClassLoader。 - 获取
MANIFEST.MF
文件中的Start-Class属性,也就是我们自定义的主类。通过第二步获取的ClassLoader加载获取到Class文件,通过反射调用main
方法,启动应用。