还不会JavaSPI机制?
什么是 SPI 1. 背景
在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。
为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点类似 IOC 的思想,将装配的控制权移交到了程序之外。
SPI 英文为 Service Provider Interface 字面意思就是:"服务提供者的接口",我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 2. 使用场景
很多框架都使用了 Java 的 SPI 机制,比如:数据库加载驱动,日志接口,以及 dubbo 的扩展实现等等。 3. SPI 和 API 有啥区别
说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个"接口"。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务,举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
实战演示
Spring框架提供的日志服务 SLF4J 其实只是一个日志门面(接口),但是 SLF4J 的具体实现可以有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。
这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。 1. Service Provider Interface
新建一个 Java 项目 service-provider-interface 目录结构如下:├─.idea └─src ├─META-INF └─org └─spi └─service ├─Logger.java ├─LoggerService.java ├─Main.java └─MyServicesLoader.java
新建 Logger 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。 package org.spi.service; public interface Logger { void info(String msg); void debug(String msg); }
接下来就是 LoggerService 类,这个主要是为服务使用者(调用方)提供特定功能的。如果存在疑惑的话可以先往后面继续看。 package org.spi.service; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; public class LoggerService { private static final LoggerService SERVICE = new LoggerService(); private final Logger logger; private final List loggerList; private LoggerService() { ServiceLoader loader = ServiceLoader.load(Logger.class); List list = new ArrayList<>(); for (Logger log : loader) { list.add(log); } // LoggerList 是所有 ServiceProvider loggerList = list; if (!list.isEmpty()) { // Logger 只取一个 logger = list.get(0); } else { logger = null; } } public static LoggerService getService() { return SERVICE; } public void info(String msg) { if (logger == null) { System.out.println("info 中没有发现 Logger 服务提供者"); } else { logger.info(msg); } } public void debug(String msg) { if (loggerList.isEmpty()) { System.out.println("debug 中没有发现 Logger 服务提供者"); } loggerList.forEach(log -> log.debug(msg)); } }
新建 Main 类(服务使用者,调用方),启动程序查看结果。 package org.spi.service; public class Main { public static void main(String[] args) { LoggerService service = LoggerService.getService(); service.info("Hello SPI"); service.debug("Hello SPI"); } }
程序结果:
info 中没有发现 Logger 服务提供者
debug 中没有发现 Logger 服务提供者
将整个程序直接打包成 jar 包,可以直接通过 IDEA 将项目打包成一个 jar 包。
2. Service Provider
接下来新建一个项目用来实现 Logger 接口
新建项目 service-provider 目录结构如下:├─.idea ├─lib │ └─service-provider-interface.jar └─src ├─META-INF │ └─services │ └─org.spi.service.Logger └─org └─spi └─provider └─Logback.java
新建 Logback 类 package org.spi.provider; import org.spi.service.Logger; public class Logback implements Logger { @Override public void info(String msg) { System.out.println("Logback info 的输出:" + msg); } @Override public void debug(String msg) { System.out.println("Logback debug 的输出:" + msg); } }
将 service-provider-interface 的 jar 导入项目中。新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。
再点击 OK 。
接下来就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的。
实现 Logger 接口,在 src 目录下新建 META-INF/services 文件夹,然后新建文件 org.spi.service.Logger (SPI 的全类名),文件里面的内容是:org.spi.provider.Logback (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。
这是 JDK SPI 机制 ServiceLoader 约定好的标准
接下来同样将 service-provider 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。3. 效果展示
接下来再回到 service-provider-interface 项目。
导入 service-provider jar 包,重新运行 Main 方法。运行结果如下:
Logback info 的输出:Hello SPI
Logback debug 的输出:Hello SPI
说明导入 jar 包中的实现类生效了。
通过使用 SPI 机制,可以看出 服务(LoggerService)和 服务提供者两者之间的耦合度非常低,如果需要替换一种实现(将 Logback 换成另外一种实现),只需要换一个 jar 包即可。这不就是 SLF4J 原理吗?
如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个 服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现 (service-provider) 来完成我们需要的操作。
loggerList.forEach(log -> log.debug(msg));
或者
loggerList.get(1).debug(msg);
loggerList.get(2).debug(msg);
这里需要先理解一点:ServiceLoader 在加载具体的 服务实现 的时候会去扫描所有包下 src 目录的 META-INF/services 的内容,然后通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式得到你需要的那个 服务实现。
3. ServiceLoader
想要使用 Java 的 SPI 机制是需要依赖 ServiceLoader 来实现的,那么我们接下来看看 ServiceLoader 具体是怎么做的:
ServiceLoader 是 JDK 提供的一个工具类, 位于 package java.util; 包下。A facility to load implementations of a service.
这是 JDK 官方给的注释:一种加载服务实现的工具。
再往下看,我们发现这个类是一个 final 类型的,所以是不可被继承修改,同时它实现了 Iterable 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。 public final class ServiceLoader implements Iterable{ xxx...}
可以看到一个熟悉的常量定义:
private static final String PREFIX = "META-INF/services/";
下面是 load 方法:可以发现 load 方法支持两种重载后的入参; public static ServiceLoader load(Class service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static ServiceLoader load(Class service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } private ServiceLoader(Class svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }
根据代码的调用顺序,在 reload() 方法中是通过一个内部类 LazyIterator 实现的。先继续往下面看。
ServiceLoader 实现了 Iterable 接口的方法后,具有了迭代的能力,在这个 iterator 方法被调用时,首先会在 ServiceLoader 的 Provider 缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator 中进行查找。 public Iterator iterator() { return new Iterator() { Iterator> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); // 调用 LazyIterator } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); // 调用 LazyIterator } public void remove() { throw new UnsupportedOperationException(); } }; }
在调用 LazyIterator 时,具体实现如下: public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction action = new PrivilegedAction() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { //通过PREFIX(META-INF/services/)和类名 获取对应的配置文件,得到具体的实现类 String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
4. 总结
其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:我们按照规定将要暴露对外使用的具体实现类在 META-INF/services/ 文件下声明。
其实 SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的反射。还有 dubbo 框架提供同样的 SPI 扩展机制。
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: 遍历加载所有的实现类,这样效率还是相对较低的; 当多个 ServiceLoader 同时 load 时,会有并发问题。
写在最后
Freemen App是一款专注于IT程序员求职招聘的一个求职平台,旨在帮助IT技术工作者能更好更快入职及努力协调IT技术者工作和生活的关系,让工作更自由!
本文转载自江璇Up
有人说叫老公已经过时了,现在最流行的称呼方式是什么?2016年,清华大学历史系教授彭林说喊丈夫老公,这一叫法是十分不对的,因为老公在古代指的是太监的身份。而实际上,老公一词,只在清朝指太监,早在宋元时期,已经是对丈夫的普遍称呼。近代
2022年最稳的理财方式是什么?今年估计又是震荡行情,总结去年也是震荡市,坚持定投一年下来,反复做过山车,也没怎么赚钱,而且体验很差。今年一定改变去年的操作方法,一部分长期的做定投,选好标的,估值合理的,有10左
家用吸尘器哪个品牌的好?我用的是飞利浦吸尘器,很好用前言前不久钣金装修好的新家,每天最烦恼的就是打扫卫生做家务。尤其是扫地来说,家里之前的那个扫把每次用完都感觉手心腾,而且像一些灰尘宝宝吃的饼干碎屑这些小
要是你的头胎是女儿,当第二胎第一时间知道又是个女儿的时候,你会有什么想法?我两个女儿。头胎知道是女儿没啥感觉。准备生二宝的时候,去找个中医调了几个月,也是想要儿女双全才吃的药,老公一天吃200到300个类似六味地黄的小药丸,我吃200个左右,刚开始吃一个
如果山河令和陈情令的播出时间调换一下,山河令会更火吗?不会,山河令拍的不美,赤裸裸的,没什么文化底蕴,陈情令承担着中国文化的输出,里面的画面,服饰,场景等其古文化体现的淋漓尽致,弘扬锄奸扶弱的正能量。不会超过陈情令,论cp感虽然山河令
从来不化妆与长期化妆的肌肤有什么区别?关注糖小姐的奢侈品,相信我,这会是一群爱装逼爱奢侈品的艺术家们的最佳聚集地。谢邀!现在大多数女生每天出门都是要化了个美美的妆才出门的,也有的女生是不会化妆,所以总是素着。那不化妆的
有什么办法可以提亮肤色吗?如何提亮肤色?嗯,这个百度了一下。皮肤暗淡无光泽,不仅令整个人看起来毫无精神,对自己的心情也是一种影响。俗话说世界上只有懒女人,没有丑女人,而肤色,虽然是天生的,但也不是不可改善的
助听器双耳互通什么意思?将双耳助听器作为一个整体,协调处理单耳接收到的声音信息,从而达到精确定位提高言语清晰度等。双耳互通,双耳听觉所接受的声音响度要比单耳高一些,在阈值水平,双耳阈值好于单耳阈值约3分贝
听力需要保健吗?你好,听力是需要保健的,平时少去噪音特别大的场所,也不要长时间佩戴耳机听音乐,以防引起噪音性耳聋,另外还要保持耳部清洁,耳朵不要进水,以防感染发炎。平时可以做做耳保健操等。希望我的
油耳朵能戴助听器吗?你好!油耳是湿型耵聍,如果油耳听力在适配范围之内可以佩戴助听器,可以选配耳背式助听器,注意清理耳道和助听器耳塞即可。谢谢邀请,你说的右耳朵是耳朵里边的油性大吗?如果是的话,可以在经
为什么河北保定,有20多个县城?在国内来说重庆市下面有40来个区县,保定只能排第二。但是在地级市来说保定那绝对是第一,石家庄排第二。但是这里说明一下,要是保定把雄安去掉,石家庄是第一。其次北京,天津,上海,哈尔滨