ServiceLoader内部实现分析

作者: 疯狂小兵 | 2017-04-22 | 阅读
「编辑」 「本文源码」

背景

对于技术,要知其然更要知其所以然,在上一篇文章简单介绍了ServiceLoader的基本使用,完成了知其然的阶段,本篇要完成知其所以然。完成对其基本实现原理的分析。

原理分析

ServiceLoader类是final类型,并且实现了Iterable接口,使得该类可以被迭代,不能被继承。

//关键属性

private static final String PREFIX = "META-INF/services/";

// The class or interface representing the service being loaded
private final Class<S> service;

// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// Cached providers, in instantiation order
//会缓存已经实例化的实现类,并且会根据实例化顺序排序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
//该属性是懒加载实现类的迭代器
private LazyIterator lookupIterator;

接口load(Class<S> service)load(Class<S> service,ClassLoader loader)都会直接new一个新的ServiceLoader类,在实例化时会调用reload方法实例化LazyIterator迭代类。

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();
    lookupIterator = new LazyIterator(service, loader);
}

在该接口调用返回后并去查找并加载指定接口的实现类,这是完成查找前的配置工作。接口实现类的查找是在ServiceLoader迭代时通过调用nexthasNext方法去完成。接下来分析ServiceLoader的迭代过程。

对ServiceLoader进行Iterator迭代,在调用hasNext时,会调用lookupIteratorhasNext,并调用hasNextService方法,而该方法是查找实现类全限定名的关键位置

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
           //例如:对于接口cn.followtry.dubbo.UserService,fullName=META-INF/services/cn.followtry.dubbo.UserService,这就是为什么要在META-INF下创建要查询接口全限定名命名的文件了。
            String fullName = PREFIX + service.getName();
            //configs为获取到的文件信息
            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;
}

在这步判断之后,ServiceLoader内部就已经有了接口对应的实现类的全限定名集合,在接下来调用next接口中会调用nextService方法。而该方法内部会调用Class.forNameclazz.newInstance()获取到接口实现类的实例。

nextService实现如下:

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 {
      //将获取到的子类实例化并将其转为需要的类型(接口类型)
        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
}

ServiceLoader缺点分析

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。

  • 获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类

总结

如上所述完成后就分析完了ServiceLoader对接口实现类的查询,其实可以发现核心实现也没有什么难的地方,无非就一下几点:

  • 通过资源文件指定实现了接口的实现类名称;

  • 使用java I/O方式查找该资源文件获取到该文件的信息及其内容;

  • 将资源文件内部的各个实现类的名称存储在缓存中供接下来使用;

  • 使用Class.forName通过类名构造类并对其实例化返回给调用方,即可供调用方使用了。


版权声明:本文由 在 2017年04月22日发表。本文采用CC BY-NC-SA 4.0许可协议,非商业转载请注明出处,不得用于商业目的。
文章题目及链接:《ServiceLoader内部实现分析》




  相关文章:

「游客及非Github用户留言」:

「Github登录用户留言」:

TOP