关于java:阅读-Flink-源码前必会的知识-SPI-和-Classloader

45次阅读

共计 6959 个字符,预计需要花费 18 分钟才能阅读完成。

一、本文纲要

二、ClassLoader 类加载器

1、Java 中的类加载器以及双亲委派机制

Java 中的类加载器,是 Java 运行时环境的一部分,负责动静加载 Java 类到 Java 虚拟机的内存中。

有了类加载器,Java 运行零碎不须要晓得文件与文件系统。

那么类加载器,什么类都加载吗?加载的规定是什么?

Java 中的类加载器有四种,别离是:

  • BootstrapClassLoader,顶级类加载器,加载 JVM 本身须要的类;
  • ExtClassLoader,他负责加载扩大类,如 jre/lib/ext 或 java.ext.dirs 目录下的类;
  • AppClassLoader,他负责加载利用类,所有 classpath 目录下的类都能够被这个类加载器加载;
  • 自定义类加载器,如果你要实现本人的类加载器,他的父类加载器都是 AppClassLoader。

类加载器采纳了双亲委派模式,其工作原理是,如果一个类加载器收到了类加载申请,它并不会本人先去加载,而是把这个申请委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,顺次递归,申请最终将达到顶层的启动类加载器,如果父类加载器能够实现类加载工作,就胜利返回,假使父类加载器无奈实现此加载工作,子加载器才会尝试本人去加载,这就是双亲委派模式。

双亲委派模式的益处是什么?

第一,Java 类随着它的类加载器一起具备了一种带有优先级的档次关系,通过这种档次关系能够防止类的反复加载,当父类加载器曾经加载过一次时,没有必要子类再去加载一次。

第二,思考到平安因素,Java 外围 Api 类不会被随便替换,外围类永远是被下层的类加载器加载。如果咱们本人定义了一个 java.lang.String 类,它会优先委派给 BootStrapClassLoader 去加载,加载完了就间接返回了。

如果咱们定义了一个 java.lang.ExtString,能被加载吗?答案也是不能的,因为 java.lang 包是有权限管制的,自定义了这个包,会报一个错如下:

java.lang.SecurityException: Prohibited package name: java.lang

2、双亲委派机制源码浅析

Java 程序的入口就是 sun.misc.Launcher 类,咱们能够从这个类开始看起。

上面是这个类的一些重要的属性,写在正文里了。

public class Launcher {private static URLStreamHandlerFactory factory = new Launcher.Factory();
    // static launchcher 实例
    private static Launcher launcher = new Launcher();
    // bootclassPath,就是 BootStrapClassLoader 加载的系统资源
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    // 在 Launcher 构造方法中,会初始化 AppClassLoader,把它作为全局实例保存起来
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;
    ......
}

这个类加载的时候,就会初始化 Launcher 实例,咱们看一下无参构造方法。

 public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            // 取得 ExtClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            // 取得 AppClassLoader,并赋值到全局属性中
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);
        }
        
        // 把 AppClassLoader 的实例赋值到以后上下文的 ClassLoader 中,和以后线程绑定
        Thread.currentThread().setContextClassLoader(this.loader);
       // ...... 省略无关代码

    }

能够看到,先取得一个 ExtClassLoader,再把 ExtClassLoader 作为父类加载器,传给 AppClassLoader。最终会调用这个办法,把 ExtClassLoader 传给 parent 参数,作为父类加载器。

而在初始化 ExtClassLoader 的时候,没有传参:

Launcher.ExtClassLoader var1;
        try {var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);
        }

而最终,给 ExtClassLoader 的 parent 传的参数是 null。能够先记住这个属性,上面在讲 ClassLoader 源码时会用到这个 parent 属性。

而后 Launcher 源码外面还有四个零碎属性,值得咱们运行一下看看,如下图

从下面的运行后果中,咱们也能够轻易看到不同的类加载器,是从不同的门路下加载不同的资源。而即使咱们只是写一个 Hello World,类加载器也会在前面默默给咱们加载这么多类。

