What is SPI
SPI(Service Provider Interface),是一种服务发现机制。通俗地说,就是在系统运行时,能够指定接口选择哪一个实现类。
Java SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
SPI和API区别
SPI (Service Provider Interface)
是调用方
来制定接口规范,提供给外部来实现,调用方在调用时则
选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
API (Application Programming Interface)在
大多数情况下,都是实现方
制定接口并完成对接口的实现,调用方
仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI的简单实现
1. 定义一个接口与对应的方法
public interface PrintService {
/**
* print
*/
void printInfo();
}
2. 编写一个该接口的实现类
public class PrintServiceImpl implements PrintService {
@Override
public void printInfo() {
System.out.println("Hello World");
}
}
3. 在resources目录下新建META-INF/services目录,创建一个以接口全路径名命名的文件,如com.test.PrintService
4. 文件内容为具体的实现类的全路径名,如果有多个,则用分行符分割。如com.test.impl.PrintServiceImpl
5. 在代码中通过java.util.ServiceLoader加载具体的实现类
public static void main(String[] args) {
ServiceLoader<PrintService> serviceServiceLoader = ServiceLoader.load(PrintService.class);
for (PrintService printService : serviceServiceLoader) {
printService.printInfo();
}
}
SPI 原理解析
源码见ServiceLoader类。
SPI加载的主要流程
1. 根据传入的Service类型,创建一个新的ServiceLoader对象
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
2. 创建新的 LazyIterator
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator
扫描(包括所有引用的jar 包里的)META_INF/services/目录下的配置文件并解析配置中的所有service的名字。
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;
}
3. 将配置文件中的全路径名,通过反射加载实例化并放到缓存中
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
Java SPI 缺点
- 一次性实例化扩展点所有实现,如果有扩展实现,则初始化很耗时;如果没用上也加载,则浪费资源
- 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们
- 扩展如果依赖其他的扩展,做不到自动注入和装配
- 展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持
Dubbo SPI
在Java SPI的基础上进行了增强。
关键概念
- 扩展点:接口
- 扩展:接口的实现
- 自动包装、自动加载、自适应、自动激活
- 注解:@SPI 、@Adaptive、@Active
原理解析
通过ExtensionLoader加载指定的实现类。Dubbo SPI所需的配置文件放置在META-INF/dubbo的路径下。
配置模板:key-value结构
printServiceImpl=com.test.PrintService
简单实现
@Test
public void dubboSPITest() {
ExtensionLoader<PrintService> extensionLoader = ExtensionLoader.getExtensionLoader(PrintService.class);
PrintService printServiceImpl = extensionLoader.getExtension("printServiceImpl");
printServiceImpl.printInfo();
}
参考:
http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html