我们上篇文章讲到了 Java 中 Agent 用法,不少小伙伴都觉得该方式比较偏门,平常开发不常用(几乎没用)。其实不然,不常用是跟项目挂钩,项目不常用不代表该方法机制不常用,因此很多时候我们学习不能坐井观天,认为项目中没用到就可以不学,跟着项目成长往往不能成长~!
上篇跳转入口:Java 高级用法,写个代理侵入你?
那么这篇我们将继续讲 Java 中的另一个知识点,也就是 SPI
机制,乍听感觉依然陌生,这时可别再打退堂鼓!往下看你就会发现原来平时开发中经常看到!
一、SPI
我们这篇文章以问题
作为导向,用问题来驱动学习,小菜先抛出几个问题,下面将针对这几个问题进行解释并扩展
- 什么是 SPI ?
- SPI 和 API 的区别?
- 平常中有使用到 SPI 吗?
1、什么是 SPI
SPI 是三个单词的缩写 S
ervice P
rovider I
nterface,字面意思:服务提供接口。它是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换
组件。具体作用便是为这些被扩展的 API 寻找服务实现。
而Java SPI 便是 JDK 内置的一种服务提供发现机制,常用于创建可扩展、可替换组件的应用程序,是java中模块化
与插件化
的关键。
这里我们提到了两个概念,分别是 模块化
和插件化
。模块化很好理解,就是将一个项目分成多个模块,模块间可能存在相互依赖(也就是通过 maven 的方式),有使用微服务开发的同学就毫不陌生了,如果没有使用微服务开发也不打紧,单体项目中为了界定 control,service,repository层,也会将每个领域单独提取成模块,而不是以目录的方式~
2、类加载机制
上面我们已经说到了 SPI 较为粗浅的概念,小菜这里不打算直接深入 SPI,在深入 SPI 之前,我们先了解一下 Java 中的类加载机制。类加载机制可能实际开发中并不会去在意,但是它却无处不在,而这个也是面试的一大热点话题。
在JVM中,类加载器默认是使用双亲委派原则,默认的类加载器包括Bootstarp ClassLoader
、Extension ClassLoader
和 System ClassLoader(Application ClassLoader)
,当然可能还有自定义类加载器~自定义类加载器可以通过继承 java.lang.classloader 来实现
各个类加载器作用范围如下:
- Bootstrap ClassLoader:负责加载 JDK 自带的 rt.jar 包中的类文件,是所有类加载的父类
- Extension ClassLoader:负责加载 java 的扩展类库从 jre/lib/ect或 java.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的方式实现框架的扩展等等