我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》
[TOC]
Thinking
- 一个技术,为什么要用它,解决了那些问题?
- 如果不用会怎么样,有没有其它的解决方法?
- 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
- 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
- 这些问题你又如何去解决的呢?
声明:本文基于springboot 2.1.3.RELEASE
写在前面的话:Java ClassLoader
在Java类加载中存在双亲委派,是为了防止Java在类加载时,出现多个不同的ClassLoader 加载同一个Class文件,就会出现多个不同的对象,场面想想就很精彩了。
按道理来说,所有的Java文件都应该遵循这一点的,但是由于双亲委派的局限,导致很多第三方扩展时遇到很大的阻碍,比喻说在
TomCat
中,为了实现每个服务之间实现隔离性,不能遵循这种约定,只能自定义类加载器,去自己完成类加载工作。 而SpringBoot 的jar文件比较特殊,不会存在一个容器中有多个web服务的情况,但是在jar文件规范中,一个jar文件如果要运行必须将入口类放置到jar文件的顶层目录,这样才能被正确的加载。
SpringBoot Jar 通过自定义类加载器打破了这种约束,完美优雅的解决这种问题。实现了多个jar文件的嵌套
FatJar
1、FatJar 在SpringBoot 中的具体实现
在上文中说到了整个SpringBoot为什么要引入FatJar
这种模式。也讲述了它的实用性。那么具体是怎么实现jar文件嵌套还能完美的运行的呢?
/**
* 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
*/
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
- 在上文详细了讲述了
org.springframework.boot.loader.Launcher#getClassPathArchives
方法,就是获取所有符合条件的文件,获取到所有BOOT-INF/classes/
目录下所有的用户类,和BOOT-INF/lib/
下程序一来的所有程序依赖的第三方Jar
包- 现在再来看看
org.springframework.boot.loader.Launcher#createClassLoader(java.util.List
方法)
/**
* Create a classloader for the specified archives.
* @param archives the archives
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
protected ClassLoader createClassLoader(List archives) throws Exception {
List urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
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
*/
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
- 创建一个类加载去根据指定的URL
- 注意这里调用程序时,传递的是当前
Class
文件的类加载器。(加载该类文件的类加载器为 应用类加载器AppClassLoader
)
/**
* 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) {
super(urls, parent);
}
经过一系列的操作,创建一个以
AppClassLoader
为父类加载器的自定义加载器。
再看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; }
- 寻找匹配的可执行的用户定义的入口类。
)
/**
* 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 {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
Thread.currentThread().setContextClassLoader(classLoader);
- 将创建的
LaunchedURLClassLoader
类加载器,赋值为线程上下文类加载器。可以让父类加载器请求子类加载器去完成类加载的动作。- 前面做了那么多工作,就是为了这一步,使用线程上下文类加载器,去加载那些不符合jar规则的文件。这样那些不能被加载的类都可以委托给自定义的类加载器去加载。
createMainMethodRunner(mainClass, args, classLoader).run();
- 这里引入了一个使用线程上下文类加载器去加载Launcher委托的主函数。
org.springframework.boot.loader.MainMethodRunner
/**
* 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.
*
* @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 {
Class> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
}
- 初始化完成后,最重要的方法就是调用该
run()
方法,该方法就是调用用户入口程序的终极入口了。使用反射对Main函数的调用。- 并且使用自定义的ClassLoader去加载用户程序的Main函数。
这里的反射有必要说一下。其实SpringBoot为了满足应用程序的多种启动方式,将程序的启动定义为Main
函数,但是如果SpringBoot只能使用java -jar *.jar
的形式来启动程序的话,Main
完全可以换另外任何一种名称。
- 那么在调用
invoke
方法的时候,为什么第一个参数是null也可以调用成功呢? - 原因就是,SpringBoot的启动类中,
Main
函数是一个静态方法,- 静态方法是跟类的对象没有关系的,
- 静态方法是跟类的class文件挂钩的。所以在获取到该类的class对象后,调用本类的invoke方法是可以直接传递null的。
2、SpringBoot这样做的好处
2.1、为什么要引入自定义类加载器
因为SpringBoot
实现了Jar包的嵌套,一个Jar包完成整个程序的运行。引入自定义类加载器就是为了解决这些不符合jar规格的类无法加载的问题。
区别于Maven
的操作,将每个Jar都一个一个的复制到jar包的顶层。
SpringBoot
的这种方式优雅美观太多。
2.2、为什么SpringBoot要将Loader 类下的所有文件复制出来呢?
因为程序毕竟要有一个启动入口,这个入口要由应用类加载器加载,先将SpringBoot Class Loader加载到内存中。
然后通过后续的一些操作创建线程上下文加载器,去加载第三方jar
那么如果将`SpringBoot Class Loader` 也放到lib文件下,是根本无法被加载到的,因为它根本不符合jar文件的一个标准规范
springboot 这种优雅的方式将我们自己的类和第三方jar包全部分开来放置了。将AppClassLoader加载符合jar规范的
SpringBoot Class Loader
后,整个后续类加载操作都会有自定义类加载器来完成,完美的实现了Jar
包的嵌套,只是添加了一个复制操作而已,带来了太多的便利了!!!
通过MANIFEST.MF
的清单文件来指定它的入口
引用
通俗易懂 启动类加载器、扩展类加载器、应用类加载器
本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!
转载请注明出处!
欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。
——努力努力再努力xLg
加油!