对于Java中的Service类和SPI机制的透彻理解,也算是对Java类加载模型的掌握的不错的一个反映。

了解一个不太熟悉的类,那么从使用案例出发,读懂源代码以及代码内部执行逻辑是一个不错的学习方式。


一、使用案例

通常情况下,使用ServiceLoader来实现SPI机制。 SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。

SPI机制可以归纳为如下的图:

起始这样说起来还是比较抽象,那么下面举一个具体的例子,案例为JDBC的调用例子:

案例如下:

JDBC中的接口即为:java.sql.Driver

SPI机制的实现核心类为:java.util.ServiceLoader

Provider则为:com.mysql.jdbc.Driver

外层调用则是我们进行增删改查JDBC操作所在的代码块,但是对于那些现在还没有学过JDBC的小伙伴来说(不难学~),这可能会有点难理理解,所以我这里就举一个使用案例:

按照上图的SPI执行逻辑,我们需要写一个接口、至少一个接口的实现类、以及外层调用的测试类。

但是要求以这样的目录书结构来定义项目文件,否则SPI机制无法实现(类加载机制相关,之后会讲):

E:.│  MyTest.java│├─com│  └─fisherman│      └─spi│          │  HelloInterface.java│          ││          └─impl│                  HelloJava.java│                  HelloWorld.java│└─META-INF    └─services            com.fisherman.spi.HelloInterface123456789101112131415

其中:

  1. MyTest.java为测试java文件,负责外层调用;
  2. HelloInterface.java为接口文件,等待其他类将其实现;
  3. HelloJava.java 以及 HelloWorld.java 为接口的实现类;
  4. META-INF
    └─services
    com.fisherman.spi.HelloInterface 为配置文件,负责类加载过程中的路径值。

首先给出接口的逻辑:

public interface HelloInterface {    void sayHello();}123

其次,两个实现类的代码:

public class HelloJava implements HelloInterface {    @Override    public void sayHello() {        System.out.println("HelloJava.");    }}123456
public class HelloWorld implements HelloInterface {    @Override    public void sayHello() {        System.out.println("HelloWorld.");    }}123456

然后,配置文件:com.fisherman.spi.HelloInterface

com.fisherman.spi.impl.HelloWorldcom.fisherman.spi.impl.HelloJava12

最后测试文件:

public class MyTest26 {    public static void main(String[] args) {        ServiceLoader loaders = ServiceLoader.load(HelloInterface.class);                for (HelloInterface in : loaders) {            in.sayHello();        }            }}12345678910111213

测试文件运行后的控制台输出:

HelloWorld.HelloJava.12

我们从控制台的打印信息可知我们成功地实现了SPI机制,通过 ServiceLoader 类实现了等待实现的接口和实现其接口的类之间的联系。

下面我们来深入探讨以下,SPI机制的内部实现逻辑。


二、ServiceLoader类的内部实现逻辑

Service类的构造方法是私有的,所以我们只能通过掉用静态方法的方式来返回一个ServiceLoader的实例:

方法的参数为被实现结构的Class对象。

ServiceLoader loaders = ServiceLoader.load(HelloInterface.class); 1

其内部实现逻辑如所示,不妨按调用步骤来分步讲述:

1.上述load方法的源代码:

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

完成的工作:

  1. 得到当前线程的上下文加载器,用于后续加载实现了接口的类
  2. 调用另一个load方法的重载版本(多了一个类加载器的引用参数)

2.被调用的另一个load重载方法的源代码:

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

完成的工作:

  • 调用了类ServiceLoader的私有构造器

3.私有构造器的源代码:

private ServiceLoader(Class 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();}123456

完成的工作:

  1. 空指针和安全性的一些判断以及处理;
  2. 并对两个重要要的私有实例变量进行了赋值:private final Class service; private final ClassLoader loader; 12
  3. reload()方法来迭代器的清空并重新赋值

SercviceLoader的初始化跑完如上代码就结束了。但是实际上联系待实现接口和实现接口的类之间的关系并不只是在构造ServiceLoader类的过程中完成的,而是在迭代器的方法hasNext()中实现的。

这个联系通过动态调用的方式实现,其代码分析就见下一节吧:


三、动态调用的实现

在使用案例中写的forEach语句内部逻辑就是迭代器,迭代器的重要方法就是hasNext():

ServiceLoader是一个实现了接口Iterable接口的类。

hasNext()方法的源代码:

