Java SPI的原理和实践

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Java SPI的原理和实践

在Java中,我们经常会提到面向接口编程,这样减少了模块之间的耦合,更加灵活。

在一个项目中我们也通常将接口和实现类放在一起,但是如果哪天我们要替换其它的实现类,或者是修改实现类,涉及到实现类的代码也要相应地修改。

能不能这样:在调用服务的时候,我们只调用接口,不用关心实现类呢?无论我们怎么切换实现类,调用接口的部分代码都能正常运行?

当然是可以的,Java SPI (Service Provider Interface)就提供了这样的机制。

Java SPI机制中,我们不再是手动指定接口和实现类的关系,而是让接口去寻找可用的实现类

事实上,我们经常使用的Spring框架、日志接口等等,都是使用了SPI机制实现了扩展。

1,SPIAPI

在说起SPI之前,我们还是先看一下APIAPI我们已经很熟悉了,和SPI都可以被称作接口

只不过API功能的实现,以及接口的定义全部是接口的实现者提供的,调用者只需要调用接口即可:

image.png

不过SPI就不一样了,在SPI机制中,调用者仍然是调用接口,但是这个接口是独立存在的,并且可以由不同的实现者实现

image.png

也就是说,这里接口只是一个标准,并且提供接口的那一方并不一定回去实现接口,而是根据接口的定义,由更多的第三方实现。

这个接口可以由一个甚至是多个实现者去实现。也因此,调用者在调用接口时,可能还需要指定一下使用哪个实现者的实现类

实现者也叫做 服务提供者

事实上,我们日常生活中经常使用的U盘也很类似SPI机制,U盘使用的是USB接口,USB接口仅仅是一个规范(接口),但是发明USB接口的公司并没有去生产U盘,而是由不同的U盘厂商例如金士顿、闪迪(实现者)等等去根据这个规范生产U盘,然后我们就可以去选择自己喜欢的牌子(选择实现者)购买U盘,不过平时无论使用什么牌子的U盘,我们只需要插入到电脑的USB接口(调用接口)即可使用,而不用关心不同的厂商是怎么实现USB接口的功能的。

可见,SPI机制将实现者和接口再次解耦合了,使得接口更加易于扩展。

事实上,我们常常用的SLF4J就是一个Java的日志接口,但是它也仅仅是一个接口,所以被称作门面。而它的实现有LogbackLog4j等等,并且在切换实现的时候,我们只需要修改一下依赖配置即可,代码并不需要任何变动,因为代码中也仅仅是调用了接口。

2,自己完成一个SPI

那么现在,我们也来以一个最简单的日志接口为例,实现自己的SPI

(1) 定义SPI接口

先新建一个空的Maven项目log-interface,然后在里面创建一个日志接口,声明日志接口具备的方法(功能):

package com.gitee.swsk33.loginterface.spi;

/**
 * 定义日志接口
 */
public interface Logger {

    /**
     * INFO级别日志方法
     *
     * @param message 日志打印消息
     */
    void info(String message);

    /**
     * DEBUG级别日志方法
     *
     * @param message 日志打印消息
     */
    void debug(String message);

}

这样,我们便定义了这么一个日志接口,并声明日志接口需要有infodebug这两个日志功能。

然后就是编写服务类,这个服务类是这里最为重要的地方,它的作用是扫描所有实现了Logger接口的实现类并加载进来,然后供调用者去调用

先看代码:

package com.gitee.swsk33.loginterface.service;

import com.gitee.swsk33.loginterface.spi.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

/**
 * 服务,用于加载所有服务使用者的实现类,以及供外部调用
 * 该类为一个单例
 */
public class LoggerService {

    /**
     * 该类唯一单例
     */
    private static final LoggerService LOGGER = new LoggerService();

    /**
     * 默认的Logger实现类
     */
    private final Logger defaultLogger;

    /**
     * 所有的Logger实现类列表
     */
    private final List<Logger> allLoggers = new ArrayList<>();

    /**
     * 私有化构造器
     */
    private LoggerService() {
        // 加载全部Logger接口的实现类
        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
        // 将实现类放入我们的Logger实现类列表
        for (Logger logger : loader) {
            allLoggers.add(logger);
        }
        // 这里取出第一个作为默认实现类
        if (!allLoggers.isEmpty()) {
            defaultLogger = allLoggers.get(0);
        } else {
            defaultLogger = null;
        }
        System.out.println("加载到" + allLoggers.size() + "个服务实现!");
    }

