我是 SPI,我让框架更加优雅了!
原文地址:我是 SPI,我让框架更加优雅了!
这篇文章是看其他大佬写的,读完以后收获非常大,所以这里分享给大家
自从上次小黑进入公司的架构组之后,小黑就承担起整个公司底层框架的开发工作。就在刚刚,小黑又接到一个任务:做一个通用的歌曲信息解析框架。即输入歌曲数据,之后返回该歌曲的名称、作者、时长等时间。
接到项目的小黑经过两天的奋战,终于把第一个版本的歌曲解析框架完成了。第一版的歌曲解析框架是这样的:
public class ParseUtil{public static Song parseMp3Song(byte[] data){//parse song according to mp3 data format}
}
使用的人只需要引入工具类,之后调用 parseMp3Song() 方法即可,非常方便。
ParseUtil.parseMp3Song(data); //song stored with mp3 format
过了几天领导又找上门来了,说有些歌曲是用 mp4 格式存储的,你这个方法就用不了啊。你今天下班之前赶紧把这个功能加上,其他项目急着用呢。苦逼的小黑加班加点在 ParseUtil 中加上了 parseMp4Song 这个方法,于是第二版的歌曲解析框架是这样的:
public class ParseUtil{public static Song parseMp4Song(byte[] data){//parse song according to mp4 data format}
}
写完之后小黑赶紧将框架版本升级到 2.0.0,并通知使用框架的兄弟们升级框架,并修改相关代码。这时候使用框架是这样的:
ParseUtil.parseMp3Song(data); //song stored with mp3 format
ParseUtil.parseMp4Song(data); //song stored with mp4 format
但第二版本的歌曲解析框架上线之后,小黑觉得这样的设计并不好,要是后面又有新的歌曲格式,那我岂不是还得修改框架。而且对于使用框架的人来说,这种使用方式并不友好。因为每次调用框架之前,都需要知道解析的歌曲是什么格式,如果是 mp3 格式的歌曲,那么调用 ParseUtil.parseMp3Song(data) 方法。如果是 mp4 格式的歌曲,那么调用 ParseUtil.parseMp4Song(data) 方法。这未免太笨了吧!
小黑想:无论对于什么样歌曲,都不应该让框架使用者去关心它的格式。框架使用者只需要将数据传给我,我再将结果告诉他就好了。
就在小黑冥思苦想的时候,站在一旁的树义同学说:你想一想,这种情况是不是有点像我们使用 JDBC 连接数据库?
当我们想使用 MySQL 数据库的时候,我们需要引入 mysql 的驱动包。
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version>
</dependency>
而当我们使用 SQLServer 数据库的时候,我们需要引入 SQLServer 的驱动包。
<dependency><groupId>com.microsoft.sqlserver</groupId><artifactId>mssql-jdbc</artifactId><version>6.4.0.jre8</version>
</dependency>
但是我们在获取数据库连接的时候,却都是用同样的代码:
Connection conn = DriverManager.getConnection(DB_URL,USER,PASS);
Statement stmt = conn.createStatement();
String sql = "SELECT id, name, url, comment FROM blog";
ResultSet rs = stmt.executeQuery(sql);
那我们能不能也参考 JDBC 的设计方法,把歌曲解析器这个单独抽离出来,当需要增加一个新的歌曲解析器时,直接引入相关的解析器 Jar 包就好了。这样在增加歌曲格式解析器时,我们就不需要修改框架代码,只需要新增一个特定格式解析器的 Jar 包就可以。
按着这种实现思路,小黑立即着手开始第三版歌曲解析框架的开发。经过三天三夜的开发,框架终于开发完成,这时候的框架分成了三个部分:
song-parser 项目。负责定义通用的歌曲解析接口,并不提供任何具体的歌曲解析器实现。
song-parser-mp3 项目。实现了 song-parser 项目的歌曲解析接口,实现了 mp3 格式歌曲的解析。
song-parser-mp4 项目。实现了 song-parser 项目的歌曲解析接口,实现了 mp4 格式歌曲的解析。
这时候使用歌曲解析框架的流程是这样的:
首先,在项目中引入 song-parser 项目以及具体的解析实现,例如这里我引入 song-parser-mp3、song-parser-mp4 项目。
//歌曲解析框架
<dependency><groupId>com.chenshuyi.demo</groupId><artifactId>song-parser</artifactId><version>1.0.0</version>
</dependency>
//引入MP3歌曲解析器
<dependency><groupId>com.xiaohei.demo</groupId><artifactId>song-parser-mp3</artifactId><version>1.0.0</version>
</dependency>
这里引入了 mp3 歌曲解析器,那么我们就可以在项目中解析 mp3 格式的歌曲。
//parse mp3 song
Song song = ParserManager.getSong(mockSongData("MP3"));
如果需要解析 mp4 格式的歌曲,那我们引入 mp4 歌曲解析器:
<dependency><groupId>com.chenshuyi.demo</groupId><artifactId>song-parser</artifactId><version>1.0.0</version>
</dependency>
<dependency><groupId>com.xiaohei.demo</groupId><artifactId>song-parser-mp3</artifactId><version>1.0.0</version>
</dependency>
//引入MP4歌曲解析器
<dependency><groupId>com.xiaoshu.demo</groupId><artifactId>song-parser-mp4</artifactId><version>1.0.0</version>
</dependency>
之后还是使用 ParserManager.getSong(byte[] data)
方法进行歌曲信息解析:
//parse mp4 song
Song song = ParserManager.getSong(mockSongData("MP4"));
经过这样的一个设计,我们发现升级之后,使用的人并不需要修改原有的代码,也不需要升级原有的框架版本,只需要将新的歌曲解析器 Jar 包引入即可。
看着最新完成的第三版歌曲解析框架,小黑暗暗得意自己的架构设计,觉得这绝对是一个划时代的创造。于是赶紧跟树义分享自己的设计思路,没想到树义却淡定地说:其实这个就是 Java 的 SPI 机制,英文全称是 Service Provider Interface,常用于框架的可扩展实现。Java 语言的 JDBC、JDNI 就使用了这种技术,甚至我们常用的 dubbo 也是在 Java SPI 机制基础上做的改进。
小黑怪不好意思地摸摸头,原来 Java 的创造者早就想到了,我还以为自己创造了一种新的开发方式呢!虽然树义知道是用 SPI 机制实现的,但树义还是对小黑怎么做出这个框架感到好奇,于是问小黑:你这个框架到底是咋做的叻,说出来让我们学习学习呗!
小黑得意地打开 IDE 编辑器,滔滔不绝地说起来。其实这个「歌曲解析框架」分为两个部分:
song-parser 项目。负责定义通用的歌曲解析接口,并不提供任何具体的歌曲解析器实现。
song-parser-xxx 项目。实现了 song-parser 项目的歌曲解析接口,实现了 xxx 格式歌曲的解析。例如上面说的,song-parser-mp3 实现了 mp3 格式歌曲的解析,song-parser-mp4 实现了 mp4 格式歌曲的解析,等等。
song-parser 项目
song-parser 项目定义了通用的歌曲解析接口,并不提供具体的解析实现。在 song-parser 项目定义了下面两个关键的接口和类:Parser 接口、ParserManager 类。
Parser 接口
定义了抽象的解析方法,传入歌曲的数据,返回歌曲的信息。
public interface Parser {Song parse(byte[] data) throws Exception;
}
ParseManager 类
主要包括两个三个部分:
loadInitialParsers()
用于程序启动时初始化所有的歌曲解析器。
ParserManager.registerParser()
用于歌曲解析器的注册。
ParserManager.getSong()
提供了获取歌曲信息的方法。
public class ParserManager {private final static CopyOnWriteArrayList<ParserInfo> registeredParsers = new CopyOnWriteArrayList<>();static {loadInitialParsers();System.out.println("SongParser initialized");}private static void loadInitialParsers() {ServiceLoader<Parser> loadedParsers = ServiceLoader.load(Parser.class);Iterator<Parser> driversIterator = loadedParsers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}}public static synchronized void registerParser(Parser parser) {registeredParsers.add(new ParserInfo(parser));}public static Song getSong(byte[] data) {for (ParserInfo parserInfo : registeredParsers) {try {Song song = parserInfo.parser.parse(data);if (song != null) {return song;}} catch (Exception e) {//wrong parser, ignored it.}}throw new ParserNotFoundException("10001", "Can not find corresponding data:" + new String(data));}
}
其实上面的几个方法对应了 Service Provider Framework 的四个概念:
Service Interface 服务接口,这里对应 Song 接口。
Provider Registration API 用户注册接口,这里对应 ParserManager.registerParser() 方法。
Service Access API 获取服务实例方法,这里对应 ParserManager.getSong() 方法。
Service Provider Interface 创建服务实现的接口,这里对应 Parser 接口。
所有借助 Java SPI 机制实现的框架,除了 Service Interface 服务接口不是必须的之外,其他三个都是必须要有的。
这里我们用 mp3 歌曲解析器为例,来看看到底是如何实现的插件式的歌曲解析的。
在 song-parse-mp3 项目中有两个类和一个描述文件,分别是:com.chenshuyi.demo.Parser 文件、Parser 类和 Mp3Parser 类。
com.chenshuyi.demo.Parser 文件
该文件位于/resources/META-INF/services
目录下,包含如下地址:com.xiaohei.demo.Parser
,表示歌曲解析的具体实现类。
Parser 类
在 Parser 类中,其调用 ParserManager.registerParser() 类将解析器注册到一个 List 集合中。
public class Parser extends Mp3Parser implements com.chenshuyi.demo.Parser {static{try{ParserManager.registerParser(new Parser());}catch (Exception e){throw new RuntimeException("Can't register parser!");}}
}
Mp3Parser 类
而在 Parser.parse() 方法中,则实现了具体的解析业务逻辑。
public class Mp3Parser implements Parser {public final byte[] FORMAT = "MP3".getBytes();public final int FORMAT_LENGTH = FORMAT.length;@Overridepublic Song parse(byte[] data) throws Exception{if (!isDataCompatible(data)) {throw new Exception("data format is wrong.");}//parse data by mp3 format typereturn new Song("刘千楚", "mp3", "《北京东路的日子》", 220L);}private boolean isDataCompatible(byte[] data) {byte[] format = Arrays.copyOfRange(data, 0, FORMAT_LENGTH);return Arrays.equals(format, FORMAT);}
}
当我们调用以下语句去获取歌曲信息时,因为 ParserManager.getSong() 是静态方法,所以会先初始化 ParserManager 类。
Song song = ParserManager.getSong(mockSongData("MP3"));
在 ParserManager 类中有下面这段代码:
static {loadInitialParsers();System.out.println("SongParser initialized");
}
在 loadInitialParsers() 方法中,调用了 Java 的 ServiceLoader 类获取 Parser 接口的所有实现。
private static void loadInitialParsers() {// 这里会创建好ServiceLoader自己重写的Iterator->LazyIterator 以及寻找// /resources/META-INF/services 下面声明的类ServiceLoader<Parser> loadedParsers = ServiceLoader.load(Parser.class);Iterator<Parser> driversIterator = loadedParsers.iterator();try{while(driversIterator.hasNext()) {// LazyIterator 的next方法会 创建声明过的实现类的实例,// 我们这里声明的类的实例创建的时候,会调用register方法,往list里面加数据driversIterator.next();}} catch(Throwable t) {// Do nothing}
}
当 ParserManager 初始化完成之后,就调用 getSong() 静态方法。
public static Song getSong(byte[] data) {for (ParserInfo parserInfo : registeredParsers) {try {Song song = parserInfo.parser.parse(data);if (song != null) {return song;}} catch (Exception e) {//wrong parser, ignored it.}}throw new ParserNotFoundException("10001", "Can not find corresponding data:" + new String(data));
}
从下图我们可以得知,其实 loadInitialParsers() 方法运行之后,是将所有 Parser 接口的所有视线都放到了 ParserManager.registeredParsers 这个 List 中。
ParserManager.getSong 方法循环遍历所有歌曲解析器,一旦获得正确的解析结果便返回。如果全部遍历结束,还找不到正确的解析器,那么就返回 null。
在一旁的树义听着虽然有点懵,但是还是大概听懂了。这不就是,但是还是觉得小黑很厉害。但说了这么多,我还不知道怎么用这个框架呢。如果我要新增一种来解析 rmvb 格式歌曲,那应该怎么做呢?小黑淡定地摆出 OK 的手势说:10 分钟搞定。
小黑首先创建了一个项目 song-parser-rmvb:
<groupId>com.anonymous.demo</groupId>
<artifactId>song-parser-rmvb</artifactId>
<version>1.0.0</version>
接着创建了一个 RmvbParser 类,用于实现具体的歌曲信息解析:
public class RmvbParser implements com.chenshuyi.demo.Parser {public final byte[] FORMAT = "RMVB".getBytes();public final int FORMAT_LENGTH = FORMAT.length;@Overridepublic Song parse(byte[] data) throws Exception{if (!isDataCompatible(data)) {throw new Exception("data format is wrong.");}//parse data by rmvb format typereturn new Song("AGA", "rmvb", "《Wonderful U》", 240L);}private boolean isDataCompatible(byte[] data) {byte[] format = Arrays.copyOfRange(data, 0, FORMAT_LENGTH);return Arrays.equals(format, FORMAT);}
}
之后创建了一个 Parser 类,用于在启动的时候向 ParserManager 类注册解析器:
public class Parser extends RmvbParser implements com.chenshuyi.demo.Parser {static{try{ParserManager.registerParser(new Parser());}catch (Exception e){throw new RuntimeException("Can't register parser!");}}
}
最后在创建了一个描述文件resources/META-INF/services/com.chenshuyi.demo.Parser
,并填上了下面的内容:
com.anonymous.demo.Parser
改造完成之后,小黑将新的 RMVB 解析器信息告诉了开发兄弟。开发兄弟在项目中引入了新的歌曲解析器依赖:
//新增rmvb歌曲解析器
<dependency><groupId>com.anonymous.demo</groupId><artifactId>song-parser-rmvb</artifactId><version>1.0.0</version>
</dependency>
之后使用 ParserManager.getSong(byte[] data)
方法进行歌曲信息解析:
Song song = ParserManager.getSong(mockSongData("RMVB"));
System.out.println("Name:" + song.getName());
System.out.println("Author:" + song.getAuthor());
System.out.println("Time:" + song.getTime());
System.out.println("Format:" + song.getFormat());
代码成功运行,输出:
Name:《Wonderful U》
Author:AGA
Time:240
Format:rmvb
站在一旁的树义看得眼睛都呆了,这样的开发效率真的很快,而且又很优雅!
树义有话说
Java SPI 无处不在,通过使用 SPI 能够让框架的实现更加优雅,实现可插拔的插件开发。本文中的歌曲解析框架就是借鉴这种方式进行开发的,虽然只是一个简化版的实现,但是其能让你更快了解 SPI 机制的实现原理。
「歌曲解析框架」代码已经上传到 Github 上,感兴趣的朋友可以下载代码:chenyurong/song-parser-spi-demo。如果想进一步掌握 Java SPI 的应用,建议下载项目并自行扩展一个歌曲解析器,这样可以最大程度上理解 Java SPI 机制。
我是 SPI,我让框架更加优雅了!相关推荐
- postmapping注解参数说明_从零搭建后端框架:优雅的参数校验Validator
前两天项目群里发生了关于参数校验的问题讨论,很多开发团队没有对这些做硬性规范时,还是有很多童鞋本着"不多事"的原则,产品文档里没有特别说明就不写.对于2B的产品经理来说,因为一次新 ...
- css框架和js框架_优雅设计的顶级CSS框架
css框架和js框架 Brief discussion: 简要讨论: Well, who doesn't want their website or web page to look attracti ...
- 框架 go_Colly - 优雅极速的Go语言爬虫框架
写爬虫,Python 是目前的第一选择,但总觉得 Python 太慢了,而且缺乏静态类型.能不能换 Go 语言来试试呢?Colly,这个既优雅又快速的 Go 语言爬虫框架,是你的不二选择. 爬虫框架 ...
- platform框架--Linux MISC杂项框架--Linux INPUT子系统框架--串行集成电路总线I2C设备驱动框架--串行外设接口SPI 设备驱动框架---通用异步收发器UART驱动框架
platform框架 input. pinctrl. gpio 子系统都是 Linux 内核针对某一类设备而创建的框架, input子系统是管理输入的子系统 pinctrl 子系统重点是设置 PIN( ...
- linux spi不使用框架,Linux spi驱动框架之执行流程
Linux spi驱动架构由三部分构成:SPI核心层.SPI控制器驱动层.和SPI设备驱动程序. 1.SPI核心层: SPI核心层是Linux的SPI核心部分,提供了核心数据结构的定义.SPI控制器驱 ...
- php返回json套数组_教你PHP怎么不用框架写优雅的中小网站
php这种语言本来就是函数化为主的语言,讲究的是简单实用.但是现在市面上现在流行的框架大多臃肿,复杂,学习难度大,同时有大量用不着的东西,做个小网站或者小项目还是太笨重.所以 这里我提供一点用纯php ...
- 从事前端多年,我是这样看待三大框架的
Vue.Angular.React对比 前端在复杂性日增的今天,三个框架被我们熟知,Vue.React.Angular,三个经常经常被我们拿来讨论,对比,比如学习哪个? 前端框架解决的核心问题 在我入 ...
- web前端学习(四):基于koa的EggJs框架,优雅而又完美的Nodejs框架
**前言: ** Egg.js为企业级开发应用,而产生的一门Node.js框架 它使用模块化渐进式开发,内置多进程管理(Node是单进程,无法使用多核CPU的能力),具有高度可扩展的插件机制. 基于K ...
- RK3399平台开发系列讲解(SPI子系统)4.36、SPI子系统驱动框架详解
平台 内核版本 安卓版本 RK3399 Linux4.4 Android7.1
- 谈谈我的框架设计经验
不出意外,很多技术人应该都有写个框架给别人用的想法,自己会不会用暂且不说,肯定用过别人写的,比如Spring这种主流的web框架,如果debug Spring源码,一步步跳来跳去,看着眼花缭乱,可能会 ...
最新文章
- iOS 支付宝支付集成获取私钥
- React Native之提示Unable to load script from assets ‘index.android.bundle
- Spring Cloud Alibaba基础教程:Sentinel Dashboard中修改规则同步到Nacos
- ubuntu14.04安装opencv3.1(亲测)
- 解决placeholder样式设置无效问题,更改placeholder默认样式颜色
- mysql三高讲解(三)3.2:如何确定用哪条索引
- 一线城市程序员平均工资11770元,你拖后腿了吗?
- 埋石图根点lisp代码_GPS测量作业流程.doc
- Gym 100703F Game of words 动态规划
- Beego 框架学习(一)
- WordPress简约mkBlog博客主题模板v2.1
- 如何将PDF转换成word文档
- 2.4.4 Profile基本参数
- 电信计费系列2-手机+座机计费
- node.js+uniapp计算机毕业设计安卓和悦少年文明礼仪监管APP(程序+APP+LW)
- 完全卸载 HP 打印机
- 支持NDS的视频音频转换软件Xilisoft Video Converter
- outlook自定义快捷键_如何自定义主题和Outlook邮件的格式
- Vue(3)之 过滤器
- 基本IO函数的使用(open,write,read)(一)
热门文章
- 关于短期培训月薪过万的悖论
- win10+Python3.7.3+OpenCV3.4.1入门学习(十一 图像金字塔)————11.1 图像金字塔理论基础
- 学会用CUPS管理打印机
- 全国信息流广告优化师交流群,不容错过!赶紧加入!
- tomcat8的apr模式配置SLL证书
- python车牌识别系统开源代码_python利用百度云接口实现车牌识别的示例
- 树莓派小实验 | 制作一个带快门的照相机 录像机
- html关于点击radio触发事件
- Online Calculators (在线计算器) - Math Calculators (数学计算器)
- 巴蜀1471 魔兽争霸