导语
SPI的全名叫做Service Provider Interface,在java.util.ServiceLoader的文档中有详细的介绍,下面就来通过简单的例子实现SPI深入了解
我们首先来看一下关于JavaSPI的实现
1.首先需要定义一个服务的接口
/**
* @Classname ILog
* @Description TODO
* @Date 2019/8/5 10:11 AM
* @Created by nihui
*/
public interface ILog {
void warn(String msg);
}
这个接口有两个实现类
实现类
/**
* @Classname ConsoleLogImpl
* @Description TODO
* @Date 2019/8/5 10:12 AM
* @Created by nihui
*/
public class ConsoleLogImpl implements ILog {
@Override
public void warn(String msg) {
System.out.println("Console Log "+msg+"!");
}
}
public class FileLogImpl implements ILog {
@Override
public void warn(String msg) {
System.out.println("File Log "+msg+"!");
}
}
2.定义好实现类之后,就需要在classpath下创建一个META-INF/services目录
创建完目录之后需要以接口命名一个文件,文件内容写的是两个接口实现类的类路径如下图所示。
3.接下来就是使用Java类加载机制进行操作
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @Classname Main
* @Description TODO
* @Date 2019/8/5 10:44 AM
* @Created by nihui
*/
public class Main {
private static ServiceLoader<ILogSerivce> services = ServiceLoader.load(ILog.class);
public static void main(String[] args) {
Iterator<ILog> iterator = services.iterator();
while (iterator.hasNext()) {
iterator.next().warn("Hello SPI");
}
}
}
控制台输出
到这里就简单的实现了一个SPI机制的实现。接下来就是分析一下关于ServiceLoader的源码
ServiceLoader源码分析
首先来看一下用到的load()方法
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
*
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
进入方法后首先获取到了线程上下文类加载器,这里为什么要获取线程上下文类加载器,首先在Java类加载机制中采用了双亲委派机制,就是在加载该类的时候首先需要加载该类的父类,那么在多线程的场景下,在线程之间进行信息交换会一定程度上消耗性能,为了避免这个问题,就出现了线程上下文类加载器。既可以保证性能,也可以保证类加载的高效性。
/**
* Creates a new service loader for the given service type and class
* loader.
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @param loader
* The class loader to be used to load provider-configuration files
* and provider classes, or <tt>null</tt> if the system class
* loader (or, failing that, the bootstrap class loader) is to be
* used
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
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();
}
在这里可以看到,首先进行了服务接口与类加载器的判空操作。引入了一个acc的变量,这里首先来关注一下找个变量它提供了Java中的类加载安全控制机制。这个在这里就不在多说,有兴趣的可以深入研究,在后续的博客中也会对这个块内容详细说明。
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
&emps;最后调用了真正实现加载的方法reload()。
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
LazyIterator
字面意思是懒迭代器,实际的意思是注释 Private inner class implementing fully-lazy provider lookup。这里提供了一个懒加载的迭代器的封装,实际上就是一个懒加载处理器。下面就来详细看一下各个方法的功能。
private class LazyIterator
implements Iterator<S>
{
//首先定义了一个泛型的Class
Class<S> service;
//定义一个类加载器
ClassLoader loader;
//同意资源定位配置
Enumeration<URL> configs = null;
//迭代器
Iterator<String> pending = null;
//下一个指针
String nextName = null;
//私有构造函数
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
//判断是否有下一个服务类
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;
}
//获取所有实现类的全名,具体的解析函数查看parse函数
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
}
//获取下一个
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
//是否需要特殊处理
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
//下一个服务
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
//移除操作
public void remove() {
throw new UnsupportedOperationException();
}
}
SPI的使用场景
日志处理
在Java中编程中,提供了属于自己的日志处理机制,但是这些日志处理有时候并不能完全的满足工作的需要,就有人搞出了一些第三方的日志处理框架,那么这些框架是怎么样子和Java进行融合,或者说怎么被加载到Java的日志处理机制中的呢。就是用了我们的SPI机制。
JDBC
在使用数据库连接时候我们需要为不同的数据库导入不同的数据库驱动,对于不同的数据库来说我们只需要修改对应的驱动即可。那么怎么通过类加载机制进行驱动的加载呢?就是使用到了Java的SPI机制,可以通过支持不同的第三方驱动来实现对于不同都数据库的读写操作。
总结
通过上面的解析,可以发现,使用SPI查找具体的实现的时候,需要遍历所有的实现,并实例化;获取某个实现类的方式不够灵活,只能通过Iterator的形式获取,不能根据某个参数来获取对应的实现类。