Java SPI 机制,「可插拔」的奥义所在

简介: 本文主要介绍 SPI 机制

我们上篇文章讲到了 Java 中 Agent 用法,不少小伙伴都觉得该方式比较偏门,平常开发不常用(几乎没用)。其实不然,不常用是跟项目挂钩,项目不常用不代表该方法机制不常用,因此很多时候我们学习不能坐井观天,认为项目中没用到就可以不学,跟着项目成长往往不能成长~!


上篇跳转入口:Java 高级用法,写个代理侵入你?


那么这篇我们将继续讲 Java 中的另一个知识点,也就是 SPI 机制,乍听感觉依然陌生,这时可别再打退堂鼓!往下看你就会发现原来平时开发中经常看到!


一、SPI


我们这篇文章以问题作为导向,用问题来驱动学习,小菜先抛出几个问题,下面将针对这几个问题进行解释并扩展


  • 什么是 SPI ?


  • SPI 和 API 的区别?


  • 平常中有使用到 SPI 吗?


1、什么是 SPI


SPI 是三个单词的缩写 Service Provider Interface,字面意思:服务提供接口。它是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。具体作用便是为这些被扩展的 API 寻找服务实现。


而Java SPI 便是 JDK 内置的一种服务提供发现机制,常用于创建可扩展、可替换组件的应用程序,是java中模块化插件化的关键。


这里我们提到了两个概念,分别是 模块化插件化。模块化很好理解,就是将一个项目分成多个模块,模块间可能存在相互依赖(也就是通过 maven 的方式),有使用微服务开发的同学就毫不陌生了,如果没有使用微服务开发也不打紧,单体项目中为了界定 control,service,repository层,也会将每个领域单独提取成模块,而不是以目录的方式~


网络异常,图片无法展示
|


2、类加载机制


上面我们已经说到了 SPI 较为粗浅的概念,小菜这里不打算直接深入 SPI,在深入 SPI 之前,我们先了解一下  Java 中的类加载机制。类加载机制可能实际开发中并不会去在意,但是它却无处不在,而这个也是面试的一大热点话题。


在JVM中,类加载器默认是使用双亲委派原则,默认的类加载器包括Bootstarp ClassLoaderExtension ClassLoaderSystem ClassLoader(Application ClassLoader),当然可能还有自定义类加载器~自定义类加载器可以通过继承 java.lang.classloader 来实现


各个类加载器作用范围如下:


  • Bootstrap ClassLoader:负责加载 JDK 自带的 rt.jar 包中的类文件,是所有类加载的父类


  • Extension ClassLoader:负责加载 java 的扩展类库从 jre/lib/ectjava.ext.dirs 系统属性指定的目录下加载类


  • System ClassLoader:负责从 classpath 环境变量中加载类文件


类加载继承关系图如下:


网络异常,图片无法展示
|


1)双亲委派模型


什么是双亲委派模型?


当一个类加载器收到加载类的任务时,会先交给自己的父加载器去完成,一级一级往上,因此最后都会传递到 Bootstrap ClassLoader 进行加载,只有当父加载器无法完成加载任务的时候,才会尝试自己进行加载


为什么要这样设计呢?


1、采用双亲委派原则可以避免相同类重复加载,每个加载器在进行类加载任务的时候都会委派给自己的父类加载器进行加载,如果父类加载无法加载才自己进行加载,避免重复加载的局面


2、可以保证类加载的安全性,不管是哪个加载器加载这个类,最终都是委托给顶层的加载器进行加载,保证任何加载器最终得到的都是同一个类对象


加载过程如下:


网络异常,图片无法展示
|


这样做的缺陷?


