浅析JDK中ServiceLoader的源码

前提

紧接着上一篇《通过源码浅析JDK中的资源加载》,ServiceLoader是SPI(Service Provider Interface)中的服务类加载的核心类,也就是,这篇文章先介绍ServiceLoader的使用方式,再分析它的源码。

ServiceLoader的使用

这里先列举一个经典的例子,MySQL的Java驱动就是通过ServiceLoader加载的,先引入mysql-connector-java的依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

查看这个依赖的源码包下的META-INF目录,可见:

我们接着查看java.lang.DriverManager,静态代码块里面有:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

其中,可以查看loadInitialDrivers()有如下的代码片段:

java.lang.DriverManager是启动类加载器加载的基础类,但是它可以加载rt.jar包之外的类,上篇文章提到,这里打破了双亲委派模型,原因是:ServiceLoader中使用了线程上下文类加载器去加载类。这里JDBC加载的过程就是典型的SPI的使用,总结规律如下:

  • 1、需要定义一个接口。
  • 2、接口提供商需要实现第1步中的接口。
  • 3、接口提供商在META-INF/services目录下建立一个文本文件,文件名是第1步中定义的接口的全限定类名,文本内容是接口的实现类的全限定类名,每个不同的实现占独立的一行。
  • 4、使用ServiceLoader加载接口类,获取接口的实现的实例迭代器。

举个简单的实例,先定义一个接口和两个实现:

public interface Say {

  void say();
}

public class SayBye implements Say {

    @Override
    public void say() {
        System.out.println("Bye!");
    }
}

public class SayHello implements Say {

    @Override
    public void say() {
        System.out.println("Hello!");
    }
}

接着在项目的META-INF/services中添加文件如下:

最后通过main函数验证:

基于SPI或者说ServiceLoader加载接口实现这种方式也可以广泛使用在相对基础的组件中,因为这是一个成熟的规范。

ServiceLoader源码分析

上面通过一个经典例子和一个实例介绍了ServiceLoader的使用方式,接着我们深入分析ServiceLoader的源码。我们先看ServiceLoader的类签名和属性定义:

public final class ServiceLoader<S> implements Iterable<S>{
    //需要加载的资源的路径的目录,固定是ClassPath下的META-INF/services/
    private static final String PREFIX = "META-INF/services/";
    // ServiceLoader需要正在需要加载的类或者接口
    // The class or interface representing the service being loaded
    private final Class<S> service;
    // ServiceLoader进行类加载的时候使用的类加载器引用
    // 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;
    //基于实例的顺序缓存类的实现实例,其中Key为实现类的全限定类名
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 当前的"懒查找"迭代器,这个是ServiceLoader的核心
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

    //暂时忽略其他代码...
}    

ServiceLoader实现了Iterable接口,这一点提示了等下我们在分析它源码的时候,需要重点分析iterator()方法的实现。ServiceLoader依赖于类加载器实例进行类加载,它的核心属性LazyIterator是就是用来实现iterator()方法的,下文再重点分析。接着,我们分析ServiceLoader的构造函数:

public void reload() {
    //清空缓存
    providers.clear();
    //构造LazyIterator实例
    lookupIterator = new LazyIterator(service, loader);
}

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

ServiceLoader只有一个私有的构造函数,也就是它不能通过构造函数实例化,但是要实例化ServiceLoader必须依赖于它的静态方法调用私有构造去完成实例化操作,而实例化过程主要做了几步:

  • 1、判断传入的接口或者类的Class实例不能为null,否则会抛出异常。
  • 2、如果传入的ClassLoader实例为null,则使用应用类加载器(Application ClassLoader)。
  • 3、实例化访问控制上下文。
  • 4、调用实例方法reload(),清空目标加载类的实现类实例的缓存并且构造LazyIterator实例。

注意一点是实例方法reload()的修饰符是public,也就是可以主动调用去清空目标加载类的实现类实例的缓存和重新构造LazyIterator实例。接着看ServiceLoader提供的静态方法:

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

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
        prev = cl;
        cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
}

上面的三个公共静态方法都是用于构造ServiceLoader实例,其中load(Class<S> service, ClassLoader loader)就是典型的静态工厂方法,直接调用ServiceLoader的私有构造器进行实例化,除了需要指定加载类的目标类型,还需要传入类加载器的实例。load(Class<S> service)实际上也是委托到load(Class<S> service, ClassLoader loader),不过它使用的类加载器指定为线程上下文类加载器,一般情况下,线程上下文类加载器获取到的就是应用类加载器(系统类加载器)。loadInstalled(Class<S> service)方法又看出了"双亲委派模型"的影子,它指定类加载器为最顶层的启动类加载器,最后也是委托到load(Class<S> service, ClassLoader loader)。接着我们需要重点分析ServiceLoader#iterator()

public Iterator<S> iterator() {

    //Iterator的匿名实现
    return new Iterator<S>() {

    //目标类实现类实例缓存的Map的Entry的迭代器实例
    Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();

        //先从缓存中判断是否有下一个实例,否则通过懒加载迭代器LazyIterator去判断是否存在下一个实例
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        //如果缓存中判断是否有下一个实例,如果有则从缓存中的值直接返回
        //否则通过懒加载迭代器LazyIterator获取下一个实例
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        //不支持移除操作,直接抛异常
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

iterator()内部仅仅是Iterator接口的匿名实现,hasNext()next()方法都是优先判断缓存中是否已经存在实现类的实例,如果存在则直接从缓存中返回,否则调用懒加载迭代器LazyIterator的实例去获取,而LazyIterator本身也是一个Iterator接口的实现,它是ServiceLoader的一个私有内部类,源码如下:

private class LazyIteratorimplements Iterator<S>{

        Class<S> service;
        ClassLoader loader;
        //加载的资源的URL集合
        Enumeration<URL> configs = null;
        //所有需要加载的实现类的全限定类名的集合
        Iterator<String> pending = null;
        //下一个需要加载的实现类的全限定类名
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            //如果下一个需要加载的实现类的全限定类名不为null,则说明资源中存在内容
            if (nextName != null) {
                return true;
            }
            //如果加载的资源的URL集合为null则尝试进行加载
            if (configs == null) {
                try {
                    //资源的名称,META-INF/services + '需要加载的类的全限定类名'
                    //这样得到的刚好是需要加载的文件的资源名称
                    String fullName = PREFIX + service.getName();
                    //这里其实ClassLoader实例应该不会为null
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        //从ClassPath加载资源
                        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 S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射构造Class<S>实例
                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 {
                //通过Class#newInstance()进行实例化,并且强制转化为对应的类型的实例
                S p = service.cast(c.newInstance());
                //添加缓存,Key为实现类的全限定类名,Value为实现类的实例
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

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

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

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

    }

LazyIterator也是Iterator接口的实现,它的Lazy特性表明它总是在ServiceLoader的Iterator接口匿名实现iterator()执行hasNext()判断是否有下一个实现或者next()获取下一个实现类的实例的时候才会"懒判断"或者"懒加载"下一个实现类的实例。最后是加载资源文件后对资源文件的解析过程的源码:

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);
            }
        }
        //返回的是ArrayList的迭代器实例
        return names.iterator();
}

//解析资源文件中每一行的内容
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                      List<String> names)throws IOException, ServiceConfigurationError{
        // 下一行没有内容,返回-1,便于上层可以跳出循环
        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) {
            //不能存在空格字符' '和特殊字符'\t'
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            //判断第一个char是否一个合法的Java起始标识符
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            //判断所有其他字符串是否属于合法的Java标识符
            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;
    }

整个资源文件的解析过程并不复杂,主要包括文件内容的字符合法性判断和缓存避免重复加载的判断。

小结

SPI被广泛使用在第三方插件式类库的加载,最常见的如JDBC、JNDI、JCE(Java加密模块扩展)等类库。理解ServiceLoader的工作原理有助于编写扩展性良好的可插拔的类库。

