百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

理解 ServiceLoader类与SPI机制 @service原理

liebian365 2024-11-09 13:48 31 浏览 0 评论

对于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.HelloInterface
123456789101112131415

其中:

  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.HelloWorld
com.fisherman.spi.impl.HelloJava
12

最后测试文件:

public class MyTest26 {

    public static void main(String[] args) {

        ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface.class);
        
        for (HelloInterface in : loaders) {
            in.sayHello();
        }
        
    }

}
12345678910111213

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

HelloWorld.
HelloJava.
12

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

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


二、ServiceLoader类的内部实现逻辑

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

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

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

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

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

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

完成的工作:

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

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

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

123456

完成的工作:

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

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

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

完成的工作:

  1. 空指针和安全性的一些判断以及处理;
  2. 并对两个重要要的私有实例变量进行了赋值:private final Class<S> 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<Boolean> action = new PrivilegedAction<Boolean>() {
            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对象创建完毕时,迭代器内部并没有相关元素引用,只有真正迭代的时候,才会去解析、加载、最终返回相关类(迭代的元素);

相关推荐

“版本末期”了?下周平衡补丁!国服最强5套牌!上分首选

明天,酒馆战棋就将迎来大更新,也聊了很多天战棋相关的内容了,趁此机会,给兄弟们穿插一篇构筑模式的卡组推荐!老规矩,我们先来看10职业胜率。目前10职业胜率排名与一周前基本类似,没有太多的变化。平衡补丁...

VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符&quot;

首先,程序中头文件的选择,要选择头文件,在文件中是没有对M_PI的定义的。选择:项目——>”XXX属性"——>配置属性——>C/C++——>预处理器——>预处理器定义,...

东营交警实名曝光一批酒驾人员名单 88人受处罚

齐鲁网·闪电新闻5月24日讯酒后驾驶是对自己和他人生命安全极不负责的行为,为守护大家的平安出行路,东营交警一直将酒驾作为重点打击对象。5月23日,东营交警公布最新一批饮酒、醉酒名单。对以下驾驶人醉酒...

Qt界面——搭配QCustomPlot(qt platform)

这是我第一个使用QCustomPlot控件的上位机,通过串口精确的5ms发送一次数据,再将读取的数据绘制到图表中。界面方面,尝试卡片式设计,外加QSS简单的配了个色。QCustomPlot官网:Qt...

大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写

老友相聚,仗剑江湖!《大话西游2》2021全民PK季4月激燃打响,各PK玩法鏖战齐开,零门槛参与热情高涨。PK季期间,不仅各种玩法奖励丰厚,参与PK趣闻录活动,投稿自己在PK季遇到的趣事,还有机会带走...

测试谷歌VS Code AI 编程插件 Gemini Code Assist

用ClaudeSonnet3.7的天气测试编码,让谷歌VSCodeAI编程插件GeminiCodeAssist自动编程。生成的文件在浏览器中的效果如下:(附源代码)VSCode...

顾爷想知道第4.5期 国服便利性到底需优化啥?

前段时间DNF国服推出了名为“阿拉德B计划”的系列改版计划,截至目前我们已经看到了两项实装。不过关于便利性上,国服似乎还有很多路要走。自从顾爷回归DNF以来,几乎每天都在跟我抱怨关于DNF里面各种各样...

掌握Visual Studio项目配置【基础篇】

1.前言VisualStudio是Windows上最常用的C++集成开发环境之一,简称VS。VS功能十分强大,对应的,其配置系统较为复杂。不管是对于初学者还是有一定开发经验的开发者来说,捋清楚VS...

还嫌LED驱动设计套路深?那就来看看这篇文章吧

随着LED在各个领域的不同应用需求,LED驱动电路也在不断进步和发展。本文从LED的特性入手,推导出适合LED的电源驱动类型,再进一步介绍各类LED驱动设计。设计必读:LED四个关键特性特性一:非线...

Visual Studio Community 2022(VS2022)安装图文方法

直接上步骤:1,首先可以下载安装一个VisualStudio安装器,叫做VisualStudioinstaller。这个安装文件很小,很快就安装完成了。2,打开VisualStudioins...

Qt添加MSVC构建套件的方法(qt添加c++11)

前言有些时候,在Windows下因为某些需求需要使用MSVC编译器对程序进行编译,假设我们安装Qt的时候又只是安装了MingW构建套件,那么此时我们该如何给现有的Qt添加一个MSVC构建套件呢?本文以...

Qt为什么站稳c++GUI的top1(qt c)

为什么现在QT越来越成为c++界面编程的第一选择,从事QT编程多年,在这之前做C++界面都是基于MFC。当时为什么会从MFC转到QT?主要原因是MFC开发界面想做得好看一些十分困难,引用第三方基于MF...

qt开发IDE应该选择VS还是qt creator

如果一个公司选择了qt来开发自己的产品,在面临IDE的选择时会出现vs或者qtcreator,选择qt的IDE需要结合产品需求、部署平台、项目定位、程序猿本身和公司战略,因为大的软件产品需要明确IDE...

Qt 5.14.2超详细安装教程,不会来打我

Qt简介Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开库,主要用来开发图形用户界面(GraphicalUserInterface,GUI)程序。Qt是纯C++开...

Cygwin配置与使用(四)——VI字体和颜色的配置

简介:VI的操作模式,基本上VI可以分为三种状态,分别是命令模式(commandmode)、插入模式(Insertmode)和底行模式(lastlinemode),各模式的功能区分如下:1)...

取消回复欢迎 发表评论: