2-LaunchedURLClassLoader在FatJar中的重要作用分析及反射的经典应用


我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》


Thinking

  1. 一个技术,为什么要用它,解决了那些问题?
  2. 如果不用会怎么样,有没有其它的解决方法?
  3. 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
  4. 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
  5. 这些问题你又如何去解决的呢?

声明:本文基于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<org.springframework.boot.loader.archive.Archive>)方法
	/**
	 * 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<Archive> archives) throws Exception {
		List<URL> 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;
	}
  • 寻找匹配的可执行的用户定义的入口类。
  • image-20200525122730113)
	/**
	 * 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后端知识,我们一起超神。
qrcode.jpg

——努力努力再努力xLg

加油!

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值