您的位置:首页 > 编程语言 > Java开发

jdk的ServiceLoader

2018-01-07 02:22 211 查看
ServiceLoader是jdk提供动态加载类的一种方式。可以使得用户能够在运行时动态解析目标文件夹下接口配置文件来动态加载相关类使得直接可以在运行时直接保证相关类的加载

在jdk的nio包下SelectorProvider给出来相应的使用方式。

private static boolean loadProviderAsService() {

ServiceLoader<SelectorProvider> sl =
ServiceLoader.load(SelectorProvider.class,
ClassLoader.getSystemClassLoader());
Iterator<SelectorProvider> i = sl.iterator();
for (;;) {
try {
if (!i.hasNext())
return false;
provider = i.next();
return true;
} catch (ServiceConfigurationError sce) {
if (sce.getCause() instanceof SecurityException) {
// Ignore the security exception, try the next provider
continue;
}
throw sce;
}
}
}

在这个方法中selectorProvicer可以在运行时动态加载目标目录下实现了SelectorProvider接口的SelectorProvider,并选取第一个作为运行时的SelectorProvider用来提供Selector以便nio的开发。

可以看到第一步只是简单调用了load()静态方法,将目标接口以及当前的系统类加载器。

可以直接看load()方法。

public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}

public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}

首先load()方法只是简单的将之前目标类和类加载器作为参数简单调用了ServiceLoader的构造方法,在构造方法中,只是将相应的参数存放,然后调用了reload()方法。首先会把当前用来缓存加载过的类作为LinkedHashMap的provider清空,然后通过构造方法生成一个lazyIterator。lazyIterator作为ServiceLoader的内部类,构造方法也是只需要两个参数,目标类和类加载器,同时类如其名,lazyIterator的构造方法也只是简单的将目标参数,真正的逻辑操作在调用时才开始。

在通过load()完成了ServiceLoader的构造方法并返回之后,SelectorProvider调用了其iterator()方法。

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();
}

public void remove() {
throw new UnsupportedOperationException();
}

};
}

其iterator()方法只是返回了一个新的Iterator()类,并且重写了hasNext()和next()和remove()方法。这里也并没有任何逻辑,而之后SelectorProvider立即调用了其hasNext()方法,lazyIterator也类如其名的完成了延迟加载类的使命。

那么可以看到,hasNext()方法也就是类真正开始加载的号角,只要在真正要用到相关类的时候,才会真的开始加载所需要用到的类。

首先,会从knownProviders中选择,可以看到,这个类是在Iterator初始化的时候给创建的,是直接取得了providers的迭代器,但是由于在一开始ServiceLoader的构造方法中已经将其clear了,那么显然此时的knownProviders已经没有任何可以取得的类了,那么导致会调用lazyIterator的hasNext()。

public boolean hasNext() {
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) {

4000
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;
}

在hasNext()方法中,会首先判断是否已经存在过nextName,也就是下一个要加载类名,当然此时,并不存在,所以方法会往下走。Config是一个url的迭代器,此时想必也是不存在的,那么会构造目标文件名fullName。可以看到fullName在这里的全名是/src/META-INF/services加上目标类名,这个就是lazyIterator所要解析的目标配置文件。Pending作为字符串的迭代器来保存已经取得的类名,取得文件后,如果pending还没有任何数据就会调用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();
}

这里会获得目标配置文件的BufferReader,以便读取解析,用lc作为当前解析的行号,以保证目标配置文件的每一行被都被顺利解析,name 作为数组来存放已经读取的类名。具体的解析在parseLine()当中。

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);
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;
}

这里首先会逻辑取得文件的下一行,如果已经到了文件最后一行会返回lc为-1,这样在parse()方法中也会使得读取文件行数的循环被打破,标志这配置文件的读取完成。之后保证每行都是一个类名,通过保证不存在空格与/t。然后会逐个字符逐个字符的读取,保证每个字符都符合java规范的,并且每次都会根据字符动态调整读取的跨度,这样保证在这一行中,可以准确无误的读取到符合java规范的类名,读取完毕之后去providers和names数组去保证不会重复加载,只要这样,才会将读取到的类名作为字符串保存在数组中,之后返回lc+1,确保再读取下一行数据。

经过上面的步骤parse()方法已经读取完了相应的配置文件,并已经把相应的类名存放在了字符串迭代器pending中,把pending的next()存放在nextName中,作为下一个要加载的类名,并返回true。

public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}

当SelectorProvider中通过hasNext()得到true时,自然会调用Iterator()的next()方法,在其next()方法中,首先还会去knownProviders中去找,但显然之前的操作并没有对其进行任何操作,所以最后还是会到LazyIterator的next()方法中来。

public S next() {
if (!hasNext()) {
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
}

next()方法的实现就相当简单,降将刚刚的nextName的类名通过Class.forName()加载,然后通过newInstance()得到实例强转成一开始配置的接口类或者需要被继承的类,然后将其通过类名和实例作为键值对存放在作为map的providers中,这样之后若是在又调用到了ServiceLoader的iterator()方法,那么就会将其键值对迭代器赋给knownProvider,以便不用重复同一个类的重复加载。

这样,如果之前配置了相应的配置文件SelectorProviders就可以在这里动态加载相应的provider并获取实例,很方便。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: