前言
偶然间发现一个问题,工程中同时有H2*.jar,sqlite*.jar,但代码中只使用到了h2数据库,可是发现org.sqlite.JDBC类被加载了,并且org.sqlite.JDBC的静态代码块执行了!这是怎么做到的呢?还好之前了解过spi,发现这是通过SPI机制实现的。
1 SPI是什么? 和API有啥区别呢?
API(Application Programming Interface,应用程序接口 ),SPI(Service Provider Interface,服务提供接口)
API是应用程序内部接口,而SPI是给外部(第三方)服务暴露的接口。
SPI本质上提供了一种服务发现机制,通过某个接口来查找外部实现了该接口的服务,配合ServiceLoader等库,可以实现服务的自动装载,类似于Spring的IOC,本质上都是解耦,面向接口编程。
看着迷糊?来个例子看看。
2 举个栗子
场景:我们需要一个告警服务,但服务的实现者并不存在于本工程内。我们在外部提供syslog和kafka两种方式实现告警服务。
定义一个通用告警接口:
/**
* 告警APi
* @author chaozai
* @date 2019年4月17日
*
*/
public interface WarningAPI {
/**
* 发送告警消息
*/
void sendWarningMsg(String msg);
}
两个外部告警实现:
public class SyslogWarningAPI implements WarningAPI{
@Override
public void sendWarningMsg(String msg) {
System.out.println("use syslog send warning msg : "+msg);
}
}
public class KafkaWarningAPI implements WarningAPI{
@Override
public void sendWarningMsg(String msg) {
System.out.println("use kafka send warning msg : "+msg);
}
}
SPI配置(关键):
src(java工程)或者src\main\resources下创建META-INF/services文件夹,在该文件夹下创建spi.WarningAPI(接口的全限定名:包+类名)文件,文件内容为服务的全限定名:
spi.SyslogWarningAPI
spi.KafkaWarningAPI
测试程序:
/**
* SPI测试
* @author chaozai
* @date 2019年4月17日
*
*/
public class SPITest {
public static void main(String[] args) {
ServiceLoader<WarningAPI> loadedAPIs = ServiceLoader.load(WarningAPI.class);
Iterator<WarningAPI> apiIterator = loadedAPIs.iterator();
try{
while(apiIterator.hasNext()) {
WarningAPI warningAPI = apiIterator.next();
warningAPI.sendWarningMsg("spi test");
}
} catch(Throwable t) {
t.printStackTrace();
}
}
}
测试结果:
use syslog send warning msg : spi test
use kafka send warning msg : spi test
使用总结:
1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
2、接口实现类所在的jar包放在主程序的classpath中;
3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描所有META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
4、SPI的实现类必须有一个无参的构造方法;
ServiceLoader提供了服务发现,加载,遍历,初始化等功能,下面简略的看看源码吧。
3 ServiceLoader源码
成员变量:被加载的服务接口类;类加载器;权限控制器;缓存服务实例;懒加载器(使用时才去遍历文件哦);
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;
第一步:加载
public static <S> ServiceLoader<S> load(Class<S> service) {
//上下文的类加载器,一般为SystemClassloader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
//创建ServiceLoader对象
return new ServiceLoader<>(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();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
发现:仅仅是初始化了各个成员变量
第二步:得到遍历器
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
第三步:查找服务
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;
}
第四步:获取服务对象
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
}
4 使用场景
- 数据库驱动Driver加载
- SLF4J加载不同提供商的日志实现类
- Spring和Dubbo等
5 不足之处
- 遍历过程中会将所有相关Class全部加载,一般加载过程同时也进行了初始化操作,如Driver。
- ServiceLoader线程不安全,如provider使用了线程不安全的集合,且未做线程安全处理
爱家人,爱生活,爱设计,爱编程,拥抱精彩人生