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

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

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


一、使用案例

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

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

86efc34d5ba0e66970159de60fb6d2cb.png


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

案例如下:

ad01d72b34d727beb0e0c9f7cb8e6632.png


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对象创建完毕时,迭代器内部并没有相关元素引用,只有真正迭代的时候,才会去解析、加载、最终返回相关类(迭代的元素);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/513618.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探秘RocketMQ源码——Series1:Producer视角看事务消息

简介&#xff1a; 探秘RocketMQ源码——Series1&#xff1a;Producer视角看事务消息1. 前言 Apache RocketMQ作为广为人知的开源消息中间件&#xff0c;诞生于阿里巴巴&#xff0c;于2016年捐赠给了Apache。从RocketMQ 4.0到如今最新的v4.7.1&#xff0c;不论是在阿里巴巴内部还…

三大院士、十大数据库掌门人,岳麓对话开启数字经济新时代!

10月23日&#xff0c;第二届“长沙 中国1024程序员节”在湖南长沙盛大开幕。大会以“开源开放、算据赋能——开启数字经济新时代”为主题&#xff0c;囊括岳麓尖峰对话、2021技术英雄大会、18场专业主题论坛/峰会&#xff1b;50企业创新展&#xff0c;联动100海内外高校&#…

java 队列_百战程序员:Java并发阻塞队列

阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构&#xff0c;BlockingQueue提供了线程安全的队列访问方式&#xff1a;当阻塞队列进行插入数据时&#xff0c;如果队列已满&#xff0c;线程将会阻塞等待直到队列非满&#xff1b;从阻塞队列取数据时&#xff…

select事件有哪些_Android 深入底层:Linux事件管理机制 epoll

在linux 没有实现epoll事件驱动机制之前&#xff0c;我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在linux新的内核中&#xff0c;有了一种替换它的机制&#xff0c;就是epoll。select()和poll() IO多路复用模型select的缺点&#xff1a;单个进程能够监…

如何从 0 到 1 开发 PyFlink API 作业

简介&#xff1a; 以 Flink 1.12 为例&#xff0c;介绍如何使用 Python 语言&#xff0c;通过 PyFlink API 来开发 Flink 作业。 Apache Flink 作为当前最流行的流批统一的计算引擎&#xff0c;在实时 ETL、事件处理、数据分析、CEP、实时机器学习等领域都有着广泛的应用。从 F…

殷浩详解DDD:如何避免写流水账代码?

简介&#xff1a; 在日常工作中我观察到&#xff0c;面对老系统重构和迁移场景&#xff0c;有大量代码属于流水账代码&#xff0c;通常能看到开发在对外的API接口里直接写业务逻辑代码&#xff0c;或者在一个服务里大量的堆接口&#xff0c;导致业务逻辑实际无法收敛&#xff0…

重度使用Flutter研发模式下的页面性能优化实践

简介&#xff1a; 淘宝特价版是集团内应用Flutter技术场景比较多&#xff0c;且用户量一亿人以上的应用了。目前我们首页、详情、店铺、我的&#xff0c;看看短视频&#xff0c;及评价&#xff0c;设置等二级页面都在用Flutter技术搭建。一旦Flutter有性能瓶颈&#xff0c;重度…

蚂蚁构建服务演进史

简介&#xff1a; 自动化构建和CI/CD往往是相辅相成的&#xff0c;可以理解为&#xff0c;自动化构建是温饱问题&#xff0c;解决了温饱就会有更多的提高生产力的诉求&#xff0c;也就是对应的CI平台&#xff0c;CI/CD本篇文章不做扩展。 作者 | 琉克 来源 | 阿里技术公众号 一…

这个云原生开发的痛点你遇到了吗?

简介&#xff1a; 上云从来都不是一片坦途&#xff0c;在此过程中我们总会遇到一些困难和挑战&#xff0c;得益于云原生技术的日益成熟&#xff0c;这些问题一定会有相应的解法。 作者&#xff1a;纳海 背景 在云原生时代&#xff0c;国内外众多云厂商释放出强大的技术红利…

mysql安装pymyaql_python安装mysql的依赖包mysql-python操作

一般情况下&#xff0c;使用pip命令安装即可&#xff1a;[rootdthost27 ~]# pip install mysql-python但是在实际工作环境中&#xff0c;往往会安装失败&#xff0c;这是因为系统缺少mysql的相关依赖组件。所以必须先安装mysql-devel类的包&#xff0c;而且必须要对应好mysql客…

