在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则"。所以我们一般有两种选择:一种是使用API(Application Programming Interface),另一种是SPI(Service Provider Interface),API通常被应用程序开发人员使用,而SPI通常被框架扩展人员使用。

  在进入下面学习之前,我们先来再加深一下API和SPI这两个的印象:

  API:由实现方制定接口标准并完成对接口的不同实现,这种模式服务接口从概念上更接近于实现方;

  SPI:由调用方制定接口标准,实现方来针对接口提供不同的实现;从前半句话我们来看,SPI其实就是"为接口查找实现"的一种服务发现机制;这种模式,服务接口组织上位于调用方所在的包中,实现位于独立的包中。

  API和SPI简略图示:

  

  看完上面的简单图示,相信大家对API和SPI的区别有了一个大致的了解,现在我们使用SPI机制来实现我们一个简单的日志框架:

  第一步,创建一个maven项目命名为spi-interface,定义一个SPI对外服务接口,用来后续提供给调用者使用;

package cn.com.wwh;
/**
*
* @FileName Logger.java
* @version:1.0
* @Description: 服务提供者接口
* @author: wwh
* @date: 2022年9月19日 上午10:31:53
*/
public interface Logger { /**
*
* @Description:(功能描述)
* @param msg
*/
public void info(String msg); /**
*
* @Description:(功能描述)
* @param msg
*/
public void debug(String msg);
}
package cn.com.wwh;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader; /**
*
* @FileName LoggerService.java
* @version:1.0
* @Description: 为服务的调用者提供特定的功能,是SPI的核心功能
* @author: wwh
* @date: 2022年9月19日 上午10:33:30
*/
public class LoggerService { private static final LoggerService INSTANCE = new LoggerService(); private final Logger logger; private final List<Logger> loggers = new ArrayList<>(); private LoggerService() {
     //ServiceLoader是实现SPI的核心类
ServiceLoader<Logger> sl = ServiceLoader.load(Logger.class);
Iterator<Logger> it = sl.iterator();
while (it.hasNext()) {
loggers.add(it.next());
} if (!loggers.isEmpty()) {
logger = loggers.get(0);
} else {
logger = null;
}
} /**
* @Description:(功能描述)
* @return
*/
public static LoggerService getLoggerService() {
return INSTANCE;
} /**
*
* @Description:(功能描述)
* @param msg
*/
public void info(String msg) {
if (logger == null) {
System.err.println("在info方法中没有找到Logger的实现类...");
} else {
logger.info(msg);
}
} /**
*
* @Description:(功能描述)
* @param msg
*/
public void debug(String msg) {
if (logger == null) {
System.err.println("在debug方法中没有找到Logger的实现类...");
} else {
logger.info(msg);
}
}
}

  将上面这个这个项目打成spi-interface.jar包。

  第二步,新建一个maven项目并导入第一步中打出来的spi-interface.jar包,这个项目用来提供服务的实现,定义一个类,实现第一步中定义的cn.com.wwh.Logger接口,示例代码如下:

package cn.com.wwh;

import cn.com.pep.Logger;

/**
*
* @FileName Logback.java
* @version:1.0
* @Description: 服务接口的实现类
* @author: wwh
* @date: 2022年9月19日 上午10:50:31
*/
public class Logback implements Logger { @Override
public void debug(String msg) {
System.err.println("调用Logback的debug方法,输出的日志为:" + msg);
} @Override
public void info(String msg) {
System.err.println("调用Logback的info方法,输出的日志为:" + msg);
} }

  同时在当前项目的classpath路径下建立META-INF/services/文件夹(至于为什么这么建立目录,我们一会儿再解释),并且新建一个名称为cn.com.wwh.Logger内容为cn.com.wwh.Logback的文件,这一步是关键(具体作用后面再详细说明),然后将上面第二步这个这个项目打成spi-provider.jar包,供给之后使用,我目前使用的开发工具是Eclipse,目录结构如下图所示:

  

  第三步,编写测试类,新建一个maven项目,命名为spi-test,导入前面两个步骤打的spi-interface.jar和spi-provider.jar这两个jar包,并编写测试代码,示例如下:

package cn.com.wwh;

import cn.com.pep.LoggerService;

/**
*
* @FileName SpiTest.java
* @version:1.0
* @Description:
* @author: wwh
* @date: 2022年9月19日 上午10:56:31
*/
public class SpiTest { public static void main(String[] args) {
LoggerService logger = LoggerService.getLoggerService();
logger.info("我是中国人");
logger.debug("白菜多少钱一斤");
}
}

  SPI的大致原理是:应用启动的时候,扫描classpath下面的所有jar包,将jar包下的/META-INF/services/目录下的文件加载到内存中,进行一系列的解析(文件的名称是spi接口的全路径名称,文件内容应该是spi接口实现类的全路径名,可以用多个实现类,在文件中换行保存),之后判断当前类和当前接口是否是同一类型?结果为true,则通过反射生成指定类的实例对象,保存到一个map集合中,可以通过遍历或者迭代的方式拿出来使用。

  SPI实质就是一个加载服务实现的工具,核心类是ServiceLoader,其实了解了SPI的原理,我们再接着探究JDK中的源码就没有那么费力了,下面我们开始源码分析吧。

  ServiceLoader类是定义在java.util包下的,使用final定义禁止子类继承和修改,实现了Iterable接口,使得可以通过迭代或者遍历的方式获取SPI接口的不同实现。

从上面的我们所举的例子中,我们知道SPI的入口是ServiceLoader.load(Class<S> service)方法,我们来看看它都干了什么?   

  这4步总的来说,就是使用指定的类型和当前线程绑定的classLoader实例化了一个LazyIterator对象赋值给lookupIterator这个引用,并且清除了原来providers列表中缓存的服务的实现。接下来我们调用了ServiceLoader实例的iterator()方法获取了一个迭代器,代码如下:

 1 public Iterator<S> iterator() {
2 //通过匿名内部类方式提供了一个迭代器
3 return new Iterator<S>() {
4 //获取缓存的服务实现者的迭代器
5 Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator();
6
7 //判断迭代器中是否还有元素
8 public boolean hasNext() {
9 //缓存的服务实现者的迭代器中已经没有元素了
10 if (knownProviders.hasNext())
11 return true;
12 return lookupIterator.hasNext();//判断延迟加载的迭代器中是否还有元素
13 }
14
15 //获取迭代其中的下一个元素
16 public S next() {
17 if (knownProviders.hasNext())
18 return knownProviders.next().getValue();
19 return lookupIterator.next();//获取延迟加载的迭代器中的下一个元素
20 }
21
22 public void remove() {
23 throw new UnsupportedOperationException();
24 }
25 };
26 }

  我们接着调用上步获取的迭代器it的hasNext()方法,因为我们在ServiceLoader.load()过程中其实是清除了providers列表中的缓存服务实现的,所以其实调用的是lookupIterator.hasNext()方法,如下:

 1 public boolean hasNext() {
2 if (nextName != null) {//存在下一个元素
3 return true;
4 }
5 if (configs == null) {//配置文件为空
6 try {
7 String fullName = PREFIX + service.getName();//获取配置文件路径
8 if (loader == null)
9 configs = ClassLoader.getSystemResources(fullName);
10 else
11 configs = loader.getResources(fullName);//加载配置文件
12 } catch (IOException x) {
13 fail(service, "Error locating configuration files", x);
14 }
15 }
16 //遍历配置文件内容
17 while ((pending == null) || !pending.hasNext()) {
18 if (!configs.hasMoreElements()) {
19 return false;
20 }
21 pending = parse(service, configs.nextElement());//配置文件内容解析
22 }
23 nextName = pending.next();//获取服务实现类的全路径名
24 return true;
25 }
26

  假如上部判断为true,紧接着我们又调用了迭代器it的next()方式,同理也调用的是lookupIterator.next()方法,源码如下:

 1 public S next() {
2 if (!hasNext()) {
3 throw new NoSuchElementException();
4 }
5 String cn = nextName;//文件中保存的服务接口实现类的全路径名
6 nextName = null;
7 Class<?> c = null;
8 try {
9 //获取全限定名的Class对象
10 c = Class.forName(cn, false, loader);
11 } catch (ClassNotFoundException x) {
12 fail(service, "Provider " + cn + " not found");
13 }
14 //判断实现类和服务接口是否是同一类型
15 if (!service.isAssignableFrom(c)) {
16 fail(service, "Provider " + cn + " not a subtype");
17 }
18 try {
19 //通过反射生成服务接口的实现类,并判断这个实例是否是接口的实现
20 S p = service.cast(c.newInstance());
21 //将服务接口的实现缓存起来,并返回
22 providers.put(cn, p);
23 return p;
24 } catch (Throwable x) {
25 fail(service, "Provider " + cn + " could not be instantiated", x);
26 }
27 throw new Error(); // This cannot happen
28 }

  其实spi实现的主要流程是:扫描classpath路径下的所有jar包下的/META-INF/services/目录(即我们需要将服务接口的具体实现类暴露在这个目录下,之前我们提到需要在实现类的classpath下面建立一个/META-INF/services/文件夹就是这个原因。),找到对应的文件,读取这个文件名找到对应的SPI接口,然后通过InputStream流将文件内容读出来,获取到实现类的全路径名,并得到这个全路径名所表示的Class对象,判断其与服务接口是否是同一类型,然后通过反射生成服务接口的实现,并保存在providers列表中,供给后续的使用。

  SPI这种设计方式为我们的应用扩展提供了极大的便利,但是它的短板也是显而易见的,Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。所以说 Java SPI 无法按需加载实现类。

  另外,SPI 机制在很多框架中都有应用:slf4j日志框架、Spring 框架的基本原理也是类似的反射。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别(Dubbo可以实现按需加载实现类),不过整体的原理都是一致的,我们今天先对SPI有个简单的了解,相信有了今天的基础理解剩下的那几个也不是什么难事。

  好了,今天就到这儿了,文章中有说的不对的地方还请各位大佬批评指正,一起学习,共同进步,谢谢。