看完了 Launcher 类的代码,咱们再来看 java.lang.ClassLoader 的代码,真正的双亲委派机制的源码是在这个类的 loaderClass 办法中。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {synchronized (getClassLoadingLock(name)) {
            // 首先,查看这个类是否曾经被加载了,最终实现是一个 native 本地实现
            Class<?> c = findLoadedClass(name);
            // 如果还没有被加载,则开始架子啊
            if (c == null) {long t0 = System.nanoTime();
                try {
                    // 首先如果父加载器不为空,则应用父类加载器加载。Launcher 类里提到的 parent 就在这里应用的。if (parent != null) {c = parent.loadClass(name, false);
                    } else {
                        // 如果父加载器为空(比方 ExtClassLoader),就应用 BootStrapClassloader 来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { }
                
                // 如果还没有找到,则应用 findClass 类来加载。也就是说如果咱们自定义类加载器,就重写这个办法
                if (c == null) {long t1 = System.nanoTime();
                    c = findClass(name);

                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {resolveClass(c);
            }
            return c;
        }
    }

这段代码还是比拟清晰的,加载类的时候,首先判断类是不是曾经被加载过了,如果没有被加载过,则看本人的父类加载器是不是为空。如果不为空,则应用父类加载器加载;如果父类加载器为空,则应用 BootStrapClassLoader 加载。

最初,如果还是没有加载到,则应用 findClass 来加载类。

类加载器的基本原理就剖析到这里,上面咱们再来剖析一个 Java 中乏味的概念,SPI。

三、SPI 技术

1、什么是 SPI,为什么要有 SPI

SPI 全称(Service Provide Interface),在 JAVA 中是一个比拟重要的概念,在框架设计中被宽泛应用。

在框架设计中,要遵循的准则是 对扩大凋谢,对批改敞开 ,保障框架实现对于使用者来说是黑盒。因为框架不可能做好所有的事件,只能把共性的局部抽离进去进行流程化,而后留下一些扩大点让使用者去实现,这样不同的扩大就不必批改源代码或者对框架进行定制。也就是咱们常常说的 面向接口编程

我了解的 SPI 用更艰深的话来讲,就是一种可插拔技术。最容易了解的就是 USB,定义好 USB 的接口标准,不同的外设厂家依据 USB 的规范去制作本人的外设,如鼠标,键盘等。另外一个例子就是 JDBC,Java 定义好了 JDBC 的标准,不同的数据库厂商去实现这个标准。Java 并不会管某一个数据库是如何实现 JDBC 的接口的。

2、如何实现 SPI

这里我在 Github 上有一个工程,Flink-Practice,是公众号文章附带的代码,有须要能够下载:

Flink 实战代码

实现 SPI 的话,要遵循上面的一些标准:

  • 服务提供者提供了接口的具体实现后,须要在资源文件夹中创立 META-INF/services 文件夹,并且新建一个以全类名为名字的文本文件,文件内容为实现类的全名(如图中上面的红框);
  • 接口实现类必须在工程的 classpath 下,也就是 maven 中须要退出依赖或者 jar 包援用到工程里(如图中的 serviceimpl 包,我就放在了以后工程下了,执行的时候,会把类编译成 class 文件放到以后工程的 classpath 下的);
  • SPI 的实现类中,必须有一个不带参数的空构造方法

执行测试类之后输入如下:

能够看到,实现了提供方接口的类,都被执行了。

3、SPI 源码浅析

入口在 ServiceLoader.load 办法这里

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 获取以后线程的上下文类加载器。ContextClassLoader 是每个线程绑定的
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

首先须要晓得,Thread.currentThread().getContextClassLoader(); 应用这个获取的类加载器是 AppClassLoader,因为咱们的代码是在 main 函数执行的,而自定义的代码都是 AppClassLoader 加载的。

能够看到最终这个 classloader 是被传到这个中央

那么不传这个 loader 进来,就加载不到吗?答案是的确加载不到。

因为 ServiceLoader 是在 rt.jar 包中,而 rt.jar 包是 BootstrapClassLoader 加载的。而实现了接口提供者的接口的类,个别是第三方类,是在 classpath 下的,BootStrapClassLoader 能加载到 classpath 下的类吗?不能,AppClassLoader 才会去加载 classpath 的类。

所以,这里的上下文类加载器(ContextClassLoader),它其实是毁坏了双亲委派机制的,然而也为程序带来了微小的灵活性和可扩展性。

其实 ServiceLoader 外围的逻辑就在这两个办法里

        private boolean hasNextService() {if (nextName != null) {return true;}
            if (configs == null) {
                try {
                    // 寻找 META-INF/services/ 类
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}
                // 解析这个类文件的所有内容
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() {if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                // 加载这个类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider" + cn + "not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider" + cn  + "not a subtype");
            }
            try {
                // 初始化这个类
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider" + cn + "could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen}

寻找 META-INF/services/ 类,解析类的内容,结构 Class,初始化,返回,就这么简略了。

4、SPI 的毛病以及 Dubbo 是如何重构 SPI 的

通过后面的剖析,能够发现,JDK SPI 在查找扩大实现类的过程中,须要遍历 SPI 配置文件中定义的所有实现类,该过程中会将这些实现类全副实例化。如果 SPI 配置文件中定义了多个实现类,而咱们只须要应用其中一个实现类时,就会生成不必要的对象。例如,在 Dubbo 中,org.apache.dubbo.rpc.Protocol 接口有 InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol、ThriftProtocol 等多个实现,如果应用 JDK SPI,就会加载全副实现类,导致资源的节约。

Dubbo SPI 不仅解决了上述资源节约的问题,还对 SPI 配置文件扩大和批改。

首先,Dubbo 依照 SPI 配置文件的用处,将其分成了三类目录。

META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI。

META-INF/dubbo/ 目录:该目录用于寄存用户自定义 SPI 配置文件。

META-INF/dubbo/internal/ 目录:该目录用于寄存 Dubbo 外部应用的 SPI 配置文件。

而后,Dubbo 将 SPI 配置文件改成了 KV 格局,例如:

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

其中 key 被称为扩展名(也就是 ExtensionName),当咱们在为一个接口查找具体实现类时,能够指定扩展名来抉择相应的扩大实现。例如,这里指定扩大名为 dubbo,Dubbo SPI 就晓得咱们要应用:org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 这个扩大实现类,只实例化这一个扩大实现即可,毋庸实例化 SPI 配置文件中的其余扩大实现类。

四、Flink 源码中应用到 SPI 和 Classloader 的中央

在 Flink 源码中,有很多这样的 SPI 扩大点

在 flink-clients 模块中

执行器工厂的接口,有本地执行器的实现和近程执行器工厂类的实现,这些都是通过 SPI 来实现的。

另外,在 Flink Clients 入口类 CliFronted 中,也应用了经典的 ContextClassLoader 用法,应用反射的形式来执行用户程序中编写的 main 办法

下一篇文章,咱们来剖析 Flink-Clients 的源码实现,敬请期待了

正文完
 0