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


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

[TOC]
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)方法
    /**
     * 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;
  }
  • 寻找匹配的可执行的用户定义的入口类。
  • 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-LaunchedURLClassLoader在FatJar中的重要作用分析及反射的经典应用)