子类加载器可以使用父类加载器已经加载过的类,而父类加载器无法使用子类加载器加载过的类(类似继承的关系)。这里就可以扯到 Java SPI 了,Java 提供了很多服务提供者接口(SPI),它可以允许第三方为这些接口提供实现,比如数据库中的 SPI 服务 - JDBC,这些 SPI 的接口由Java核心类提供,实现者确实第三方,这样就会存在问题,提供者由 Bootstrap ClassLoader加载,而实现者是由第三方自定义类加载器加载,而这个时候顶层类加载就无法使用子类加载器加载过的类


网络异常,图片无法展示
|


解决方法


想要解决这个问题就得打破双亲委派原则


可以使用线程上下文类加载器(ContextClassLoader)加载


Java 应用上下文加载器默认是使用AppClassLoader,想要在父类加载器使用到子类加载器加载的类可以使用 Thread.currentThread().getContextClassLoader()


比如我们想要加载资源可以使用以下方式:


// 使用线程上下文类加载器加载资源
public static void main(String[] args) throws Exception{
    String name = "java/sql/Array.class";
    Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        System.out.println(url.toString());
    }
}


3、Java SPI


说完类加载机制,我们再回到 Java SPI 来,我们先通过例子熟悉下 SPI 的使用方式

使用过程图如下:


网络异常,图片无法展示
|


更加通俗的理解,SPI 实际上就是一种策略模式的实现,基于接口编程再配合上配置文件来读取。这也符合我们的编程方式:可插拔~


使用例子如下:


项目结构


网络异常,图片无法展示
|


  • ICustomSvc:服务提供接口(也就是 SPI)


  • CustomSvcOne/CustomSvcTwo:实现者(这里直接在一个项目中简单实现,也可以通过 jar 包导入的方式实现)


  • cbuc.life.spi.service.ICustomSvc:配置文件


文件内容


网络异常,图片无法展示
|


然后我们启动 CustomTest 查看控制台结果


网络异常,图片无法展示
|


可以看到是可以加载到我们的实现类的方法,而这也就意味着已经实现了SPI 的功能


1)实现原理


其实我们上面使用SPI的时候可以看到一个关键的类那就是ServiceLoader ,该类位于 java.util包下,我们直接点进 load() 方法查看如何调用


点进 load() 方法我们首席那看到的以下代码


网络异常,图片无法展示
|


该块代码只是简单的声明了使用线程上下文加载器,我们继续跟进 ServiceLoader.load(service, cl)


网络异常,图片无法展示
|


该块代码也没啥内容,声明返回了 ServiceLoader 对象,这个对象有什么文章?我们可以查看这个类声明


public final class ServiceLoader<S> implements Iterable<S>{}


可以看到这个对象实现了 Iterable 接口,说明具有迭代的方法,可以猜测这样是为了取出我们定义 SPI 的所有实现类。


该类的构造函数如下


网络异常,图片无法展示
|


重点在于 reload() 方法,我们继续跟进


网络异常,图片无法展示
|


这里将注释一起截取出来,我们可以看到这句话 方法将惰性查找实例化,说明了上述说到实现 Iterable 接口的用处,我们这里可以先点进 iterator() 方法查看是如何实现的


网络异常,图片无法展示
|


可以看到有个关键的缓存,该缓存存储 provider,每次操作的时候都会去该缓存中查找,如果存在则返回,否则采用 LazyIterator 进行查找,我们进行进入到LazyIterator类中查看如何实现,由于该类代码过长,我们直接截取关键代码,有兴趣的同学可以自行查看完整代码:


网络异常,图片无法展示
|


看到该代码的实现顿时豁然开朗了,我们看到了熟悉的目录名 META-INF/services/,该代码会去指定目录下获取文件资源,然后通过上传传入的线程上下文类加载器进行类加载,这样子我们的 SPI 实现类就可以供项目使用了~ 看完不得不感叹 妙啊~


到这里为止,我们就已经拆解了 JAVA SPI 的使用以及实现原理,看完后是不是觉得该技巧也没有离我们很远~!


4、小结


使用 Java SPI 机制更好的实现了 可插拔 的开发理念,使得第三方服务模块的装配与调用者的业务代码相分离,也就是 解耦 的概念,我们应用程序可以根据实际业务需要进行动态插拔。


