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<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

加油!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271