public boolean hasNext() {    if (acc == null) {        return hasNextService();    } else {        PrivilegedAction action = new PrivilegedAction() {            public Boolean run() { return hasNextService(); }        };        return AccessController.doPrivileged(action, acc);    }}12345678910

抛出复杂的确保安全的操作,可以将上述代码看作就是调用了方法:hasNextService.

hasNextService()方法的源代码:

private boolean hasNextService() {    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) {            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;}123456789101112131415161718192021222324

上述代码中比较重要的代码块是:

String fullName = PREFIX + service.getName();            if (loader == null)                configs = ClassLoader.getSystemResources(fullName);123

此处PREFIX(前缀)是一个常量字符串(用于规定配置文件放置的目录,使用相对路径,说明其上层目录为以项目名为名的文件夹):

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

那么fullName会被赋值为:"META-INF/services/com.fisherman.spi.HelloInterface"

然后调用方法getSystemResources或getResources将fullName参数视作为URL,返回配置文件的URL集合 。

pending = parse(service, configs.nextElement());1

parse方法是凭借 参数1:接口的Class对象 和 参数2:配置文件的URL来解析配置文件,返回值是含有配置文件里面的内容,也就是实现类的全名(包名+类名)字符串的迭代器;

最后调用下面的代码,得到下面要加载的类的完成类路径字符串,相对路径。在使用案例中,此值就可以为:

com.fisherman.spi.impl.HelloWorld和com.fisherman.spi.impl.HelloJava

nextName = pending.next();1

这仅仅是迭代器判断是否还有下一个迭代元素的方法,而获取每轮迭代元素的方法为:nextService()方法。

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)) {        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}123456789101112131415161718192021222324252627

抛出一些负责安全以及处理异常的代码,核心代码为:

1.得到接口实现类的完整类路径字符串:

String cn = nextName;1

2使用loader引用的类加载器来加载cn指向的接口实现类,并返回其Class对象(但是不初始化此类):

c = Class.forName(cn, false, loader);1

3.调用Class对象的newInstance()方法来调用无参构造方法,返回Provider实例:

S p = service.cast(c.newInstance());1
//cast方法只是在null和类型检测通过的情况下进行了简单的强制类型转换public T cast(Object obj) {    if (obj != null && !isInstance(obj))        throw new ClassCastException(cannotCastMsg(obj));    return (T) obj;}123456

4.将Provider实例放置于providers指向的HashMap中:

providers.put(cn, p);1

5.返回provider实例:

return p;1

ServiceLoader类的小总结:

  1. 利用创建ServiceLoader类的线程对象得到上下文类加载器,然后将此加载器用于加载provider类;
  2. 利用反射机制来得到provider的类对象,再通过类对象的newInstance方法得到provider的实例;
  3. ServiceLoader负责provider类加载的过程数据类的动态加载;
  4. provider类的相对路径保存于配置文件中,需要完整的包名,如:com.fisherman.spi.impl.HelloWorld

四、总结与评价

  1. SPI的理念:通过动态加载机制实现面向接口编程,提高了框架和底层实现的分离;
  2. ServiceLoader 类提供的 SPI 实现方法只能通过遍历迭代的方法实现获得Provider的实例对象,如果要注册了多个接口的实现类,那么显得效率不高;
  3. 虽然通过静态方法返回,但是每一次Service.load方法的调用都会产生一个ServiceLoader实例,不属于单例设计模式;
  4. ServiceLoader与ClassLoader是类似的,都可以负责一定的类加载工作,但是前者只是单纯地加载特定的类,即要求实现了Service接口的特定实现类;而后者几乎是可以加载所有Java类;
  5. 对于SPi机制的理解有两个要点:理解动态加载的过程,知道配置文件是如何被利用,最终找到相关路径下的类文件,并加载的;理解 SPI 的设计模式:接口框架 和底层实现代码分离
  6. 之所以将ServiceLoader类内部的迭代器对象称为LazyInterator,是因为在ServiceLoader对象创建完毕时,迭代器内部并没有相关元素引用,只有真正迭代的时候,才会去解析、加载、最终返回相关类(迭代的元素);

