谈到Dubbo总是避不开SPI思想,因为这个是Dubbo内核中非常重要的一部分,但是SPI是个很大的话题,本篇和之前的 dubbo源码解析-简单原理、与spring融合 一样,为Dubbo源码解析专题的知识预热篇.我们公司 实际项目
就用到了Dubbo的SPI.后面会给大家分享,我们实际项目中,是如何使用SPI,以及SPI后续我们又是如何进优化的.
你是否了解 spi
,讲一讲什么是spi,为什么要使用spi?
对类加载机制了解吗,说一下什么是双亲委托模式,他有什么弊端,这个弊端有没有什么我们熟悉的案例,解决这个弊端的原理又是怎么样的?
如果提到 api
相信大家都知道, spi
的话,知道的人就相对少一些.
简单的说, api
是给使用者使用的, spi
是给拓展者使用的.一个好的开源框架,必须要留一些拓展点.让参与者尽量黑盒拓展,而不是白盒修改代码,否则分支,质量,合并,冲突都会很难管理.并且框架作者能做到的功能,拓展者也一定能做到.
如果从使用层面来说,就是运行时,动态给接口添加实现类.其实这有有点像 IoC
的思想,将装配的控制权移到程序之外
如果从生活中的例子讲,就是比如浏览器插件,比如墙上的插头不够我们就接个排插,而不是伤筋动骨改插头(感觉不是很贴切,前期你暂且这么不规范的粗略理解)
再多的言语都是抽象的,那么我们用代码来简单实现一下 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目录结构
测试结果如下
通过改变配置文件,我们就能动态的改变一个接口的实现类.
细心的小伙伴可能发现,比如我想新增一个实现类 SayFranceNameImpl
,这样的话光改配置文件也还是不行,还要预先包里面就有这个实现类才行啊.
这个先别急,后面会介绍 javassist
,也就是动态字节码技术.这样可以在运行时动态生成Java类,就不存在要预先把接口的实现类先在包里放好.更多内容,关注肥朝即可.
当然细心的小伙伴可能还发现了,这个我就算不用 spi
,我用spring的 ioc
也能通过配置文件动态的注入不同的实现类啊
比如dubbo的设计中,就不想强依赖Spring的IoC容器,但是自已造一个小的IoC容器,也觉得有点过度设计.另外dubbo是不需要依赖任何第三方库的,引用官方文档原话如下
理论上 Dubbo 可以只依赖 JDK,不依赖于任何三方库运行,只需配置使用 JDK 相关实现策略
经常看到有人问两类问题
你可以问一下你同事,你知道什么是 spi
吗,如果他不知道的话,那你觉得他把上面的这个简单的例子实现要多久?如果从 使用
这个层面做区分的话,很难做到有效的区分.无论是做什么,要想在竞争中脱颖而出,就必须做到三个字. 差异化
.
Java基础中比较容易产生差异化的两个区域就在于 JVM
和 并发编程
.如果只是停留在使用层面,那么关注肥朝的博客意义并不大,因此,本篇的 spi
还需要与 ClassLoader
结合.
学习 JVM
和 并发编程
买本书是必不可少的,以下内容参考了 实战Java虚拟机
.如果你看的是 深入Java虚拟机
也没关系,不要纠结于获取知识的渠道,没人在意你做的是五年高考三年模拟还是王后雄学案. 以下内容截取了该书中的部分核心内容,非常感谢作者的辛勤奉献(希望大家支持正版书籍).
Class
的 装载
大体上可以分为 加载类
、 连接类
和 初始化
三个阶段,在这三个阶段中,所有的 Class
都是由 ClassLoader
进行加载的,然后Java虚拟机负责连接、初始化等操作.也就是说,无法通过 ClassLoader
去改变类的连接和初始化行为.
Java虚拟机会创建三类 ClassLoader
,分别是
BootStrap ClassLoader(启动类加载器) Extension ClassLoader(扩展类加载器) APP ClassLoader(应用类加载器,也称为系统类加载器)
此外,每个应用还可以 自定义ClassLoader
在 ClassLoader
的结构中,还有一个重要的字段 parent
,它也是一个 ClassLoader
的实例,这个字段字段表示的 ClassLoader
也成为这个 ClassLoader
的双亲.在类加载的过程中,可能会将某些请求交于自己的双亲处理.
如图, 应用类加载器
的双亲为 扩展类加载器
, 扩展类加载器
的双亲为 启动类加载器
.
系统中的 ClassLoader
在协同工作时,默认会使用 双亲委托模式
.即在类加载的时候,系统会判断当前类是否已经被加载,如果被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载.
判断类是否加载的时候,应用类加载器会顺着双亲路径往上判断,直到启动类加载器.但是启动类加载器不会往下询问,这个委托路线是单向的,即顶层的类加载器,无法访问底层的类加载器所加载的类,如图
启动类加载器中的类为系统的核心类,比如,在系统类中,提供了一个接口,并且该接口还提供了一个工厂方法用于创建该接口的实例,但是该接口的实现类在应用层中,接口和工厂方法在启动类加载器中,就会出现工厂方法无法创建由应用类加载器加载的应用实例问题.
拥有这样问题的组件有很多,比如 JDBC
、 Xml parser
等.JDBC本身是java连接数据库的一个标准,是进行数据库连接的抽象层,由java编写的一组类和接口组成,接口的实现由各个数据库厂商来完成
在Java中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,这种方式成为 spi
.那我们看一下,在 启动类加载器
中,访问由 应用类加载器
实现spi接口的原理
Thread
类中有两个方法
public ClassLoader getContextClassLoader()//获取线程中的上下文加载器 public void setContextClassLoader(ClassLoader cl)//设置线程中的上下文加载器 复制代码
通过这两个方法,可以把一个 ClassLoader
置于一个线程的实例之中,使该 ClassLoader
成为一个相对共享的实例.这样即使是启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了.如下图