Java中的SPI原理浅谈的更多相关文章

  1. 对kotlin和java中的synchronized的浅谈

    synchronized在java中是一个关键字,但是在kotlin中是一个内联函数.假如分别在java和kotlin代码锁住同一个对象,会发生什么呢,今天写了代码试了试.首先定义people类 12 ...

  2. Java中Integer和String浅谈

    Java中的基本数据类型有八种:int.char.boolean.byte.long.double.float.short.Java作为一种面向对象的编程语言,数据在Java中也是一种对象.我们用基本 ...

  3. Java中常用修饰符浅谈

    一.public.protected.default和private修饰符的作用域 public:在java程序中,如果将属性和方法定义为 public 类型,那么此属性和方法所在的类和及其子类,同一 ...

  4. Java中的容器 I————浅谈Queue和PriorityQueue

    一.Queue的实现 通过LinkedList类实现Queue接口来完成对Queue的实例类的实现,代码如下: Queue<Integer> queue=new LinkedList< ...

  5. Java中的容器 I————浅谈List

    一.List接口的继承关系 List接口是Collection接口的子接口,而ArrayList和LinkedList以及Vector是其实现类. List的特点是可以将元素维护在特定的序列中,可以再 ...

  6. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  7. CSRF漏洞原理浅谈

    CSRF漏洞原理浅谈 By : Mirror王宇阳 E-mail : mirrorwangyuyang@gmail.com 笔者并未深挖过CSRF,内容居多是参考<Web安全深度剖析>.& ...

  8. .net中对象序列化技术浅谈

    .net中对象序列化技术浅谈 2009-03-11 阅读2756评论2 序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储 ...

  9. 【Java】深入理解Java中的spi机制

    深入理解Java中的spi机制 SPI全名为Service Provider Interface是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用 ...

随机推荐

  1. 内存分析器 (MAT)

    内存分析器 (MAT) 1. 内存分析器 (MAT) 1.1   MAT介绍 MAT是Memory Analyzer tool的缩写.指分析工具. 1.2   MAT作用 Eclipse Memory ...

  2. 没有Kubernetes怎么玩Dapr?

    Dapr 被设计成一个面向开发者的企业级微服务编程平台,它独立于具体的技术平台,可以运行在"任何地方".Dapr本身并不提供"基础设施(infrastructure)&q ...

  3. Nacos 的安装与服务的注册

    Nacos 的安装与服务的注册 我们都知道naocs是一个注册中心,那么注册中心是什么呢? 什么是注册中心? 它类似与一个中介角色(不收费的良心中介), 在微服务中起纽带的作用,它提供了服务和服务地址 ...

  4. Vite+TS带你搭建一个属于自己的Vue3组件库

    theme: nico 前言 随着前端技术的发展,业界涌现出了许多的UI组件库.例如我们熟知的ElementUI,Vant,AntDesign等等.但是作为一个前端开发者,你知道一个UI组件库是如何被 ...

  5. java通过注解指定顺序导入excel

    自定义的属性,用来判断顺序的 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; impor ...

  6. 【每天学一点-01】 在SpringBoot项目中使用Swagger2

    今天在做毕设的时候,发现在前后端分离的情况下,去调用接口数据时很不方便,然后回想过去,和同学一起做项目的时候,他负责后端,我负责前端,当时调用他的弄好的接口可以说是非常方便,主要是可以通过UI页面直接 ...

  7. CF1703B ICPC Balloons 题解

    题意:输入每个团队及团队的解决问题数,若是第一次解决则获得两个气球,其余获得一个气球. 做法:开一个数组记录是否为第一次解决该问题,直接模拟. #include<cstdio> #incl ...

  8. 简单的数据结构_via牛客网

    题面 链接:https://ac.nowcoder.com/acm/contest/28537/K 来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 131072K,其他语 ...

  9. 洛谷 P2073 送花 treap 无指针

    看了那么多题解都没做对,结果今早上按自己的思路和模板做了做,然后过了. 平衡树裸题 直接上代码: #include<bits/stdc++.h> #define rint register ...

  10. java字符串类型数学运算表达式以及精度丢失问题

    字符串类型数学运算精度丢失问题 方式一:ScriptEngine 会精度丢失,可执行连续双括号 方式二:hutool ScriptUtil 会精度丢失,可执行连续双括号 方式三:hutool Scri ...