(本文完 c-1-d e-20181014)

原文地址:https://www.cnblogs.com/throwable/p/9788819.html

时间: 2024-10-13 22:45:18

浅析JDK中ServiceLoader的源码的相关文章

java jdk 中HashMap的源码解读

HashMap是我们在日常写代码时最常用到的一个数据结构,它为我们提供key-value形式的数据存储.同时,它的查询,插入效率都非常高. 在之前的排序算法总结里面里,我大致学习了HashMap的实现原理,并制作了一个简化版本的HashMap. 今天,趁着项目的间歇期,我又仔细阅读了Java中的HashMap的实现. HashMap的初始化: Java代码 public HashMap(int initialCapacity, float loadFactor) public HashMap(i

Spring中Bean命名源码分析

Spring中Bean命名源码分析 一.案例代码 首先是demo的整体结构 其次是各个部分的代码,代码本身比较简单,不是我们关注的重点 配置类 /** * @Author Helius * @Create 2019-10-25-20:16 */ @Configuration @ComponentScan(basePackages = {"service"}) public class SpringConfiguration { } 接口的实现类 public interface Use

Scala 深入浅出实战经典 第48讲:Scala类型约束代码实战及其在Spark中的应用源码解析

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-64讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2 技术爱好者尤其是大数据爱好者 可以加DT大数据梦工厂的qq群 DT大数据梦工厂① :462923555 DT大数据梦工厂②:437123764 DT大数据梦工厂③

68:Scala并发编程原生线程Actor、Cass Class下的消息传递和偏函数实战解析及其在Spark中的应用源码解析

今天给大家带来的是王家林老师的scala编程讲座的第68讲:Scala并发编程原生线程Actor.Cass Class下的消息传递和偏函数实战解析 昨天讲了Actor的匿名Actor及消息传递,那么我们今天来看一下原生线程Actor及CassClass下的消息传递,让我们从代码出发: case class Person(name:String,age:Int)//定义cass Class class HelloActor extends Actor{//预定义一个Actor  def act()

探秘Tomcat(一)——Myeclipse中导入Tomcat源码

前言:有的时候自己不知道自己是井底之蛙,这并没有什么可怕的,因为你只要蜷缩在方寸之间的井里,无数次的生活轨迹无非最终归结还是一个圆形:但是可怕的是有一天你不得不从井里跳出来生活,需要重新审视井以外的生活,你就会发现世界如此美好,我知道的如此的少! 好比,但你看到如下代码 namespace Singleton { public class Singleton { private static Singleton singleton; private Singleton() { } public

Scala 深入浅出实战经典 第60讲:Scala中隐式参数实战详解以及在Spark中的应用源码解析

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2土豆:http://www.tudou.com/programs/view/IVN4EuFlmKk/优酷:http://v.youku.com/v_show/id_

在Mac OS X中下载Android源码的一些经验

首先说明,随着近期(2014年6月开始)GFW的升级,这个网站:http://www.android.com/ 已经不能正常访问了,下面的这些操作均是在我连接VPN的时候进行的. 首先,需要做一些准备工作:据这里(http://source.android.com/source/initializing.html)介绍,Mac OS中文件系统的特点是:case-preserving but case-insensitive,也就是保留大小写,但对大小写不敏感,这样会给后续运行一些命令带来很多麻烦

Scala 深入浅出实战经典 第65讲:Scala中隐式转换内幕揭秘、最佳实践及其在Spark中的应用源码解析

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2土豆:http://www.tudou.com/programs/view/NGgUD5FBQaA/优酷:http://v.youku.com/v_show/id_

在eclipse中关联android源码

1打包源码成jar: 1 新建一个java项目 2  import  想打包的源码文件 3 export 这个文件 : 选择java->jar file .  这里会让你选择输出路径 2 添加源码关联: 1 在 项目右键-> properties 中的 java build path -> libraries 中选择 想要关联的 jar包, 选择里面的 source attachment . 2 edit -> 选择刚才打包的jar 文件 3 完成 在eclipse中关联andro