    /**
     * 获取该服务类的唯一单例
     *
     * @return 该服务类的唯一单例
     */
    public static LoggerService getInstance() {
        return LOGGER;
    }

    /**
     * 调用默认的实现类的info日志打印方法
     *
     * @param message 消息
     */
    public void info(String message) {
        if (defaultLogger == null) {
            System.err.println("没有找到实现了Logger接口的类!");
            return;
        }
        defaultLogger.info(message);
    }

    /**
     * 调用默认的实现类的debug日志打印方法
     *
     * @param message 消息
     */
    public void debug(String message) {
        if (defaultLogger == null) {
            System.err.println("没有找到实现了Logger接口的类!");
            return;
        }
        defaultLogger.debug(message);
    }

}

首先这个类是一个单例的类,在构造器中,我们使用ServiceLoader这个类来将实现了Logger接口的所有类都扫描进来,并存入我们的实现类列表,然后我们取出列表中的第一个作为默认实现。

在下面我们定义了infodebug来完成对接口的默认实现类的调用。

最后,在项目目录下执行mvn install命令将其安装至本地Maven仓库,以便后续服务提供者引入并实现。

(2) 完成一个接口的实现

现在再新建一个空的Maven项目logservice-one,并引入上面接口项目为依赖:

image.png

然后编写实现类:

package com.gitee.swsk33.logserviceone.service;

import com.gitee.swsk33.loginterface.spi.Logger;

/**
 * Logger SPI的实现类
 */
public class LogOne implements Logger {

    @Override
    public void info(String s) {
        System.out.println("[LogOne INFO] " + s);
    }

    @Override
    public void debug(String s) {
        System.out.println("[LogOne DEBUG] " + s);
    }

}

然后在resources目录下创建目录META-INF/services,这个目录中是用于声明该服务实现中有哪些实现类实现了什么接口

在这个目录下我们新建一个文件名为com.gitee.swsk33.loginterface.spi.Logger,文件中的内容为:

com.gitee.swsk33.logserviceone.service.LogOne

可见,该目录下文件名要实现的接口的全限定类名(包名 + 类名),而文件中内容实现了该接口的实现类的全限定类名

大家参考这里的文件名及其中的内容,与我们上述的接口全限定类名、实现类全限定类名对比一下就知道了!

如果说这个项目中有多个类实现了Logger接口,那么我们都需要在文件中声明,一行一个实现类的全限定类名。

最终整个项目结构如下:

image.png

同样地,最后记得在项目目录下执行mvn install命令将其安装至本地Maven仓库,以便调用者调用。

(3) 测试接口

这里再新建一个Maven空项目log-test,作为接口的调用者,在依赖中引入实现者

image.png

然后创建一个主类调用一下接口试试:

package com.gitee.swsk33.logtest;

import com.gitee.swsk33.loginterface.service.LoggerService;

public class Main {

    private static final LoggerService LOGGER = LoggerService.getInstance();

    public static void main(String[] args) {
        LOGGER.info("测试info消息");
        LOGGER.debug("测试debug消息");
    }

}

结果:

image.png

可见,我们成功地调用了Logger接口中的方法。

通常调用者的依赖中可能会同时引入 SPI接口依赖和服务提供者(实现)的依赖,这样也没问题,不过通常服务提供者本身就依赖于 SPI接口,因此只引入服务提供者依赖,也会间接地引入 SPI接口依赖,不影响我们调用 SPI接口。

我们这里只有一个服务提供者logservice-one,如果说还有logservice-two等等多个服务提供者,我们只需要在依赖中更换一下即可,代码完全不需要改变。

也可见调用者在调用接口的时候,只需要关注接口就行了,不需要关心实现类。

3,再看ServiceLoader

可见在SPI接口中,我们使用ServiceLoader完成了对所有实现了Logger接口的类的扫描和加载,那么具体的过程是什么样的呢?

如果大家去查看这个类的源码,可以发现它实现了Iterable接口,这也说明我们可以通过迭代的方式去完成多个实现类的切换。

然后在其源码中,有这么一个常量定义:

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

这就说明,ServiceLoader会去扫描服务提供者的classpath路径下的META-INF/services目录,来扫描哪些类实现了指定接口,而其静态方法load的参数,正是指定了被实现的接口。也因此我们要在服务提供者的项目的resources目录下创建这个目录并申明接口和对应实现类的全限定类名。

在Maven项目中, resources目录就对应的是 classpath的根目录。

