【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

阅读 Flink 源码前必会的知识 SPI 和 Classloader

KK架构发布于 今天 13:54

一、本文大纲

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

二、ClassLoader 类加载器

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

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

有了类加载器,Java 运行系统不需要知道文件与文件系统。

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

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

  • BootstrapClassLoader,顶级类加载器,加载JVM自身需要的类;
  • ExtClassLoader,他负责加载扩展类,如 jre/lib/ext 或 java.ext.dirs 目录下的类;
  • AppClassLoader,他负责加载应用类,所有 classpath 目录下的类都可以被这个类加载器加载;
  • 自定义类加载器,如果你要实现自己的类加载器,他的父类加载器都是AppClassLoader。

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

类加载器采用了双亲委派模式,其工作原理是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

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

第一,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 参数,作为父类加载器。

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

而在初始化 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 属性。

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

然后 Launcher 源码里面还有四个系统属性,值得我们运行一下看看,如下图

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

从上面的运行结果中,我们也可以轻易看到不同的类加载器,是从不同的路径下加载不同的资源。而即便我们只是写一个 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实战代码

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

实现 SPI 的话,要遵循下面的一些规范:

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

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

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

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

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 是被传到这个地方

【Java】阅读 Flink 源码前必会的知识 SPI 和 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 模块中

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

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

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

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

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

java大数据Flink

阅读 56发布于 今天 13:54

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议

avatar

KK架构

1 声望

0 粉丝

0 条评论

得票时间

avatar

KK架构

1 声望

0 粉丝

宣传栏

一、本文大纲

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

二、ClassLoader 类加载器

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

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

有了类加载器,Java 运行系统不需要知道文件与文件系统。

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

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

  • BootstrapClassLoader,顶级类加载器,加载JVM自身需要的类;
  • ExtClassLoader,他负责加载扩展类,如 jre/lib/ext 或 java.ext.dirs 目录下的类;
  • AppClassLoader,他负责加载应用类,所有 classpath 目录下的类都可以被这个类加载器加载;
  • 自定义类加载器,如果你要实现自己的类加载器,他的父类加载器都是AppClassLoader。

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

类加载器采用了双亲委派模式,其工作原理是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

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

第一,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 参数,作为父类加载器。

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

而在初始化 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 属性。

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

然后 Launcher 源码里面还有四个系统属性,值得我们运行一下看看,如下图

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

从上面的运行结果中,我们也可以轻易看到不同的类加载器,是从不同的路径下加载不同的资源。而即便我们只是写一个 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实战代码

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

实现 SPI 的话,要遵循下面的一些规范:

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

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

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

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

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 是被传到这个地方

【Java】阅读 Flink 源码前必会的知识 SPI 和 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 模块中

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

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

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

【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader

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

以上是 【Java】阅读 Flink 源码前必会的知识 SPI 和 Classloader 的全部内容, 来源链接: utcz.com/a/109301.html

回到顶部