美文网首页Java
2-LaunchedURLClassLoader在FatJar中

2-LaunchedURLClassLoader在FatJar中

作者: 小安的大情调 | 来源:发表于2020-05-25 13:07 被阅读0次

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

    [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<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-LaunchedURLClassLoader在FatJar中

        本文链接:https://www.haomeiwen.com/subject/sghyahtx.html