简而言之,ServiceLoader加载实现类的过程如下:

  • 先是调用load方法并指定要扫描的接口
  • 然后扫描项目中META-INF/services目录,这包括调用者项目以及它所引入的所有依赖包中的META-INF/services目录下的声明
  • 扫描到所有实现类后,根据其类名,先判断是否跟SPI接口为同一类型,如果是则利用反射的方式将所有实现类实例化,加载进内存,并返回所有实现类的实例列表

可见,这就是JDK中SPI机制加载服务的大致过程,事实上,现在很多框架也利用SPI机制实现了灵活地扩展。

示例仓库地址:传送门

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
8天前
|
Java API 开发者
探索Java中的Lambda表达式:简洁与强大的代码实践
本文深入探讨Java中Lambda表达式的定义、用法及优势,通过实例展示其如何简化代码、提升可读性,并强调在使用中需注意的兼容性和效率问题。Lambda作为Java 8的亮点功能,不仅优化了集合操作,还促进了函数式编程范式的应用,为开发者提供了更灵活的编码方式。
|
12天前
|
Java
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
本文介绍了拼多多面试中的模拟拼团问题,通过使用 `CyclicBarrier` 实现了多人拼团成功后提交订单并支付的功能。与之前的 `CountDownLatch` 方法不同,`CyclicBarrier` 能够确保所有线程到达屏障点后继续执行,并且屏障可重复使用。文章详细解析了 `CyclicBarrier` 的核心原理及使用方法,并通过代码示例展示了其工作流程。最后,文章还提供了 `CyclicBarrier` 的源码分析,帮助读者深入理解其实现机制。
|
5天前
|
安全 Java 编译器
Java反射的原理
Java 反射是一种强大的特性,允许程序在运行时动态加载、查询和操作类及其成员。通过 `java.lang.reflect` 包中的类,可以获取类的信息并调用其方法。反射基于类加载器和 `Class` 对象,可通过类名、`getClass()` 或 `loadClass()` 获取 `Class` 对象。反射可用来获取构造函数、方法和字段,并动态创建实例、调用方法和访问字段。虽然提供灵活性,但反射会增加性能开销,应谨慎使用。常见应用场景包括框架开发、动态代理、注解处理和测试框架。
|
8天前
|
Java 程序员 数据库连接
Java中的异常处理机制:理解与实践
本文将深入探讨Java语言中异常处理的核心概念、重要性以及应用方法。通过详细解析Java异常体系结构,结合具体代码示例,本文旨在帮助读者更好地理解如何有效利用异常处理机制来提升程序的健壮性和可维护性。
|
11天前
|
Java 调度 开发者
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java多线程编程的核心概念和实际应用,通过浅显易懂的语言解释多线程的基本原理,并结合实例展示如何在Java中创建、控制和管理线程。我们将从简单的线程创建开始,逐步深入到线程同步、通信以及死锁问题的解决方案,最终通过具体的代码示例来加深理解。无论您是Java初学者还是希望提升多线程编程技能的开发者,本文都将为您提供有价值的见解和实用的技巧。
15 2
|
8天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
11天前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
|
2月前
|
Dubbo Java 关系型数据库
Java SPI机制分析
文章深入分析了Java SPI机制,以JDBC为例,详细探讨了服务提供者接口的发现、加载过程,并提供了一个序列化服务的实战示例,展示了如何使用ServiceLoader进行服务发现和扩展。
19 3
|
2月前
|
Java 开发者
Java SPI机制大揭秘:动态加载服务提供者,一文让你彻底解锁!
【8月更文挑战第25天】Java SPI(服务提供者接口)是一种强大的扩展机制,允许程序在运行时动态加载服务实现。本文首先介绍SPI的基本原理——定义接口并通过配置文件指定其实现类,随后通过示例演示其实现过程。接着,对比分析了SPI与反射及插件机制的不同之处,强调SPI在灵活性与扩展性方面的优势。最后,基于不同场景推荐合适的选择策略,帮助读者深入理解并有效利用SPI机制。
48 1
|
2月前
|
开发者 C# 自然语言处理
WPF开发者必读:掌握多语言应用程序开发秘籍,带你玩转WPF国际化支持!
【8月更文挑战第31天】随着全球化的加速,开发多语言应用程序成为趋势。WPF作为一种强大的图形界面技术,提供了优秀的国际化支持,包括资源文件存储、本地化处理及用户界面元素本地化。本文将介绍WPF国际化的实现方法,通过示例代码展示如何创建和绑定资源文件,并设置应用程序语言环境,帮助开发者轻松实现多语言应用开发,满足不同地区用户的需求。
44 0
下一篇
无影云桌面