美文网首页
Classloader、插件化开发(结合Presto)

Classloader、插件化开发(结合Presto)

作者: byamao1 | 来源:发表于2019-03-07 14:53 被阅读0次

    Classloader

    JVM加载class文件到内存有两种方式:

    • 隐式加载:不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存,例如:当类中继承或者引用某个类时,JVM在解析当前这个类不在内存中时,就会自动将这些类加载到内存中。
    • 显式加载:在代码中通过ClassLoader类来加载一个类,例如调用this.getClass.getClassLoader().loadClass()或者Class.forName()

    ClassLoader工作机制

    注意:

    程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制来动态加载某个class文件到内存中。
    ClassLoader工作机制

    双亲委派模型

    双亲委派模式是在Java 1.2后引入的,其工作原理的是:

    如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成

    双亲委派模式优势

    • 避免类的重复加载
    • 安全因素,java核心api中定义类型不会被随意替换

    深入理解Java类加载器

    用类加载器显式加载类例如java.lang.Integer,类加载器只会返回给已加载过的;如果自定义java.lang.Integer并加载之,会报错。
    https://blog.csdn.net/Mint6/article/details/80864788?from=singlemessage

    具体解析

    一般来说,例如程序hello.jar执行到:

    Demo demo = new Demo();
    

    会按照双亲委派模型进行加载类Demo。如果Demohello.jar内,AppClassLoader就将其加载完成;但是如果例如SPI这种,既不在应用hello.jar内又不在系统类路径内,那么就要抛弃双亲委派模型,获取线程上下文类加载器加载(线程上下文类加载器默认是AppClassLoader,此时的线程上下文类加载器肯定是自定义的类加载器)。

    在DriverManager类初始化时执行了loadInitialDrivers()方法,在该方法中通过ServiceLoader.load(Driver.class);去加载外部实现的驱动类,ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容

    这样ServiceLoader会帮助我们处理一切,并最终通过load()方法加载,看看load()方法实现就知道最终是通过线程上下文类加载器加载

    public static <S> ServiceLoader<S> load(Class<S> service) {
         //通过线程上下文类加载器加载
          ClassLoader cl = Thread.currentThread().getContextClassLoader();
          return ServiceLoader.load(service, cl);
    }
    

    自定义一个破坏双亲委派模型的类加载器的方法:

    • 父加载器parent设置为null
    • 重写loadClass()方法直接调用findClass
      (可以参考ClassLoader代码)

    深入理解Java类加载器

    加载指定路径的class或jar

    这里介绍2种加载方式:

    • URLClassLoader直接加载
    • ServiceLoader加载

    URLClassLoader直接加载

    例如要加载类:

    package com;
    
    public class Demo {
        public Demo() {
            System.out.println("\n" + this.getClass().getClassLoader().toString());
        }
    
    }
    

    将其编译为class文件,存放在路径/Users/root/Projects/idea/my/com

    注意!

    • 根目录是/Users/root/Projects/idea/my/com是表示包路径。
    • 该类里面如果引用根目录以外的类,必须在runtime中能够获取到

    这时要加载它:

    @Test
    public void test() throws Exception {
        // 使用根路径
        URL url = new URL("file:/Users/root/Projects/idea/my/");
        ClassLoader newCL = new URLClassLoader(new URL[]{url}, Thread.currentThread().getContextClassLoader());
        
        // 加载。注意要使用全限定名
        Class clazz = Class.forName("com.Demo", false, newCL);
        // 或者
        clazz = newCL.loadClass("com.Demo");
    }
        
    

    ServiceLoader加载

    对于SPI这种,就需要用到ServiceLoader加载。可以参考地址:https://github.com/byamao1/try-plugin

    需要注意:

    • 放入URLClassLoader的URL,目标是jar必须是到jar文件路径,目标是class可以是class的根文件夹路径
    • 自定义类加载器或自己new的URLClassLoader,要在重写的方法loadClass中先判断要加载的类是否为非本加载器加载的类(如spi中的类),如果是则用其他类加载器(例如spi加载器)加载,否则才由自己加载
    • 在idea中resources文件夹下不要直接新建META-INFO.services文件夹,而是要新建文件夹META-INFO后再在其下新建文件夹services(虽然这样建idea的显示就是META-INFO.services,但绝不能按照前面的做,那样只是1个名字叫META-INFO.services的文件夹)。
    • 插件类例如Demo必须有一个无参构造方法,否则ServiceLoader无法实例化插件类

    知识点

    • 放入URLClassLoader的URL的用途就是让该类加载器能加载其应该拥有的jar或class
    • URLClassLoader符合双亲委派模型
    • 从日志中可以看出:Demo中的IDemo是由AppClassLoader加载的;Demo、OtherClass、Internal是由插件类加载器加载。

    插件化

    插件化的一个重要目标就是利用类加载器实现类隔离(比如不同厂商版本的依赖包),其原理在于在类中(例如Demo)隐式类加载器就是Demo的类加载器(一般为插件类加载器),对于插件中出现的插件外的类(例如SPI接口类)则不加载。

    这里分析Presto的connector插件架构。

    Presto的自定义类加载器PluginClassLoader继承URLClassLoader类并重写了loadClass,其类加载逻辑为:

    • 如果类已加载了,就返回它

    • 如果是个SPI接口类,则委托给spiClassLoader(就是PluginManager的类加载器)加载

    • 否则交给父方法super.loadClass加载。这里是真正加载插件类的地方,会到该加载器的成员URLClassPath中找该类。要注意的是,PluginManager.parent为空,实际上就是不会委托父加载器加载,而是只由自己加载(实际上打破了双亲委派模型)。插件类的加载过程是:

      PluginClassLoader载入插件类过程

    注意:

    更改当前线程的ContextClassLoader,只是为了应对扩展程序中可能出现的如下代码:

    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    classLoader.loadClass(...);
    

    Java 自定义 ClassLoader 实现隔离运行不同版本jar包的方式


    从上面我们得知,如果采取ServiceLoader的SPI方案,应该在resources/META-INF/services中存放实现类的全限定名。有意思的是Presto的插件基本都没有这个声明文件,但是编译打包后插件模块的target/classes中却能找到。如果观察插件的pom.xml文件,就会发现<packaging>presto-plugin</packaging>。其实在根pom.xml中使用了presto自己的打包插件presto-maven-plugin,将该maven插件打开看就能发现ServiceDescriptorGenerator中会在打包时自动生成了声明文件。

    SOFA-Ark

    SOFA-Ark是蚂蚁金服开源的一款基于Java实现的轻量级类隔离加载容器。
    具体可以参考博客:sofa-ark类隔离技术分析调研

    站在插件的角度看待,我觉得:

    SOFA-Ark = SPI接口声明 + 插件间可依赖

    Ref

    你应该知道的Java Classloader - 知乎

    相关文章

      网友评论

          本文标题:Classloader、插件化开发(结合Presto)

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