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

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  ServiceLoader load(Class 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文件,就会发现presto-plugin。其实在根pom.xml中使用了presto自己的打包插件presto-maven-plugin,将该maven插件打开看就能发现ServiceDescriptorGenerator中会在打包时自动生成了声明文件。

SOFA-Ark

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

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

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

Ref

你应该知道的Java Classloader - 知乎

你可能感兴趣的:(Classloader、插件化开发(结合Presto))