对于Java中的Service类和SPI机制的透彻理解,也算是对Java类加载模型的掌握的不错的一个反映。
了解一个不太熟悉的类,那么从使用案例出发,读懂源代码以及代码内部执行逻辑是一个不错的学习方式。
通常情况下,使用ServiceLoader
来实现SPI机制。 SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。
SPI机制可以归纳为如下的图:
起始这样说起来还是比较抽象,那么下面举一个具体的例子,案例为JDBC的调用例子:
案例如下:
SPI机制的实现核心类为:java.util.ServiceLoader
Provider则为:com.mysql.jdbc.Driver
外层调用则是我们进行增删改查JDBC操作所在的代码块,但是对于那些现在还没有学过JDBC的小伙伴来说(不难学~),这可能会有点难理理解,所以我这里就举一个使用案例:
按照上图的SPI执行逻辑,我们需要写一个接口、至少一个接口的实现类、以及外层调用的测试类。
但是要求以这样的目录书结构来定义项目文件,否则SPI机制无法实现(类加载机制相关,之后会讲):
E:.
│ MyTest.java
│
├─com
│ └─fisherman
│ └─spi
│ │ HelloInterface.java
│ │
│ └─impl
│ HelloJava.java
│ HelloWorld.java
│
└─META-INF
└─services
com.fisherman.spi.HelloInterface
其中:
首先给出接口的逻辑:
public interface HelloInterface {
void sayHello();
}
其次,两个实现类的代码:
public class HelloJava implements HelloInterface {
@Override
public void sayHello() {
System.out.println("HelloJava.");
}
}
public class HelloWorld implements HelloInterface {
@Override
public void sayHello() {
System.out.println("HelloWorld.");
}
}
然后,配置文件:com.fisherman.spi.HelloInterface
com.fisherman.spi.impl.HelloWorld
com.fisherman.spi.impl.HelloJava
最后测试文件:
public class MyTest26 {
public static void main(String[] args) {
ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface.class);
for (HelloInterface in : loaders) {
in.sayHello();
}
}
}
测试文件运行后的控制台输出:
HelloWorld.
HelloJava.
我们从控制台的打印信息可知我们成功地实现了SPI机制,通过 ServiceLoader 类实现了等待实现的接口和实现其接口的类之间的联系。
下面我们来深入探讨以下,SPI机制的内部实现逻辑。
Service类的构造方法是私有的,所以我们只能通过掉用静态方法的方式来返回一个ServiceLoader的实例:
方法的参数为被实现结构的Class对象。
ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface.class);
其内部实现逻辑如所示,不妨按调用步骤来分步讲述:
1.上述load
方法的源代码:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
完成的工作:
2.被调用的另一个load重载方法的源代码:
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
完成的工作:
3.私有构造器的源代码:
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();
}
完成的工作:
空指针和安全性的一些判断以及处理;
并对两个重要要的私有实例变量进行了赋值:
private final Class<S> service;
private final ClassLoader loader;
reload()方法来迭代器的清空并重新赋值
SercviceLoader的初始化跑完如上代码就结束了。但是实际上联系待实现接口和实现接口的类之间的关系并不只是在构造ServiceLoader类的过程中完成的,而是在迭代器的方法hasNext()
中实现的。
这个联系通过动态调用的方式实现,其代码分析就见下一节吧:
在使用案例中写的forEach语句内部逻辑就是迭代器,迭代器的重要方法就是hasNext()
:
ServiceLoader是一个实现了接口Iterable接口的类。
hasNext()
方法的源代码:
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);
}
}
抛出复杂的确保安全的操作,可以将上述代码看作就是调用了方法:hasNextService
.
hasNextService()
方法的源代码:
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;
}
上述代码中比较重要的代码块是:
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
此处PREFIX(前缀)是一个常量字符串(用于规定配置文件放置的目录,使用相对路径,说明其上层目录为以项目名为名的文件夹):
private static final String PREFIX = "META-INF/services/";
那么fullName会被赋值为:"META-INF/services/com.fisherman.spi.HelloInterface"
然后调用方法getSystemResources
或getResources
将fullName参数视作为URL,返回配置文件的URL集合 。
pending = parse(service, configs.nextElement());
parse
方法是凭借 参数1:接口的Class对象 和 参数2:配置文件的URL来解析配置文件,返回值是含有配置文件里面的内容,也就是实现类的全名(包名+类名)字符串的迭代器;
最后调用下面的代码,得到下面要加载的类的完成类路径字符串,相对路径。在使用案例中,此值就可以为:
com.fisherman.spi.impl.HelloWorld
和com.fisherman.spi.impl.HelloJava
nextName = pending.next();
这仅仅是迭代器判断是否还有下一个迭代元素的方法,而获取每轮迭代元素的方法为:nextService()
方法。
nextService()
方法源码:
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
}
抛出一些负责安全以及处理异常的代码,核心代码为:
1.得到接口实现类的完整类路径字符串:
String cn = nextName;
2使用loader引用的类加载器来加载cn指向的接口实现类,并返回其Class对象(但是不初始化此类):
c = Class.forName(cn, false, loader);
3.调用Class对象的newInstance()
方法来调用无参构造方法,返回Provider实例:
S p = service.cast(c.newInstance());
//cast方法只是在null和类型检测通过的情况下进行了简单的强制类型转换
public T cast(Object obj) {
if (obj != null && !isInstance(obj))
throw new ClassCastException(cannotCastMsg(obj));
return (T) obj;
}
4.将Provider实例放置于providers指向的HashMap中:
providers.put(cn, p);
5.返回provider实例:
return p;
ServiceLoader类的小总结:
com.fisherman.spi.impl.HelloWorld
Service.load
方法的调用都会产生一个ServiceLoader实例,不属于单例设计模式;ServiceLoader使用及原理分析
Create Extensible Applications using Java ServiceLoader