正常情况下 classloader 只能找到 jar 里面当前目录或者文件类里面的 *.class 文件。为了能够加载嵌套 jar 里面的资源之前都是把嵌套 jar 里面的 class 文件和应用的 class 文件打包为一个 jar,这样就不存在嵌套 jar 了,但是这样做就不能很清晰的知道应用到底依赖了哪些东西,哪些是应用自己的,另外多个 jar 里面的 class 可能内容不一样但是文件名却一样。springboot 中 spring-boot-loader 就是为优雅解决这个问题而诞生的。
spring-boot-loader 模块允许我们使用 java -jar archive.jar 运行包含嵌套依赖 jar 的 jar 或者 war 文件,它提供了三种类启动器 (JarLauncher, WarLauncher and PropertiesLauncher),这些类启动器的目的一样都是为了能够加载嵌套在 jar 里面的资源(比如 class 文件,配置文件等)。[Jar|War] Launcher 固定去查找当前 jar 的 lib 目录里面的嵌套 jar 文件里面的资源。
熟悉后端服务开发的小伙伴,在使用 SpringBoot 时一定会有这样的感受,咦,以前繁琐的配置,现在都不用再去配置一大堆东西了,以前跑起来一个 demo,感觉真是千辛万苦,错一步就 game over,以前服务基本都是已 war 包的形式运行在 Tomcat 中,而现在,你基本不需要手动写太多的代码,一个应用服务就可以运行起来,其次现在应用基本已 jar 包方式直接运行,虽然本质还是运行在 Tomcat 中,但现在 jar 包中已经有了服务运行的基础环境,可以直接使用 jar 相关的运行命令就可以运行起服务。好了,废话了这么多,先看看我们如何运行起一个 DEMO 应用。
bootRun:Runs this project as a Spring Boot application.(以 bootJar 的形式运行当前项目)
Build tasks
bootJar:Assembles an executable jar archive containing the main classes and their dependencies.(装配一个可执行的 jar(自包含的 jar 包,不依赖其他容器) 归档,这个归档 jar 中包含了所需的依赖以及主类等)
Run jar
1
java -jar jar-name.jar
Other
1
2
# 解压 jar 到当前 start 目录下
unzip start-0.0.1-SNAPSHOT.jar -d ./start
对于 Java 标准的 jar 文件来说,规定在一个 jar 文件中,我们必须要将指定 main.class 的类直接放置在文件的顶层目录中(也就是说,它不予许被嵌套),否则将无法加载,对于 BOOT-INF/class/ 路径下的 class 因为不在顶层目录,因此也是无法直接进行加载, 而对于 BOOT-INF/lib/ 路径的 jar 属于嵌套的(Fatjar),也是不能直接加载,因此 Spring 要想启动加载,就需要自定义实现自己的类加载器去加载。
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
* included inside a {@code /BOOT-INF/classes} directory.
*
* 用于基于JAR的归档。这个启动程序假设依赖jar包含在{@code /BOOT-INF/lib}目录中,
* 应用程序类包含在{@code /BOOT-INF/classes}目录中
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
紧接着,要进行源码分析,那肯定是找到入口,一步步深入,那么对于 JarLauncher 就是它的 main 方法了
1
2
3
4
public static void main(String[] args) throws Exception {
// launch 方法是调用父类 Launcher 的 launch 方法
new JarLauncher().launch(args);
}
那我们去看一看 Launcher 的 launch 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
*
* 启动一个应用,这个方法应该被初始的入口点,这个入口点应该是一个Launcher的子类的
* 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 {
// 1. 注册一些 URL的属性
JarFile.registerUrlProtocolHandler();
// 2. 创建类加载器(LaunchedURLClassLoader),加载得到集合要么是BOOT-INF/classes/
// 或者BOOT-INF/lib/的目录或者是他们下边的class文件或者jar依赖文件
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 3. 启动给定归档文件和完全配置的类加载器的应用程序
launch(args, getMainClass(), classLoader);
}
/**
* Determine if the specified {@link JarEntry} is a nested item that should be added
* to the classpath. The method is called once for each entry.
*
* 确定指定的{@link JarEntry}是否是应该添加到类路径的嵌套项。对每个条目调用该方法一次
*
* @param entry the jar entry
* @return {@code true} if the entry is a nested item (jar or folder)
*/
protected abstract boolean isNestedArchive(Archive.Entry entry);
/
// 紧接着我们查看 isNestedArchive() 实现 //
/
// JarLauncher.class 中的 isNestedArchive()方法
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
// 如果是目录判断是不是BOOT-INF/classes/目录
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 如果是文件判断文件的前缀是不是BOOT-INF/lib/开头
return entry.getName().startsWith(BOOT_INF_LIB);
}
/**
* Returns the main class that should be launched.
* @return the name of the main class
* @throws Exception if the main class cannot be obtained
*/
protected abstract String getMainClass() throws Exception;
/
// 紧接着我们查看 getMainClass() 实现 //
/
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 获取到 Manifest 文件中属性为`Start-Class`对应的值,也就是当前项目工程启动的类的完整路径
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
/**
* Launch the application given the archive file and a fully configured classloader.
*
* 加载指定存档文件和完全配置的类加载器的应用程序
*
* @param args the incoming arguments
* @param mainClass the main class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
// 将应用的加载器换成了自定义的 LaunchedURLClassLoader 加载器,然后入到线程类加载器中
// 最终在未来的某个地方,通过线程的上下文中取出类加载进行加载
Thread.currentThread().setContextClassLoader(classLoader);
// 创建一个主方法运行器运行
createMainMethodRunner(mainClass, args, classLoader).run();
}
/**
* Create the {@code MainMethodRunner} used to launch the application.
*
* 创建一个 MainMethodRunner 用于启动这个应用
*
* @param mainClass the main class
* @param args the incoming arguments
* @param classLoader the classloader
* @return the main method runner
*/
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
/**
* Utility class that is used by {@link Launcher}s to call a main method. The class
* containing the main method is loaded using the thread context class loader.
*
* 被 Launcher 使用来调用 main 方法的辅助类,使用线程类加载来加载包含 main 方法的类
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
// 关键方法
public void run() throws Exception {
// 获取到当前线程上下文的类加载器,实际就是 springboot 自定义的加载器(LaunchedURLClassLoader)
// 加载 this.mainClassName所对应的类,实际也就是清单文件中对应 Start-Class 属性的类
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
// 通过反射获取到 main 方法和参数
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
// 调用目标方法运行
// invoke 方法参数一:是被调用方法所在对象,这里为 null,原因是我们所调用的目标方法是一个静态方法
// invoke 方法参数二:被调用方法所接收的参数
mainMethod.invoke(null, new Object[] { this.args });
}
}
到此为止,invoke 方法成功调用,那么我们项目中的 main 方法就执行了,这时我们的所编写的 springboot 应用就正式的启动了。那么关于 springboot 的 loader 加载过程已经分析完
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------
Application tasks
-----------------
bootRun - Runs this project as a Spring Boot application.
Build tasks
-----------
assemble - Assembles the outputs of this project.
bootJar - Assembles an executable jar archive containing the main classes and their dependencies.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.
Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'spring-lecture'.
components - Displays the components produced by root project 'spring-lecture'. [incubating]
dependencies - Displays all dependencies declared in root project 'spring-lecture'.
dependencyInsight - Displays the insight into a specific dependency in root project 'spring-lecture'.
dependencyManagement - Displays the dependency management declared in root project 'spring-lecture'.
dependentComponents - Displays the dependent components of components in root project 'spring-lecture'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'spring-lecture'. [incubating]
projects - Displays the sub-projects of root project 'spring-lecture'.
properties - Displays the properties of root project 'spring-lecture'.
tasks - Displays the tasks runnable from root project 'spring-lecture'.
Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.
Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.
To see all tasks and more detail, run gradlew tasks --all
To see more detail about a task, run gradlew help --task <task>
BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
【bootJar - Assembles an executable jar archive containing the main classes and their dependencies.】 这里就是将整个工程打包,生成一个自包含的 jar 包。 我们可以将当前工程下的 build/libs 下的 jar 包删除,然后执行: ./gradlew bootJar 就会重新生成 jar 包。jar 包名字:工程名字 - 版本号.jar, 这个 jar 包我们可以直接用 java 运行:
bootjar.png
这个 jar 是一个自包含的,里边包含了运行的所有依赖,使用 jar 命令对其解压,查看内部有哪些内容:
1
2
3
4
jar -xvf spring-lecture-0.0.1-SNAPSHOT.jar
BOOT-INF
META-INF
org
Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.
基于 jar 的启动器,启动器假设 jar 包的依赖在 /BOOT-INF/lib 目录下,应用的 class 在 / BOOT-INF/classes 目录内。
JarLauncher 的继承体系结构与源码分析:
bootjar4.png
ExecutableArchiveLauncher 下面有 JarLauncher 和 WarLauncher,我们只看 JarLauncher。 JarLauncher 由于是启动类,肯定有 main 方法:
1
2
3
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
启动一个应用,这个方法应该被初始的入口点,这个入口点应该是一个Launcher的子类的 public static void main(String[] args)这样的方法调用
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();//不重要略过,只是注册一些关于url的属性
//构建一个类加载器,我么先看一下getClassPathArchives方法做了什么
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
getClassPathArchives():
1
2
3
4
5
6
7
/**
* Returns the archives that will be used to construct the class path.
* @return the class path archives
* @throws Exception if the class path archives cannot be obtained
*/
Launcher的一个抽象方法,返回用来构建class path的归档文件
protected abstract List<Archive> getClassPathArchives() throws Exception;
/**
* Returns nested {@link Archive}s for entries that match the specified filter.
* @param filter the filter used to limit entries
* @return nested archives
* @throws IOException if nested archives cannot be read
*/
根据过滤器filter返回嵌套的归档文件
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
getNestedArchives 实现类 JarFileArchive:
1
2
3
4
5
6
7
8
9
10
11
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
// JarFileArchive实现了 Iterable<Archive.Entry>
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
ExecutableArchiveLauncher的对isNestedArchive的抽象:
/**
* Determine if the specified {@link JarEntry} is a nested item that should be added
* to the classpath. The method is called once for each entry.
* @param entry the jar entry
* @return {@code true} if the entry is a nested item (jar or folder)
*/
判断指定的JarEntry是不是一个嵌套的,如果是嵌套的需要添加到classpath下边,这个方法对于每个entry只被调用一次。
protected abstract boolean isNestedArchive(Archive.Entry entry);
JarLauncher的实现:
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
//如果是目录判断是不是BOOT-INF/classes/目录
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
//如果是文件判断文件的前缀是不是BOOT-INF/lib/开头
return entry.getName().startsWith(BOOT_INF_LIB);
}
这里做一下补充:规范要求,jar 文件的里边的启动类,必须是位于目录的顶层结构中,启动类不能位于嵌套的 jar 里边。 我们看到 jar 包的 org 目录下边的 JarLauncher 是在顶层目录的结构里边,是可以被加载的,而 BOOT-INF/lib/ 里边的 jar 属于嵌套的(FatJar),同样 BOOT-INF/classes/ 里边的类 没有在 jar 目录的顶层结构同样无法加载执行,spring 要想加载执行他们需要做一些工作,后续我们会介绍,其实是 spring 自定义了自己的类加载器去加载。 回到 Launcher 的 launch 方法
/**
* Create a classloader for the specified archives.
* @param archives the archives
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
// 为指定为归档文件创建类加载器,archives是我们拿到的所有的要加载的归档,返回一个spring定义的一个自定义加载器
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
//将所有归档文件的url统一放在一个集合里边
return createClassLoader(urls.toArray(new URL[0]));
}
/**
* Create a classloader for the specified URLs.
* @param urls the URLs
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
//为指定的url创建一个类加载器
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
//自定义类加载器需要指定父加载器,getClass()是Launcher.clas 得到Launcher类的类加载器,即appClassLoader,系统类加载器
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
LaunchedURLClassLoader 继承了 URLClassLoader:
1
2
3
4
5
6
7
8
9
10
/**
* Create a new {@link LaunchedURLClassLoader} instance.
* @param urls the URLs from which to load classes and resources
* @param parent the parent class loader for delegation
*/
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
//直接调用了URLClassLoader的构造器
super(urls, parent);
}
URLClassLoader 的说明: This class loader is used to load classes and resources from a search path of URLs referring to both JAR files and directories. Any URL that ends with a ‘/‘ is assumed to refer to a directory. Otherwise, the URL is assumed to refer to a JAR file which will be opened as needed. The AccessControlContext of the thread that created the instance of URLClassLoader will be used when subsequently loading classes and resources. The classes that are loaded are by default granted permission only to access the URLs specified when the URLClassLoader was created. 这个类加载器用来加载类和资源,这些资源是通过搜索一个 jar 文件或者一个目录,任何一个以 “/” 结尾的会认为是一个目录,否则会认为是一个 jar 文件。
/**
* Launch the application given the archive file and a fully configured classloader.
* @param args the incoming arguments
* @param mainClass the main class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
//当前线程的上下文类加载器模式是系统类加载器,此处将线程山下文类加载器设置为spring自定义的LaunchedURLClassLoader
//此处是set了LaunchedURLClassLoader,后边在某个时刻一定会get出来使用,去加载类,惯用套路
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
/**
* Create the {@code MainMethodRunner} used to launch the application.
* @param mainClass the main class
* @param args the incoming arguments
* @param classLoader the classloader
* @return the main method runner
*/
//创建一个MainMethodRunner用来驱动运用
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
MainMethodRunner 的 doc: Utility class that is used by Launchers to call a main method. The class containing the main method is loaded using the thread context class loader. 工具类,用来执行一个 main 方法,包含 main 方法的类是由线程上下文类加载器加载(线程山下文类加载器是 LaunchedURLClassLoader)
这里有个点就是 其实我们的 com.twodragonlake.boot.MyApplication 的 main 方法,不用非得名字是 main,只不过是 spring 规定的是 main,因为 spring 的代码里边写死是 main,spring 之所以规定方法名字必须是 main 的原因 是我们可能使用 idea 直接右单击运行我们的应用,,所以 2 者结合进行兼容,规定名字必须是 main。
验证
我们修改 MyApplication 的 main 方法加入一行打印 MyApplication 的加载器的代码;
1
2
3
4
5
6
7
8
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
//打印MyApplication的类加载器
System.out.println("MyApplication classloader: "+MyApplication.class.getClassLoader());
SpringApplication.run(MyApplication.class,args);
}
}
使用如下两种方式运行:
直接用 idea 右单击运行 main 方法
使用 gradle bootJar 打成 jar 包,然后使用 java -jar spring_lecture-1.0-SNAPSHOT.jar 运行