您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
深入了解SPI插件机制
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
深入了解SPI插件机制
自猿其说Tech
2021-11-08
IP归属:未知
102480浏览
计算机编程
### 1 概述 SPI(全称:Service Provider Interface),是Java提供的一套用来服务发现机制。主要是框架的研发用来启用框架扩展和替换组件。SPI应用比较广泛,比如java.sql.Driver接口,数据库厂商针对标准接口做出各自实现,MySQL和PostgreSQL都有不同的实现,用户通过引入对应jar包插件式使用。 ### 2 思想 对于机制的使用,首先要了解机制背后思想。其实SPI主要是插件式思想。 #### 2.1 举个例子 小霸王学习机大家都不陌生(童年的回忆)。当我们想玩游戏的时候需要插卡。假如我们拥有两张卡:“四合一游戏卡”和“64合一游戏卡”,想玩哪个卡直接插入小霸王就可以了。 ![](//img1.jcloudcs.com/developer.jdcloud.com/3cefb038-bc7f-4761-a57e-3f8ceda8666620211108185948.png) 其实这种思想遵循了设计模式的依赖倒置原则。符合的设计模式比如工厂模式、模板模式等。 #### 3 java SPI 下面通过一个小例子,说明java中如何使用SPI。 先看下SPI遵循的规范 ![](//img1.jcloudcs.com/developer.jdcloud.com/e37a1c72-892a-4fe7-ba2a-4bf7f75e1e0b20211108184825.jpg) 然后展示下demo的目录结构 ![](//img1.jcloudcs.com/developer.jdcloud.com/188fcc6e-166c-4381-979d-7ed83ab77da020211108185832.png) 说明: 为了更好体现扩展,所以使用三个模块: 1. api模块,代表提出标准接口的jar包。 1. impl模块,代表具体实现标准接口厂商的jar包,注意我的SPI配置文件是放在这个模块的,而非test模块。 1. test,是使用方,通过引入api决定使用接口,通过引入impl来确定自己要用的实现方。 #### 3.1 OrderApi通用接口 ```java public interface OrderApi { void createOrder(String orderId); } ``` #### 3.2 CompanyOrderApiImpl接口实现类 ```java public class CompanyOrderApiImpl implements OrderApi { @Override public void createOrder(String orderId) { System.out.println("CompanyOrderApiImpl createOrder orderId = " + orderId); } } ``` #### 3.3 UserOrderApiImpl接口实现类 ```java public class UserOrderApiImpl implements OrderApi { @Override public void createOrder(String orderId) { System.out.println("UserOrderApiImpl createOrder orderId = " + orderId); } } ``` #### 3.4 com.demo.api.OrderApi配置文件 ``` com.demo.api.impl.CompanyOrderApiImpl com.demo.api.impl.UserOrderApiImpl ``` #### 3.5 SpiMain测试类 ```java public class SpiMain { public static void main(String[] args) { ServiceLoader<OrderApi> loaders = ServiceLoader.load(OrderApi.class); for (OrderApi loader : loaders) { loader.createOrder("order_1"); } } } ``` #### 3.6 输出结果 ![](//img1.jcloudcs.com/developer.jdcloud.com/8e9ca173-848d-47d9-89d6-5b7fe5f18d7d20211108190151.png) ### 4 java spi原理 先看下ServiceLoader类结构 ```java public final class ServiceLoader<S> implements Iterable<S> { //配置文件的路径 private static final String PREFIX = "META-INF/services/"; //加载的服务类或接口 private final Class<S> service; //已加载的服务类集合 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); //类加载器 private final ClassLoader loader; //内部类,真正加载服务类 private LazyIterator lookupIterator; } ``` 这里重点就是通过LazyIterator这个内部加载类完成。 #### 4.1 LazyIterator 看下内部加载类的结构 ![](//img1.jcloudcs.com/developer.jdcloud.com/c19cbdc7-a7b5-4b9b-b1c2-7283f0dfea3920211108190234.png) 通过结构可以看出内部类实现Iterator接口,且使用父类相同的泛型。既然是迭代器的实现。由于代码比较简单,我们就看关键的hastNextService()和nextService()方法。 ```java private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { 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; } 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 } ``` - 通过配置地址获取所有类型名相同的文件 - 读取文件,获取实现类全路径名 - 通过反射方式创建实现类实例并缓存 ### 5 java SPI与双亲委派 ![](//img1.jcloudcs.com/developer.jdcloud.com/5b4b2265-d63c-4264-9a5a-7bd828372b2120211108190318.jpg) 值得注意的是spi是一种打破双亲委派的例子,在面试中很容易被面试官问道。jvm在类加载阶段,通过双亲委派原则来保证类安全。但是为了扩展,提出SPI机制,是通过Thread.currentThread().getContextClassLoader()方式获取当前线程的类加载器完成类的加载。具体细节就不在这里展开了,大家感兴趣可以查看JVM类加载器相关文章。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:京喜达技术部 吕顺
原创文章,需联系作者,授权转载
上一篇:京东快递APP的Flutter代码规范实践
下一篇:Vue3 + Vite 前端工程化-基础篇
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号