「技术人生」专题第1篇:什么是技术一号位?

前言 什么是技术一号位、有哪些关注点、怎么做技术一号位&#xff1f; 做了研发团队的技术 leader 以后&#xff0c;要处理的事情非常多&#xff0c;如果对自己扮演的角色没有一个清晰的认知&#xff0c;就会出现该做的事情没有做&#xff0c;不该做的事情投入了过多的精力&…

服务器之后加码存储,浪潮信息重磅发布新一代 G6 存储平台

作者 | 宋慧 出品 | CSDN云计算 提到浪潮&#xff0c;业界首先想到的是浪潮信息服务器占有的优势和市场份额。不过&#xff0c;其实浪潮在存储领域也持续深耕和发力中。据国际分析机构 Gartner 报告显示&#xff0c;2021 年第一季度&#xff0c;浪潮存储在全闪存存储、分布式存…

技术干货 | 轻松两步完成向 mPaaS 小程序传递启动参数

简介&#xff1a; 以传递 name 和 pwd 参数为例&#xff0c;分别介绍此场景在 Android 小程序和 iOS 小程序中的实现过程。 前言 在部分场景下&#xff0c;需要向小程序的默认接收页&#xff08;pages/index/index&#xff09;传递参数。 本文将以传递 name 和 pwd 参数为例&…

腾讯安全发布安全托管服务MSS,推动网络安全建设向服务驱动转变

近年来&#xff0c;随着黑产组织逐渐规模化、产业化&#xff0c;网络攻击态势愈发严峻&#xff1b;同时&#xff0c;由于DevOps、云原生等新技术的落地&#xff0c;以及IT架构的变化&#xff0c;企业研发和运营的模型随之改变&#xff0c;风险应对策略也越发复杂&#xff0c;越…

深入解读:获得 2021 Forrester 全球云数仓卓越表现者的阿里云数据仓库

简介&#xff1a; 阿里云在最新发布的 The Forrester Wave™: Cloud Data Warehouse, Q1 2021 全球云数据仓库技术评比中进入卓越表现者象限&#xff0c;成为国内唯一入选厂商。本文针对 Forrester 的报告&#xff0c;结合阿里云的以 MaxCompute 为核心的云数仓产品&#xff0c…

Azkaban业务流程如何转化为DataWorks业务流程

简介&#xff1a; 用户在迁移上云的时候&#xff0c;需要将云下的的Azkaban任务迁移上云&#xff0c;之前通过用户在DataWroks一步步创建对应的业务流程&#xff0c;其转化难度和转化时间都是一定的成本和时间&#xff0c;但如何能做到省时省力的方式迁移,为此本文提供了使用迁…

深度 | 数据湖分析算力隔离技术剖析

简介&#xff1a; 随着越来越多的企业开始做数据湖分析&#xff0c;数据量的持续增加&#xff0c;数据分析需求也会越来越多&#xff0c;在一个共享的数据湖分析引擎&#xff0c;如何防止多租户之间的查询相互影响是一个很通用的问题&#xff0c;本文以阿里云DLA Presto为例&am…

mfc链表中的数据如何排序输出_java程序员面试中最容易被问到的18个算法题(附答案!)...

算法是比较复杂又基础的学科&#xff0c;每个学编程的人都会学习大量的算法。而根据统计&#xff0c;以下这18个问题是面试中最容易遇到的&#xff0c;本文给出了一些基本答案&#xff0c;供算法方向工程师或对此感兴趣的程序员参考。1&#xff09;请简单解释算法是什么&#x…

深度解析PolarDB数据库并行查询技术

简介&#xff1a; 随着数据规模的不断扩大&#xff0c;用户SQL的执行时间越来越长&#xff0c;这不仅对数据库的优化能力提出更高的要求&#xff0c;并且对数据库的执行模式也提出了新的挑战。本文将介绍基于代价进行并行优化、并行执行的云数据库的并行查询引擎的关键问题和核…

万物互联、应用现代化、云原生新范式,华为云为数字化转型提供最优解

10月27日&#xff0c;华为云TechWave全球技术峰会&#xff08;应用现代化&#xff09;在广州举办。华为云发布“云原生2.0”新范式&#xff0c;并分享应用现代化、万物互联等最新理念及产品进展。 华为云CTO张宇昕发表主题演讲 华为云CTO张宇昕表示&#xff1a;“云原生新范式…