转载

从Dubbo内核-SPI聊聊双亲委派机制

谈到Dubbo总是避不开SPI思想,因为这个是Dubbo内核中非常重要的一部分,但是SPI是个很大的话题,本篇和之前的 dubbo源码解析-简单原理、与spring融合 一样,为Dubbo源码解析专题的知识预热篇.我们公司 实际项目 就用到了Dubbo的SPI.后面会给大家分享,我们实际项目中,是如何使用SPI,以及SPI后续我们又是如何进优化的.

插播面试题

  • 你是否了解 spi ,讲一讲什么是spi,为什么要使用spi?

  • 对类加载机制了解吗,说一下什么是双亲委托模式,他有什么弊端,这个弊端有没有什么我们熟悉的案例,解决这个弊端的原理又是怎么样的?

spi的简单介绍

如果提到 api 相信大家都知道, spi 的话,知道的人就相对少一些.

简单的说, api 是给使用者使用的, spi 是给拓展者使用的.一个好的开源框架,必须要留一些拓展点.让参与者尽量黑盒拓展,而不是白盒修改代码,否则分支,质量,合并,冲突都会很难管理.并且框架作者能做到的功能,拓展者也一定能做到.

如果从使用层面来说,就是运行时,动态给接口添加实现类.其实这有有点像 IoC 的思想,将装配的控制权移到程序之外

如果从生活中的例子讲,就是比如浏览器插件,比如墙上的插头不够我们就接个排插,而不是伤筋动骨改插头(感觉不是很贴切,前期你暂且这么不规范的粗略理解)

再多的言语都是抽象的,那么我们用代码来简单实现一下 spi

spi的简单实现

接口和具体实现类

public interface ISayName {
    void say();
}
复制代码
public class SayEnglishName implements ISayName{
    @Override
    public void say() {
        System.out.println("Toby");
    }
}
复制代码
public class SayChineseName implements ISayName{
    @Override
    public void say() {
        System.out.println("肥朝");
    }
}
复制代码

配置文件,需放置在 META-INF/services/接口全限定名

com.toby.spi.impl.SayChineseName
com.toby.spi.impl.SayEnglishName
复制代码

demo目录结构

从Dubbo内核-SPI聊聊双亲委派机制

测试结果如下

从Dubbo内核-SPI聊聊双亲委派机制

通过改变配置文件,我们就能动态的改变一个接口的实现类.

细心的小伙伴可能发现,比如我想新增一个实现类 SayFranceNameImpl ,这样的话光改配置文件也还是不行,还要预先包里面就有这个实现类才行啊.

这个先别急,后面会介绍 javassist ,也就是动态字节码技术.这样可以在运行时动态生成Java类,就不存在要预先把接口的实现类先在包里放好.更多内容,关注肥朝即可.

当然细心的小伙伴可能还发现了,这个我就算不用 spi ,我用spring的 ioc 也能通过配置文件动态的注入不同的实现类啊

比如dubbo的设计中,就不想强依赖Spring的IoC容器,但是自已造一个小的IoC容器,也觉得有点过度设计.另外dubbo是不需要依赖任何第三方库的,引用官方文档原话如下

理论上 Dubbo 可以只依赖 JDK,不依赖于任何三方库运行,只需配置使用 JDK 相关实现策略

敲黑板划重点

经常看到有人问两类问题

  • java人这么多,是否饱和了?
  • 为什么总是要面试造火箭,进去拧螺丝?

你可以问一下你同事,你知道什么是 spi 吗,如果他不知道的话,那你觉得他把上面的这个简单的例子实现要多久?如果从 使用 这个层面做区分的话,很难做到有效的区分.无论是做什么,要想在竞争中脱颖而出,就必须做到三个字. 差异化 .

Java基础中比较容易产生差异化的两个区域就在于 JVM并发编程 .如果只是停留在使用层面,那么关注肥朝的博客意义并不大,因此,本篇的 spi 还需要与 ClassLoader 结合.

学习 JVM并发编程 买本书是必不可少的,以下内容参考了 实战Java虚拟机 .如果你看的是 深入Java虚拟机 也没关系,不要纠结于获取知识的渠道,没人在意你做的是五年高考三年模拟还是王后雄学案. 以下内容截取了该书中的部分核心内容,非常感谢作者的辛勤奉献(希望大家支持正版书籍).

从ClassLoader引出spi

ClassLoader的简单介绍

Class装载 大体上可以分为 加载类连接类初始化 三个阶段,在这三个阶段中,所有的 Class 都是由 ClassLoader 进行加载的,然后Java虚拟机负责连接、初始化等操作.也就是说,无法通过 ClassLoader 去改变类的连接和初始化行为.

Java虚拟机会创建三类 ClassLoader ,分别是

BootStrap ClassLoader(启动类加载器)
Extension ClassLoader(扩展类加载器)
APP ClassLoader(应用类加载器,也称为系统类加载器)

此外,每个应用还可以 自定义ClassLoader

ClassLoader的双亲委托模式

ClassLoader 的结构中,还有一个重要的字段 parent ,它也是一个 ClassLoader 的实例,这个字段字段表示的 ClassLoader 也成为这个 ClassLoader 的双亲.在类加载的过程中,可能会将某些请求交于自己的双亲处理.

如图, 应用类加载器 的双亲为 扩展类加载器 , 扩展类加载器 的双亲为 启动类加载器 .

从Dubbo内核-SPI聊聊双亲委派机制

系统中的 ClassLoader 在协同工作时,默认会使用 双亲委托模式 .即在类加载的时候,系统会判断当前类是否已经被加载,如果被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载.

双亲委托模式的弊端

判断类是否加载的时候,应用类加载器会顺着双亲路径往上判断,直到启动类加载器.但是启动类加载器不会往下询问,这个委托路线是单向的,即顶层的类加载器,无法访问底层的类加载器所加载的类,如图

从Dubbo内核-SPI聊聊双亲委派机制

启动类加载器中的类为系统的核心类,比如,在系统类中,提供了一个接口,并且该接口还提供了一个工厂方法用于创建该接口的实例,但是该接口的实现类在应用层中,接口和工厂方法在启动类加载器中,就会出现工厂方法无法创建由应用类加载器加载的应用实例问题.

拥有这样问题的组件有很多,比如 JDBCXml parser 等.JDBC本身是java连接数据库的一个标准,是进行数据库连接的抽象层,由java编写的一组类和接口组成,接口的实现由各个数据库厂商来完成

双亲委托模式的补充

在Java中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,这种方式成为 spi .那我们看一下,在 启动类加载器 中,访问由 应用类加载器 实现spi接口的原理

Thread 类中有两个方法

public ClassLoader getContextClassLoader()//获取线程中的上下文加载器
public void setContextClassLoader(ClassLoader cl)//设置线程中的上下文加载器
复制代码

通过这两个方法,可以把一个 ClassLoader 置于一个线程的实例之中,使该 ClassLoader 成为一个相对共享的实例.这样即使是启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了.如下图

从Dubbo内核-SPI聊聊双亲委派机制
原文  https://juejin.im/post/5c959901e51d456d8054c635
正文到此结束
Loading...