前言
文章是我在学习dubbo中间产生的一些疑问和自己的学习路程,记录下来,以后面试要是忘了可以回过头看看,哈哈哈!!!本篇文章讲一下我学习SPI的过程,至于为什么一上来就需要会这个玩意,说实话我也不知道,只是自己在看源码的时候冥冥之中觉得应该先看这个,至于和spring的扩展整合,暂时先不关心了,知道spring会把该做的做了就行了。一、SPI机制
什么是SPI呢?(个人吐槽一下,虽然缩略以后好记,但是对于初学的人来说真的不知道讲的是什么?)
SPI全称Service Provider Interface,翻译过来就是:服务提供者接口。
为什么会有这么个东西出现呢?
我们知道在面向对象的设计里面提倡面向接口编程,这样做的好处就是不面向实现类修改代码的时候就会轻松许多,耦合不是那么明显;同时面向接口编程可以很多做到隐藏实现细节,因此,可以让服务提供者只需要遵循接口规范,具体的细节自己实现。但是不论是面向接口,还是面向具体实现都无法回避的一个问题就是,每次新增功能以后需要修改代码,做不到很多的扩展,因此,出现了SPI。
1.1、java的SPI机制
1.1.1、介绍
SPI使用起来很简单
- 在MET-INF/services/文件夹中新建一个接口全路径名称的文件
- 在文件内部写上具体实现的类的全路径
- 使用java提供的ServiceClassLoader加载接口,会自动去路径下找到类,然后反射加载类进行实例化
- 获得实现类对象以后调用需要用到的接口
好处就是:如果不需要一些实现类,我只需要在配置文件去掉实现类,或者我想新增功能只需要实现具体接口,然后在文件中增加实现类,将模块依赖进去就可以了。
1.1.2、具体实现
- 新建文件接口全路径文件
- 文件内部编辑具体的实现类的全路径
com.cloudwise.myactiviti.spi.DubboSpiService com.cloudwise.myactiviti.spi.JavaSpiService
- 具体代码实现
public static void main(String[] args) { ServiceLoader<SpiInterface> serviceLoader = ServiceLoader.load(SpiInterface.class); Iterator<SpiInterface> iterator = serviceLoader.iterator(); while (iterator.hasNext()){ SpiInterface next = iterator.next(); //调用具体实现类的方法 next.spi(); } }
java SPI的缺点:无法实现按需加载,从上面的代码我们可以看出来,返回迭代器需要循环加载,导致很多我们不需要的类也加载了一遍,造成了内存的浪费,那么dubbo的SPI是如何实现的呢?
1.2、dubbo的SPI机制
1.2.1、介绍
为什么会有dubbo-spi?
一项技术的兴起肯定是为了解决原有技术不能提供的问题,那么dubbo-spi都改进了哪些问题呢?引自官网的一段话
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
- 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
由以上三点可以看出来,dubbo-spi是针对jdk的spi机制进行了优化以及增强,其实大致上的实现思路还是相似的,因此我们从jdk的spi机制入手开始分析dubbo-spi机制。
其实我们可以自己思考一下,为什么dubbo需要spi机制呢?
如果你看过源码,即使你没有看过源码,你只要用过dubbo,一般情况下会使用这种结构
当然,如果你看过源码,会看到如下这种结构,我们拿dubbo-rpc为例来看一下
dubbo-rpc目录结构
我们拿其中的两个协议,http和injvm来看
其实可以看出来,他们有共有的地方exporter、protocol、invoker,其实从dubbo整个工程结构可以看出来,dubbo一直都是支持可扩展机制的,不光是rpc,register也可以看出,可以有很多注册中心,在配置dubbo的时候,我们也可以指定多个协议或者多个配置中心,而不是把所有的协议或者注册中心都加载,因此jdk的spi机制就不能满足dubbo的需要,所以需要自己进行实现一套。
上面说了这么多,其实只是在说一件事,dubbo可以实现高扩展离不开dubbo-spi。同时dubbo-spi又不仅仅是实现了扩展那么简单,他还做了其他的事情,比如
- 扩展自动包装XxxProtocolWrapper
- 扩展自动装配
- 扩展自适应
- 扩展点自动激活
说了这么多我们还是先看看代码是如何实现的吧
1.2.2、具体实现
这里先不展开具体的如何扩展,先简单的看一下dubbo实现的spi如何使用,具体如何进行扩展开发后面会展开学习
如何使用呢?直接看源码怎么实现,怎么照着画一遍就行了
- MET-INF/dubbo/internal/文件夹下新建接口全路径文件名的文件
- 内容填充需要扩展的具体实现的全路径
myProtocol=com.cloudwise.myactiviti.spi.MyProtocol otherProtocol=com.cloudwise.myactiviti.spi.OtherProtocol
- 编写自己的默认实现,继承Protocol接口
public class MyProtocol implements Protocol { @Override public int getDefaultPort() { return 8089; } @Override public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { return null; } @Override public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException { return null; } @Override public void destroy() { }
public class OtherProtocol implements Protocol { @Override public int getDefaultPort() { return 8088; } @Override public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { return null; } @Override public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException { return null; } @Override public void destroy() { } }
- 使用dubbo提供的ExtensionLoader进行文件的加载
public static void main(String[] args) { Protocol myProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol"); System.out.println(myProtocol.getDefaultPort()); }
执行结果
14:32:38.890 [main] INFO org.apache.dubbo.common.logger.LoggerFactory - using logger: org.apache.dubbo.common.logger.slf4j.Slf4jLoggerAdapter
8089
Process finished with exit code 0
可以看出来是按需加载,其实从源码我们也可以看出来,跟踪进去getExtensio方法,我们可以看到
public T getExtension(String name, boolean wrap) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
} else if ("true".equals(name)) {
return this.getDefaultExtension();
} else {
Holder<Object> holder = this.getOrCreateHolder(name);
Object instance = holder.get();
if (instance == null) {
synchronized(holder) {
instance = holder.get();
if (instance == null) {
instance = this.createExtension(name, wrap);
holder.set(instance);
}
}
}
return instance;
}
}
通过经典的双重检查创建单利bean,并且缓存起来,后面通过name进行获取,而不是一开始全部加载。