1. 概述
SPI(Service Provider Interface)
,是 Java 6 引入了一个内置功能,实现服务提供发现和加载机制,使之与特定接口的匹配。
SPI
机制的核心思想就是 解耦
,将装配的控制权移到程序之外,这在企业模块化设计中非常重要。
有的人喜欢拿 SPI
与 API
之间做比较,关于二者之间的差异主要在于侧重点不一样。
API
:侧重调用
并用于实现目标的类、接口、方法等的描述;SPI
:侧重对已实现目标类、接口、方法的扩展和实现
;
2. 使用场景
调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。
-
数据库驱动加载接口实现类的加载:
JDBC
加载不同类型数据库的驱动 -
日志门面接口实现类加载:
SLF4J
加载不同提供商的日志实现类 -
Spring
框架:Spring
中大量使用SPI
,比如:对Servlet3.0
规范对ServletContainerInitializer
的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)
等
3. 入门使用介绍
在实际使用 SPI 需要遵循以下约定:
- 按照定义创建加载文件:当服务提供者提供了接口的一种具体实现后,在
jar
包的META-INF/services
目录下创建一个以接口全限定名
为命名的文件,内容为实现类的全限定名; - 动态加载:主程序通过
java.util.ServiceLoder
动态装载实现模块,它通过扫描META-INF/services
目录下的配置文件找到实现类的全限定名,把类加载到JVM
; - 无参构造方法:
SPI
实现类必须携带一个不带参数的构造方法; - 相同classpath:接口实现类所在的
jar
包放在主程序的classpath
中;
4. 示例
4.1. 构建接口
此处演示作用,只定义一组接口,接口为 灵长类
动物,它包含一个方法, 叫 动作
。
package io.github.rothschil.spi.framework;
/**
* 灵长类动物
* @author <a href="mailto:WCNGS@QQ.COM">Sam</a>
* @version 1.0.0
*/
public interface Primate {
void action();
}
4.2. 拓展实现
灵长类
这个接口,我们假定它的实现为人类 、猴子,它们都能有属于自己的动物。
- 人类
package io.github.rothschil.spi.framework.impl;
import io.github.rothschil.spi.framework.Primate;
public class Human implements Primate {
@Override
public void action() {
System.out.println("Human");
}
}
- 猴子
package io.github.rothschil.spi.framework.impl;
import io.github.rothschil.spi.framework.Primate;
public class Monkey implements Primate {
@Override
public void action() {
System.out.println("Monkey");
}
}
4.3. 构建META-INF下文件
- 文件路径:
resources/META-INF/services
- 文件名:
io.github.rothschil.spi.framework.Primate
- 文件内容:
io.github.rothschil.spi.framework.impl.Human
io.github.rothschil.spi.framework.impl.Monkey
4.4. 验证
此处用到 ServiceLoader
类加载器,通过 Primate.class
将它的实现以此加载到 JVM
中。
4.5. 结果
package io.github.rothschil.spi.framework;
import java.util.ServiceLoader;
public class TestSpi {
public static void main(String[] args) {
ServiceLoader<Primate> primates = ServiceLoader.load(Primate.class);
for (Primate pr : primates) {
pr.action();
}
}
}
4.5.1. ServiceLoader
ServiceLoader 本身就是一个迭代器,它的属性并不多,我们以 ServiceLoader.load
为入口,一步一步看下去。
- 调用
ServiceLoader.load
根据当前线程调用类记载器ClassLoader
利用ServiceLoader
构造器,创建一个实例。- 类加载器
- 访问控制器
- 目标类
- 迭代器
ServiceLoader
先判断成员变量providers
对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回- 读取
META-INF/services/
下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader
可以跨越jar
包获取META-INF
下的配置文件 - 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
- 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)然后返回实例对象
- 读取
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
/**
* Clear this loader's provider cache so that all providers will be
* reloaded.
*
* <p> After invoking this method, subsequent invocations of the {@link
* #iterator() iterator} method will lazily look up and instantiate
* providers from scratch, just as is done by a newly-created loader.
*
* <p> This method is intended for use in situations in which new providers
* can be installed into a running Java virtual machine.
*/
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
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();
}
5. 总结
这是我们第一篇 SPI
描述,主要引入它的几个概念、用途以及与我们 API
的区别,最后我们通过一个手写的样例,虽然通过 ServiceLoader
加载的,在实际生产环境中,这存在注入线程安全以及不够灵活注入从而导致资源开销大等问题,但是这只为我们 SPI
学习加深理解,算开启正式 SPI
之旅。