什么是SPI?
SPI(service provider interface)表示服务提供接口。主要用于被第三方实现或扩展的接口。
SPI的作用就是为这些被扩展的接口寻找实现类。
API和SPI的区别?
api是提供方制定接口,并对接口进行实现。调用方只能调用,不能选择实现类。
spi是提供方制定接口。调用方选择自己的实现类调用。
spi优点:
- 解耦。不需要改动源码就可实现扩展。
- 没有侵入性。扩展也不需要改动原来代码
- 只需要添加配置就可实现扩展,符合开闭原则(对扩展开放,对修改关闭)
一、java原生SPI
应用或第三方包在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类的完全限定名。而当框架调用ServiceLoader.load(XXX.class),就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成SPI实现的注入。
规范:
使用示例:
1、定义一个接口
public interface Bird {
String say();
}
2、接口实现两个实现类
public class BlueBird implements Bird {
@Override
public String say() {
return "我是蓝鸟";
}
}
public class RedBird implements Bird {
@Override
public String say() {
return "我是红鸟";
}
}
3、在类路径下创建两层文件夹META-INF/services,增加一个文件,文件名是接口的全限定名
文件内容是接口实现类的全限定名,可以有多个实现类
4、增加一个主类
public class mainApp {
public static void main(String[] args) {
ServiceLoader<Bird> birds = ServiceLoader.load(Bird.class);
for (Bird bird : birds) {
System.out.println(bird.say());
}
}
}
5、执行结果
SPI主要的类:
public final class ServiceLoader<S> implements Iterable<S> {
//扫描目录前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
private final Class<S> service;
// 用于定位、加载和实例化实现方实现的类的类加载器
private final ClassLoader loader;
// 上下文对象
private final AccessControlContext acc;
// 按照实例化的顺序缓存已经实例化的类
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;
// 私有内部类,提供对所有的service的类的加载与实例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
//...
private boolean hasNextService() {
if (configs == null) {
try {
//获取目录下所有的类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//....
}
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
//实例化
S p = service.cast(c.newInstance());
//放进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}
就是直接扫描META-INF/services路径下的文件,找到后则解析文件中的内容。通过反射方式创建实现类的实例。
缺点:
- 代码中写死了路径,只能放在META-INF/services路径下
- 代码中只能遍历所有的实现,并全部示例化。如果不是都需要用实现类,则会浪费性能。
- 接口名文件内容可以配置所有的扩展实现,但是没有命名(别名)。有多个实现类时导致程序很难寻找。
二、dubbo框架中的SPI
dubbo是一个高度可扩展的rpc框架,也依赖于java的spi机制。并且dubbo对java原生spi做了扩展(解决了上面的缺点),使功能更强大
关键一:@SPI注解
使用示例:
1、定义一个接口添加上@SPI注解(设置默认值red)
@SPI("red")
public interface Bird {
String say();
}
2、接口实现两个实现类
public class BlueBird implements Bird {
@Override
public String say() {
return "我是蓝鸟";
}
}
public class RedBird implements Bird {
@Override
public String say() {
return "我是红鸟";
}
}
3、在类路径下创建两层文件夹META-INF/dubbo,增加一个文件,文件名是接口的全限定名
文件内容是接口实现类的全限定名,可以有多个实现类。前面是实现类的别名,K-V结构
4、增加一个主类
public class mainApp {
public static void main(String[] args) {
Bird bird = ExtensionLoader.getExtensionLoader(Bird.class).getDefaultExtension();
System.out.println(bird.say()+"(默认)");
Bird bird2 = ExtensionLoader.getExtensionLoader(Bird.class).getExtension("red");
System.out.println(bird2.say());
Bird bird3 = ExtensionLoader.getExtensionLoader(Bird.class).getExtension("blue");
System.out.println(bird3.say());
}
}
getExtension方法可以通过别名获取实现类。getDefaultExtension方法是获取默认实现类(@SPI注解的值)
5、查看结果
更多方法,功能待发掘:
关键二:@Adaptive注解
用在扩展接口的方法上。表示一个自适应方法。以后有时间在研究。
关键点三:@Activate注解
有时间在研究。
三、使用SPI经典的例子
java中jdbc驱动包。只是定义了接口规范,具体的实现由各大数据库厂商提供