从实现到原理,聊聊Java中的SPI动态扩展 下

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 从实现到原理,聊聊Java中的SPI动态扩展 下

4、服务发现

现在两个服务提供方都实现了接口,下面关键的一步就是服务发现,这一步 java 中的 spi 发现机制已经帮我们实现好了。

创建一个新项目aircondition-app,引入上面打好的两个 jar 包。

<dependencies>
    <dependency>
        <groupId>com.cn.hydra</groupId>
        <artifactId>aircondition-hanging-type</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.cn.hydra</groupId>
        <artifactId>aircondition-vertical-type</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

按照上面的说法,虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法。

下面,就是关键的服务发现环节,我们写一个方法,根据型号去调用对应空调的开关方法。

public class AirconditionApp {
    public static void main(String[] args) {
        new AirconditionApp().turnOn("VerticalType");
    }
    public void turnOn(String type){
        ServiceLoader<IAircondition> load = ServiceLoader
                .load(IAircondition.class);
        for (IAircondition iAircondition : load) {
            System.out.println("检测到:"+iAircondition.getClass().getSimpleName());
            if (type.equals(iAircondition.getType())){
                iAircondition.turnOnOff();
            }
        }
    }
}

测试结果:

可以看到,测试过程中,通过定义的接口IAircondition发现了两个实现类,并通过参数,调用了特定实现类的某个方法。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用。

5、原理

了解了 spi 的工作流程,我们再来看看它的实现,其实最关键的就是上面代码中出现的ServiceLoader这个类。

上面的示例代码中,对于ServiceLoaderload()方法的结果,我们用for循环进行了遍历,这一点我们看一下源码就能明白,因为ServiceLoader实现了Iterable这一接口,而整个服务发现的核心,就在它的iterator()方法中。

注意这里面有两个关键的东西,找一下在源码中定义的地方:

image.png

注释写的非常明白,providers就是一个缓存,在迭代器中如果先从这里面进行查找,如果里面有就继续往下找,没有了的话就用这个懒加载的lookupIterator查找。

那么就简单了,接着往下看LazyIterator,看看它里面的hasNext()next()两个方法是怎么实现的。

这个acc是一个安全管理器,在前面通过System.getSecurityManager()判断并赋值,debug 看一下这里都是null,所以直接看hasNextService()nextService()方法就可以了。

hasNextService()方法中,会取出接口取出实现类的类名放到nextName中:

接下来,在nextService()方法中,则会先加载这个实现类,然后实例化对象,最终放入缓存中去。

在迭代器的迭代过程中,会完成所有实现类的实例化,其实归根结底,还是基于 java 反射去实现的。

6、应用

要说 spi 的实际应用,大家最常见的应该就是日志框架slf4j了,它利用 spi 实现了插槽式接入其他具体的日志框架。

说白了,slf4j本身就是个日志门面,并不提供具体的实现,需要绑定其他具体实现才能真正的引入日志功能。

例如我们可使用log4j2作为具体的绑定器,只需要在 pom 中引入slf4j-log4j12,就可以使用具体功能。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.3</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.3</version>
</dependency>

引入项目后,点开它的 jar 包看一下具体结构:

有没有发现一个彩蛋,先说为什么我们 pom 中引入的明明是slf4j-log4j12,实际上引入的是slf4j-reload4j?翻一下官网的文档:

大意就是在 2015 年和 2022 年,log4j1.x就已经宣布end of life终止了,原因也不难猜,估计是因为频繁爆出的漏洞。在那之后,slf4j-log4j在构建阶段就会自动重定向到slf4j-reload4j了,并且官方也强烈建议使用slf4j-reload4j作为替代。

再回头看一下 jar 包的META-INF.services里面,通过 spi 注入了Reload4jServiceProvider这个实现类,它实现了SLF4JServiceProvider这一接口,在它的初始化方法initialize()中,会完成初始化等工作,后续可以继续获取到LoggerFactoryLogger等具体日志对象。

7、总结

Java 中的 SPI 提供了一种比较特别的服务发现和调用机制,通过接口灵活的将服务调用与服务提供者分离,用于提供给第三方实现扩展时还是很方便的。但是也有缺点,比方说一旦加载一个接口,就会把所有实现类都加载进来,可能会加载到不需要的冗余服务。不过站在整体角度上,还是给我们提供了一种非常不错的框架扩展、集成的思路。



相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
15天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
26 3
|
15天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
46 2
|
2月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
2月前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
2月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
2月前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
72 2
|
2月前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
64 1
|
Java C++
详解JAVA中的 i++ 和 ++i ,案例及原理,通俗易懂
i++和++i是日常开发中,经常使用的语句形式,也是面试中经常见到的一个知识点。但是你真的理解其中的原理吗?
956 0
|
2天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
34 17
|
13天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者