本文基于jdk1.8
一.SPI是何物?
SPI是Service Provider Interface的缩写,用于解耦服务接口与其具体实现。
java编程中最常见的SPI实现是解耦数据库驱动。
想想平时引入mysql或者postgresql的jar就能连上数据库,是不是很神奇。
二. 快速实现SPI接口
来动手写一个demo,熟悉下。
1. 定义一个用户服务,及其实现
public interface UserService {
String getUserName(long userId);
}
public class UserServiceImpl implements UserService {
@Override
public String getUserName(long userId) {
return String.valueOf(userId).concat("用户名");
}
}
2. 创建资源文件
文件名称为用户服务接口全路径名,内容为其实现类全路径名,大体如下图
3. 测试下demo
@SpringBootTest
class JdkSpiApplicationTests {
@Test
void contextLoads() {
ServiceLoader<UserService> load = ServiceLoader.load(UserService.class);
Iterator<UserService> iterator = load.iterator();
while (iterator.hasNext()) {
UserService userService = iterator.next();
System.out.println("调用服务获取用户名");
System.out.println(userService.getUserName(1L));
}
}
}
结果如下:
三.服务加载器-ServiceLoader
上文简单实现了一个SPI,接下来看下,具体是如何实现上述效果。
1.获取ServiceLoader
进入load方法
ServiceLoader.load(UserService.class);
load方法会构建一个ServiceLoader对象返回,这是一个静态工厂的实现案例。
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);
}
ServiceLoader的关键属性如下,主要做了三件事:
- 保存类加载器,接口类等属性
- 清空实例缓存
- 生成惰性加载器
// 缓存提供接口实例化的
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒惰迭代器
private LazyIterator lookupIterator;
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 设置接口类
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 设置类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 这里设置安全管理器,一般是null
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
// 清空缓存
providers.clear();
// 创建惰性迭代器
lookupIterator = new LazyIterator(service, loader);
}
2.获取迭代器
Iterator<UserService> iterator = load.iterator();
第二步获取迭代对象,这里获取的ServiceLoader的内部类。
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();
}
};
}
3.判断是否有迭代对象
while (iterator.hasNext()) {
...
}
hasNext方法最终调用hasNextService方法,首次进入流程如下:
- 加载接口文件
- 逐行解析,校验接口名
- 将解析出来的接口实现类名存入names接口
// 下一个迭代的实现类名称
String nextName = null;
// 资源路径集合
Enumeration<URL> configs = null;
// 文件查找路径
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
// 首次进入这里为null
if (nextName != null) {
return true;
}
// 首次进入这里为null
if (configs == null) {
try {
// 这里就是resources目录创建方式的原因
String fullName = PREFIX + service.getName();
// 这里前文已设置过,一般不为null
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 Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
// 注意这里的格式是utf-8
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
// 逐行解析数据
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
....
}
// 返回实现类名集合迭代器
return names.iterator();
}
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
// 解析结束
if (ln == null) {
return -1;
}
// 这里说明文件可以添加# 这注释
int ci = ln.indexOf('#');
// 截取到#之前的字符串
if (ci >= 0) ln = ln.substring(0, ci);
// 去除首尾空格
ln = ln.trim();
int n = ln.length();
if (n != 0) {
// 校验字符串内部是否包含空格或者换行符
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
// 校验字符是否符合java字符标准
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
// 依次校验每个字符
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
// 添加解析的实现类名
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
4.获取迭代对象
UserService userService = iterator.next();
next()最终调用nextService方法
private S nextService() {
// 之前没有调用过hasNextService,进行解析数据,这里会尝试解析文件流
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();
}
四.总结
jdk的SPI虽然实现了解耦接口的目的,但还是有一个小缺陷:
面对多个实现时,不能指定具体的实现。
最后附上项目地址。