spi 动态加载、卸载_理解 ServiceLoader类与SPI机制相关推荐

  1. C#中动态加载卸载类库

    网上现有很多的文章是介绍怎样开发插件化的框架的,大部分无非是用Assembly.load等方法,动态加载类库,但这种方法有个缺点,就是没有办法卸载,因为net中就没有提供卸载assembly的方法,还 ...

  2. Java实现动态加载页面_[Java教程]动态加载页面数据的小工具 javascript + jQuery (持续更新)...

    [Java教程]动态加载页面数据的小工具 javascript + jQuery (持续更新) 0 2014-05-07 18:00:06 使用该控件,可以根据url,参数,加载html记录模板(包含 ...

  3. select weui 动态加载数据_浪尖以案例聊聊spark3的动态分区裁剪

    动态分区裁剪,其实就牵涉到谓词下推,希望在读本文之前,你已经掌握了什么叫做谓词下推执行. SparkSql 中外连接查询中的谓词下推规则 动态分区裁剪比谓词下推更复杂点,因为他会整合维表的过滤条件,生 ...

  4. python爬虫动态加载页面_如何爬动态加载的页面?ajax爬虫你有必要掌握

    通过前面几期Python爬虫的文章,不少童鞋已经可以随心所欲的爬取自己想要的数据,就算是一些页面很难分析,也可以用之前介绍的终极技能之「Selenium」+「Webdriver」解决相关问题,但无奈这 ...

  5. SPI动态加载配置文件

  6. 【Linux 内核】宏内核与微内核架构 ( 操作系统需要满足的要素 | 宏内核 | 微内核 | Linux 内核动态加载机制 )

    文章目录 一.操作系统需要满足的要素 二.宏内核 三.微内核 四.Linux 内核动态加载机制 一.操作系统需要满足的要素 电脑上运行的 操作系统 , 是一个 软件 ; 设备管理 : 操作系统需要 为 ...

  7. 关于c#中 的动态加载程序集

    最近在写一个解析分析程序,需要动态加载卸载程序集(其实就是一个简单的插件框架),我的 思路是在主程序的目录下,创建一个assemblis目录,用来存放插件目录,如果加载插件时将其复制到 此目录,然后主 ...

  8. Unity3d(UE4)动态加载osgb倾斜摄影数据

    在Unity3D平台动态加载调度倾斜摄影数据,利用多线程动态加载瓦片数据,可以顺畅加载海量的瓦片数据.目前测试可流畅加载200G左右数据,支持加载本地数据,数据可不放在Unity工程内,也可以将数据放 ...

  9. BPL 和动态加载包

    BPL 是一种特别的 DLL: 使用 Delphi 开发程序,可以把一个大程序的一部分,独立编译成一个 BPL,一个 Delphi 里面称作 Package 的东西,这里中文我们称作[包],然后让 E ...

最新文章

  1. Bert代码详解(二)重点
  2. mysql中group concat_mysql中group_concat()函数的使用方法总结
  3. 漫画:什么是二分查找
  4. 如何解决java乱码_java如何解决乱码
  5. 大型分布式C++框架《四:netio之请求包中转站 上》
  6. VTK:PolyData之PointSource
  7. 一个罐子统治一切:Apache TomEE + Shrinkwrap == JavaEE引导
  8. windows2000 ,windowsXP和windows2003共享页面文件
  9. VC++6.0如何创建与调用动态链接库(dll)
  10. jsp是在html里面嵌入哪种代码?_再说嵌入式入门
  11. 【Kafka】kafka消费组查看lag
  12. kernel headers
  13. 为什么我们拒绝使用 Docker
  14. 本周小结!(二叉树系列三)
  15. 神经网络的归一化(batch normalization)
  16. IDEA插件记录与使用
  17. 查找表字段-事务码 AUT10
  18. 智力题:13 个球一个天平,现知道只有一个和其它的重量不同,问怎样称才能用三次就找到那个球?
  19. vs2008 下配置 opencv2.0 的总结,以及 vc6 下配置 opencv1.0 的转帖
  20. 加拿大Introspect I3C 协议分析仪(Analyzer)及训练器(Exerciser)

热门文章

  1. 大爷与支付宝同名,曾想状告阿里巴巴侵权,现在过得怎么样
  2. 爱因斯坦鲜为人知的另一面
  3. 空难生还几率这么低,飞机上为什么不配备降落伞???
  4. 20个科学小知识,带你走进科学世界
  5. 6大设计原则之接口隔离原则
  6. ns2相关学习——TCL脚本编写(3)
  7. java实现考勤机信息同步
  8. 从框架源码中学习创建型设计模式
  9. ElasticSearch聚合查询
  10. 当爬虫遇到需要动态ip才能获取资源的时候如何解决?