二、扩展


Spring SPI


当然 SPI 机制不仅仅在 JDK 中实现,我们日常开发用到的 Spring 以及 Dubbo 框架都有对应的 SPI 机制。在Spring Boot中好多配置和实现都有默认的实现,我们如果想要修改某些配置,我们只需要在配置文件中写上对应的配置,那么项目应用的便是我们定义的配置内容,而这种方式就是采用 SPI 实现的。


Java SPI 与 Spring SPI 的区别


  • JDK 使用的加载工具类是 ServiceLoader,而 Spring 使用的是 SpringFactoriesLoader


  • JDK 目录命名方式是META-INF/services/提供方接口全类名,而 Spring 使用的是 META-INF/spring-factories


在使用 Spring Boot 中我们会将想要注入 IOC 容器的类将全类限定名写到 META-INF/spring.factories文件中,在 Spring Boot 程序启动的时候就会由 SpringFactoriesLoader 进行加载,扫描每个 jar 包 class-path 目录下的 META-INF/spring.factories 配置文件,然后解析 properties 文件,找到指定名称的配置后返回


网络异常,图片无法展示
|


所以说 SPI 在我们实际开发中随处可见,不止 Spring ,比如JDBC加载数据库驱动,SLF4J加载不同提供商的日志实现还有 Dubbo 使用SPI的方式实现框架的扩展等等


目录
相关文章
|
24天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
64 2
|
29天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
8天前
|
Java 程序员
深入理解Java异常处理机制
Java的异常处理是编程中的一块基石,它不仅保障了代码的健壮性,还提升了程序的可读性和可维护性。本文将深入浅出地探讨Java异常处理的核心概念、分类、处理策略以及最佳实践,旨在帮助读者建立正确的异常处理观念,提升编程效率和质量。
|
9天前
|
Java 开发者 UED
深入探索Java中的异常处理机制##
本文将带你深入了解Java语言中的异常处理机制,包括异常的分类、异常的捕获与处理、自定义异常的创建以及最佳实践。通过具体实例和代码演示,帮助你更好地理解和运用Java中的异常处理,提高程序的健壮性和可维护性。 ##
27 2
|
9天前
|
Java 开发者
Java中的异常处理机制深度剖析####
本文深入探讨了Java语言中异常处理的重要性、核心机制及其在实际编程中的应用策略,旨在帮助开发者更有效地编写健壮的代码。通过实例分析,揭示了try-catch-finally结构的最佳实践,以及如何利用自定义异常提升程序的可读性和维护性。此外,还简要介绍了Java 7引入的多异常捕获特性,为读者提供了一个全面而实用的异常处理指南。 ####
26 2
|
12天前
|
Java 程序员 UED
深入理解Java中的异常处理机制
本文旨在揭示Java异常处理的奥秘,从基础概念到高级应用,逐步引导读者掌握如何优雅地管理程序中的错误。我们将探讨异常类型、捕获流程,以及如何在代码中有效利用try-catch语句。通过实例分析,我们将展示异常处理在提升代码质量方面的关键作用。
25 3
|
12天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
13天前
|
运维 Java 编译器
Java 异常处理:机制、策略与最佳实践
Java异常处理是确保程序稳定运行的关键。本文介绍Java异常处理的机制,包括异常类层次结构、try-catch-finally语句的使用,并探讨常见策略及最佳实践,帮助开发者有效管理错误和异常情况。
45 4
|
12天前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
32 2
|
16天前
|
Java
深入探讨Java中的中断机制:INTERRUPTED和ISINTERRUPTED方法详解
在Java多线程编程中,中断机制是协调线程行为的重要手段。了解和正确使用中断机制对于编写高效、可靠的并发程序至关重要。本文将深入探讨Java中的`Thread.interrupted()`和`Thread.isInterrupted()`方法的区别及其应用场景。
21 4