#### SPI是什么
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
### JavaSPI
要使用Java SPI,需要遵循如下约定:
1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
2、接口实现类所在的jar包放在主程序的classpath中;
3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
4、SPI的实现类必须携带一个不带参数的构造方法;
* Java SPI代码演示
创建`com.jackgreek.dubbospi.javaspi.IShout`接口
```
public interface IShout {
void shout();
}
```
创建`com.jackgreek.dubbospi.javaspi.Dog`实现类
```public class Dog implements IShout {
@Override
public void shout() {
System.out.println("wang wang");
}
}
```
创建`com.jackgreek.dubbospi.javaspi.Cat`实现类
```
public class Cat implements IShout {
@Override
public void shout() {
System.out.println("miao miao");
}
}
```
* 在 src/main/resources/ 下建立 /META-INF/services 目录,新增一个以接口命名的文件 (`com.jackgreek.dubbospi.javaspi.IShout`文件),
```
com.jackgreek.dubbospi.javaspi.Dog
com.jackgreek.dubbospi.javaspi.Cat
```
* 使用 ServiceLoader 来加载配置文件中指定的实现
```
public static void main(String[] args) {
ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
for (IShout s : shouts) {
s.shout();
}
}
```
out
```
Connected to the target VM, address: '127.0.0.1:61147', transport: 'socket'
wang wang
miao miao
```
* ServiceLoader类
```
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
```
参考具体ServiceLoader具体源码,代码量不多,加上注释一共587行,梳理了一下,实现的流程如下:
1 应用程序调用ServiceLoader.load方法
ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
* loader(ClassLoader类型,类加载器)
* acc(AccessControlContext类型,访问控制器)
* providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
* lookupIterator(实现迭代器功能)
2 应用程序通过迭代器接口获取对象实例
ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。
如果没有缓存,执行类的装载,实现如下:
* 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下:
```
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
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;
}
```
* 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
* 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)
然后返回实例对象。
#### DubboSPI
Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。 Dubbo 中,SPI 主要有两种用法,一种是加载固定的扩展类,另一种是加载自适应扩展类。这两种方式会在下面详细的介绍。 需要特别注意的是: 在 Dubbo 中,基于 SPI 扩展加载的类是单例的

###### DubboSPI简单使用
目录结构

创建一个`Car`的接口并使用`@SPI`标注此类
```
@SPI
public interface Car {
String getCarName(URL url);
}
```
编写两个实现类
BlackCar
```
public class BlackCar implements Car {
@Override
public String getCarName(URL url) {
return "black";
}
}
```
RedCar
```
public class RedCar implements Car {
@Override
public String getCarName(URL url) {
return "red";
}
}
```
在 src/main/resources/ 下建立 META-INF/dubbo/目录新增一个以接口命名的文件 (com.jackgreek.dubbospi.spi.impl.Car文件)
com.jackgreek.dubbospi.spi.impl.Car
```
red=com.jackgreek.dubbospi.spi.RedCar
black=com.jackgreek.dubbospi.spi.BlackCar
```
编写测试类
```
ExtensionLoader<Car> extensionLoader1 = ExtensionLoader.getExtensionLoader(Car.class);
Car Car = extensionLoader1.getExtension("black");
System.out.println(Car);
ExtensionLoader<Car> extensionLoader2 = ExtensionLoader.getExtensionLoader(Car.class);
Car red = extensionLoader2.getExtension("red");
System.out.println(red);
```
out
```
com.jackgreek.dubbospi.spi.BlackCar@704a52ec
com.jackgreek.dubbospi.spi.RedCar@6ee52dcd
```
* ExtensionLoader中有三个常⽤的⽅法:
`getExtension("dubbo")`:表示获取名字为dubbo的扩展点实例
`getAdaptiveExtension()`:表示获取⼀个⾃适应的扩展点实例
`getActivateExtension(URL url, String[] values, String group)`:表示⼀个可以被url激活的扩展点
```
ExtensionLoader<Car> extensionLoader2 = ExtensionLoader.getExtensionLoader(Car.class);
Car red = extensionLoader2.getExtension("red");
System.out.println(red.getCarName(null));
```
out
```
red
```
当我们调⽤上述代码,我们会将得到⼀个DubboProtocol的实例对象,但在getExtension()⽅法中, Dubbo会对DubboProtocol对象进⾏依赖注⼊(也就是⾃动给属性赋值,属性的类型为⼀个接⼝,记为A 接⼝),这个时候,对于Dubbo来说它并不知道该给这个属性赋什么值,换句话说,Dubbo并不知道在进 ⾏依赖注⼊时该找⼀个什么的的扩展点对象给这个属性,这时就会预先赋值⼀个A接⼝的⾃适应扩展点实例,也就是A接⼝的⼀个代理对象。

###### DubboSPI⾃适应扩展点
⾃适应扩展点对象,也就是某个接⼝的代理对象是通过Dubbo内部⽣成代理类,然后⽣成代理 对象的。 额外的,在Dubbo中还设计另外⼀种机制来⽣成⾃适应扩展点,这种机制就是可以通过@Adaptive注解来 指定某个类为某个接⼝的代理类,如果指定了,Dubbo在⽣成⾃适应扩展点对象时实际上⽣成的就是 @Adaptive注解所注解的类的实例对象。 如果是由Dubbo默认实现的,那么我们就看看Dubbo是如何⽣成代理类的。
Car接口的getCarName方法上面加上` @Adaptive`
```
@SPI
public interface Car {
@Adaptive
String getCarName(URL url);
}
```
添加Person接口类并写一个 ` getCar()` 方法
```
@SPI
public interface Person {
Car getCar();
}
```
BlackPerson 实现了Person 接口并添加`Car`属性
```
public class BlackPerson implements Person {
private Car car;
public void setCar(Car car) {
this.car = car;
}
@Override
public Car getCar() {
return car;
}
}
```
在 src/main/resources/ 下建立 META-INF/dubbo/目录新增一个以接口命名的文件 (com.jackgreek.dubbospi.spi.impl.Person文件)
com.jackgreek.dubbospi.spi.impl.Person
```
black=com.jackgreek.dubbospi.spi.BlackPerson
```
代码中`black.getCar().getCarName(url)`通过URL对象的方式完成 BlackPerson 实现类中Car的代理对象生成。通过Card代理对象调用Car.getCarName的扩展点
```
ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
Person black = extensionLoader.getExtension("black");
URL url = new URL("x", "localhost", 8080);
url = url.addParameter("car", "black");
System.out.println(black.getCar().getCarName(url));
```
[Dubbo-Spig官方文档](https://dubbo.apache.org/zh/docs/references/spis/dubbo-spi/)

Dubbo-Spi机制