天天看点

【Java】SPI介绍及实例分析

前言

偶然间发现一个问题,工程中同时有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使用了线程不安全的集合,且未做线程安全处理
爱家人,爱生活,爱设计,爱编程,拥抱精彩人生