JAVA SPI源码
上一篇:Java SPI
由上面的案例我们知道JAVA SPI的入口是ServiceLoader.load(),我们直接ctrl+左键进入源码查看:
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程的classLoader,大部分情况类似getClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
跟进ServiceLoader.load方法:
//为给定的服务类型和类加载器创建一个新的服务加载器
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
再跟进创建出来的服务加载器ServiceLoader
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果svc的class对象为空,则报空指针异常,报错信息为Service interface cannot be null
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//判断类加载器cl是否为空,为空则使用系统的类加载器SystemClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
跟进最后的方法reload方法
public void reload() {
//清除此加载程序的提供程序缓存,以便重新加载所有提供程序。
providers.clear();
//创建LazyIterator,是Iterator的一个实现类,也是迭代器
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator,是Iterator的一个实现类,也是迭代器,下面我们看一下它做了什么:
acc:创建 ServiceLoader 时获取的访问控制上下文
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);
}
}
可以看出不论acc这个AccessControlContext快照做了什么,都会去执行hasNextService。
(最重要!)跟进查看hasNextService方法:这里的hasNextService就是会去获取每个方法的实现类,获取不到则返回false
private static final String PREFIX = "META-INF/services/"; //约定的目录
private boolean hasNextService() {
//如果下一个不为空则直接返回真
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//得到文件位置,即META-INF/services/cn.zsp.spi.Car
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()) {
//校验配置文件是否还有其他元素,没有就返回false,防止无限循环
return false;
}
pending = parse(service, configs.nextElement());//解析内容成配置文件
}
nextName = pending.next();
return true;
}
hasNextService这个方法主要执行了:配置文件路径(fullName)--->根据路径读取配置文件(configs)--->遍历文件内容--->将文件内容解析成配置文件。
这里有一块较难的parse方法,最后会返回迭代器,我深入讲一下,不想看的直接看下一个黑点标记即可:
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();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
//初始化标记,用于解析流
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}前面就是读取流,没什么讲的,有意思的是parseLine这个方法,这是SPI自己定义的:
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('#');
//注意,此时ln成为了主要语句,类似于:
//server.port=80#端口号是80
//被截断成为server.port=80
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);//获取开头字符编码,比如a就是97
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);//每次都去获取遍历字符的编码
//如果cp是字符,且它不是“.”这个字符,例如 spring.DataSource.url=xxxxx
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;
}总结:这两个方法的目的就是,使用流遍历配置文件,将每一行读取出来放入方法集中,最后返回方法集的迭代器。
到了方法的最后使用了next,使用完毕后才走了true判断有配置文件,进入看一下next方法,这个next方法是SPI自定义的:
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);
}
}
可以发现next方法到最后都会调用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)) {
//如果service是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
}
最终,我们终于看到了providers.put(cn, p)这一步,它将实现类名和实现的类放入了缓存。至此,JAVA SPI的所有源码我们看完了。比起大多数源码来说,相对简单。
总结:
约定一个目录:META-INF/services/
根据接口名找到对应的文件
文件解析得到实现类的全限定名
循环加载实现类和创建实例
整体流程图





