「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。
3. Java SPI 示例
下面我们自己写一个小例子,来更加深刻的理解 SPI 机制。
首先写一个接口和两个对应的实现类:
public interface SPIService {
void say();
}
public class SPIImpl1 implements SPIService {
public void say() {
System.out.println("你好");
}
}
public class SPIImpl2 implements SPIService{
public void say() {
System.out.println("Hello");
}
}
然后我在 META-INF/services/ 目录下建了个以接口全限定名命名的文件,内容是实现类的全限定类名,多个实现类之间用换行符分隔。具体内容如下:
com.demo.spi.SPIImpl1
com.demo.spi.SPIImpl2
然后我们就可以通过 ServiceLoader.load 或者 Service.providers 方法拿到实现类的实例。写一个 mian 方法来测试下:
public class Main {
public static void main(String[] args) {
ServiceLoader<SPIService> serviceLoader = ServiceLoader.load(SPIService);
Iterator<SPIService> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
SPIService next = iterator.next();
next.say();
}
}
}
运行结果:
你好
Hello
Process finished with exit code 0
4. Java SPI 源码分析
我们分析下源码,看具体做了什么,先看下 ServiceLoader 这个类的属性结构:
public final class ServiceLoader<s> implements Iterable<s>
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
//加载的服务类或接口
private final Class<s> service;
//已加载的服务类缓存集合(按实例顺序)
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//类加载器
private final ClassLoader loader;
//内部类,真正加载服务类
private LazyIterator lookupIterator;
}
从 ServiceLoader.load() 进去:
public static <s> ServiceLoader<s> load(Class<s> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
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();
}
public void reload() {
// 现清空集合缓存
providers.clear();
// 实例化内部类,得到一个 LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
可以看出,上边整个过程是先找当前线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,然后清除一下缓存,重要的是实例化了内部类:LazyIterator。最后返回 ServiceLoader 的实例。而 LazyIterator 其实就是 Iterator 的实现类。我们看下它的具体方法:
可以看出,hasNext() 其实是调用的 hasNextService();next() 其实是调用 nextService()。先看下 hasNextService():
private boolean hasNextService() {
//第二次调用的时候,已经解析完成了,直接返回
if (nextName != null) {
return true;
}
if (configs == null) {
//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
String fullName = PREFIX + service.getName();
//将文件路径转成URL对象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
//解析URL文件对象,按行遍历文件内容,最后返回
pending = parse(service, configs.nextElement());
}
//拿到第一个实现类的类名
nextName = pending.next();
return true;
}
这个方法其实就是根据约定好的路径找到接口对应的文件,并对文件内容进行加载和解析。
再看下 next() 方法:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 全限定类名
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 创建类的Class对象
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 {
// 通过newInstance实例化
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
}
这个方法其实就是通过全限定类名加载类,然后创建实例,把实例放入缓存集合最后返回示例。
以上就是 Java SPI 源码的实现过程,思路其实很简单:
- 首先约定好一个目录
- 根据接口名去那个目录找到文件
- 文件解析得到实现类的全限定名
- 然后循环加载实现类和创建其实例
5. Java SPI 总结
JDK 内置的 SPI 机制本身有它的优点,但由于实现比较简单,也有不少缺点。
优点
使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。
缺点
- 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
鉴于 SPI 的诸多缺点,很多系统都是自己实现了一套类加载机制,例如 dubbo。Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。这部分内容以后再和大家继续分享。