SPI 机制及源码分析
基础环境准备
创建父工程后删除 src 目录, 操作流程如下:
分别创建: SpiInterface, SpiObject01, SpiObject02
子项目, 夫项目是SpiParent
. 最终创建如下结构:
SPI 实现
准备类与类的关系
在SpiInterface
项目中定义接口如下:
package com.interfaces;
public interface Animal {
public void call();
}
在SpiObject01
项目中定义类, 实现Animal
接口如下:
package com.objects;
import com.interfaces.Animal;
public class Cat implements Animal {
@Override
public void call() {
System.out.println("喵喵喵...");
}
}
当然要引用Animal
, 那么pom.xml
文件中必须进行导入SpiInterface
模块:
<dependencies>
<dependency>
<groupId>com.heihu577</groupId>
<artifactId>SpiInterface</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
在SpiObject02
项目中定义类, 实现Animal
接口如下:
package com.Object;
import com.interfaces.Animal;
public class Dog implements Animal {
@Override
public void call() {
System.out.println("汪汪汪...");
}
}
导入模块:
<dependencies>
<dependency>
<groupId>com.heihu577</groupId>
<artifactId>SpiInterface</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
创建测试模块
这里再多创建一个子项目, 用于测试.
可以看到的是, 当我们引入的模块不同, 我们需要实例化的对象不同. 例如: 引入SpiObject02
我们需要new Dog
, 引入SpiObject01
我们需要new Cat
.
这里需要程序员手动的去引入模块, 手动的编写代码指明实例化谁, 实际上不是很方便. 如果我们手动引入模块之后, 使JAVA程序自动的对它进行实例化岂不是很方便. SPI 解决的就是这个问题, 我们下面看一下具体如何实现.
实现方法
我们需要在SpiObject01 && SpiObject02
的resources
中创建对应的文件:
随后在我们的测试模块中修改pom.xml
:
<dependencies>
<dependency>
<groupId>com.heihu577</groupId>
<artifactId>SpiObject02</artifactId> <!-- 引入 Dog -->
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.heihu577</groupId>
<artifactId>SpiObject01</artifactId> <!-- 引入 Cat -->
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
同时进行引入了, 修改主程序代码如下, 观察运行结果:
public class Main {
public static void main(String[] args) {
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
for (Animal animal : animals) {
System.out.print(animal.getClass() + " - ");
animal.call();
/*
class com.Object.Dog - 汪汪汪...
class com.objects.Cat - 喵喵喵...
* */
}
}
}
引入什么模块, ServiceLoader
会自动读取模块中META-INF/services
目录下的文件, 然后进行读取文件内容, 并通过Class.forName
进行实例化, 下面可以研究一下ServiceLoader
的底层机制.
自定义迭代器
快速入门
在 SPI 源码中, 里面有自定义迭代器
的参与, 所以在这里研究一下自定义迭代器的使用.
public class T1 {
public static void main(String[] args) {
MyIterable strings = new MyIterable(new String[]{"a", "b", "c", "d", "e", "f"});
for (String str : strings) { // 本质上还是调用的 Iterable 的 iterator() 方法中的 hasNext() 判断下一位, 调用 next() 方法取出值
System.out.print(str); // abcdef
}
}
}
class MyIterable implements Iterable<String> {
private String[] elements;
public MyIterable(String[] elements) {
this.elements = elements; // 初始化一个数组
}
@Override
public Iterator<String> iterator() { // 实现 Iterable 接口必须提供 iterator 方法
return new MyIterator();
}
private class MyIterator implements Iterator<String> {
private int index = 0; // 定义一个索引, 每次判断 hasNext
@Override
public boolean hasNext() {
return elements.length > index; // 判断下一个规则
}
@Override
public String next() {
return elements[index++]; // 取出规则
}
}
}
迭代器理解
迭代器比较知名的是我们常用的 ArrayList
, 那么我们通过 ArrayList
来进行理解迭代器的具体实现.
通过继承图可以看到, ArrayList
最终实现了Iterable
接口, 可以看一下该接口声明:
这里注释写的很明确, 如果一个类实现了Iterable
接口, 那么这个类是可以使用增强FOR循环
的.
而ArrayList
中定义的iterator()
方法做了什么, 我们可以看一下:
简单一句话概括: 实现 Iterable 是为了增强 For 循环的使用, 实现 Iterator 是为了定义迭代规则.
SPI 底层源码分析
终于可以进行分析我们SPI
的底层源码了, 我们准备如下DEMO
进行DEBUG
:
public class Main {
public static void main(String[] args) {
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class); // 断点打到这里进行 DEBUG
Iterator<Animal> iterator = animals.iterator();
while (iterator.hasNext()) {
Animal animal = iterator.next();
System.out.println(animal);
}
}
}
看一下ServiceLoader.load
方法到底做了一些什么事情:
可以看到代码的核心是lookupIterator
属性的初始化, 该属性是一个Iterator
, 并且在该Iterator
中放置了接口类型, 当前ClassLoader
这两个属性.
而因为ServiceLoader
类是可迭代的, 也可以使用增强FOR循环
, 所以该类实现了Iterable
接口以及定义了iterator
方法, 如图:
我们这里可以DEBUG
跟进看一下hasNext
方法做了什么:
这里的核心就是读取/META-INF/services/接口
文件, 读取之后取出值放入到names
这个ArrayList
中, 我们继续看一下next()
方法是如何运行的:
最终可以看到成功使用Class.forName
以及成功对其newInstance()
操作.
原文始发于微信公众号(Heihu Share):语言特性 | SPI 机制及源码分析 (含自定义迭代器)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论