参考:
dubbo SPI官网:http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html
https://www.jianshu.com/p/46b42f7f593c
https://blog.csdn.net/djrm11/article/details/88695347
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
使用Java SPI需要遵循一些约定:
- 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
- 接口实现类所在的jar包放在主程序的classpath中;
- 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
- SPI的实现类必须携带一个不带参数的构造方法;
示例
1、首先,我们定义一个接口,名称为 Robot。
Robot.java
package com.example.demo.dao;
public interface Robot {
void sayHello();
}
2、为Robot分别创建两个实现类:RobotImpl1和RobotImpl2。
RobotImpl1.java
package com.example.demo.service;
import com.example.demo.dao.Robot;
public class RobotImpl1 implements Robot {
@Override
public void sayHello() {
System.out.println("This is RobotImpl1 !");
}
}
RobotImpl2.java
package com.example.demo.service;
import com.example.demo.dao.Robot;
public class RobotImpl2 implements Robot {
@Override
public void sayHello() {
System.out.println("This is RobotImpl2 !");
}
}
3、META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 com.example.demo.dao.Robot。文件内容为实现类的全限定的类名,如下:
4、为Robot接口创建测试用例。
RobotTest.java
package com.example.demo.dao;
import org.junit.Test;
import java.util.ServiceLoader;
public class RobotTest {
@Test
public void sayHello() {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
System.out.println("Java SPI Test");
serviceLoader.forEach(Robot::sayHello);
}
}
5、输出结果:
Java SPI Test
This is RobotImpl1 !
This is RobotImpl2 !
机制
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
Java SPI的优点就是解耦,缺点也有。
缺点:
- 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用ServiceLoader类的实例是不安全的。
Dubbo SPI
Dubbo中也大量使用SPI的方式实现框架的扩展,不过它对java提供的原生SPI做了封装,重新实现了一套更强的SPI机制。Dubbo SPI的相关逻辑被封装在了ExtensionLoader类中,通过ExtensionLoader,我们加载指定的实现类。
与java SPI的实现类配置不同,Dubbo SPI是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。
上面的例子,我们使用Dubbo SPI实现了。
1、Robot接口。Robot接口使用@SPI注解标注下。
Robot.java
package com.example.demo.dao;
import com.alibaba.dubbo.common.extension.SPI;
@SPI
public interface Robot {
void sayHello();
}
2、Robot两个实现类不变。
3、Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,并且它是通过键值对的方式配置实现类。
4、为Robot接口创建测试用例。
package com.example.demo.dao;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import org.junit.Test;
import java.util.ServiceLoader;
public class RobotTest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("RobotImpl1");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("RobotImpl2");
bumblebee.sayHello();
}
}
5、输出结果:
This is RobotImpl1 !
This is RobotImpl2 !
上面是Dubbo SPI的一个例子,除此之外,它可以对实现类实例的属性进行依赖注入,即IOC
Dubbo IOC
Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。
在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供了两种 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于创建自适应的拓展,后者是用于从 Spring 的 IOC 容器中获取所需的拓展。