我是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;
@Override
public Song parse(byte[] data) throws Exception{
if (!isDataCompatible(data)) {
throw new Exception("data format is wrong.");
}
//parse data by mp3 format type
return 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<Parser> loadedParsers = ServiceLoader.load(Parser.class);
Iterator<Parser> driversIterator = loadedParsers.iterator();
try{
while(driversIterator.hasNext()) {
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;
@Override
public Song parse(byte[] data) throws Exception{
if (!isDataCompatible(data)) {
throw new Exception("data format is wrong.");
}
//parse data by rmvb format type
return 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 机制。
很多朋友看到了这篇文章都说我好厉害啊,怎么能想出这么巧妙的方法,其实这些都是模仿 Java JDBC 的源码的。有兴趣的同学可以到我的博客看看这篇文章:带你一行行深入解析JDBC源码
文章首发于【陈树义的博客】,点击跳转到原文《我是 SPI,我让框架更加优雅了!》
我是SPI,我让框架更加优雅了!的更多相关文章
- RT Thread的SPI设备驱动框架的使用以及内部机制分析
注释:这是19年初的博客,写得很一般,理解不到位也不全面.19年末得空时又重新看了RTThread的SPI和GPIO,这次理解得比较深刻.有时间时再整理上传. -------------------- ...
- 自监督SOTA框架 | BYOL(优雅而简洁) | 2020
文章原创自微信公众号「机器学习炼丹术」 作者:炼丹兄 联系方式:微信cyx645016617 本篇文章主要讲解两个无监督2020年比较新比较火的论文: 论文名称:"Bootstrap You ...
- spi驱动框架全面分析,从master驱动到设备驱动
内核版本:linux2.6.32.2 硬件资源:s3c2440 参考: 韦东山SPI视频教程 内容概括: 1.I2C 驱动框架回顾 2.SPI 框架简单介绍 3.maste ...
- 策略模式与SPI机制,到底有什么不同?
这里说的策略模式是一种设计模式,经常用于有多种分支情况的程序设计中.例如我们去掉水果皮,一般来说对于不同的水果,会有不同的拨皮方式.此时用程序语言来表示是这样的: if(type == apple){ ...
- 【Dubbo】带着问题看源码:什么是SPI机制?Dubbo是如何实现的?
什么是SPI? 在Java中,SPI全称为 Service Provider Interface,是一种典型的面向接口编程机制.定义通用接口,然后具体实现可以动态替换,和 IoC 有异曲同工之妙. ...
- Java中的SPI原理浅谈
在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则& ...
- spi子系统之驱动SSD1306 OLED
spi子系统之驱动SSD1306 OLED 接触Linux之前,曾以为读源码可以更快的学习软件,于是前几个博客都是一边读源码一边添加注释,甚至精读到每一行代码,实际上效果并不理想,看过之后就忘记了.主 ...
- yii框架的理解
Yii Framework是一个基于组件.用于开发大型 Web 应用的高性能 PHP 框架.Yii提供了今日Web 2.0应用开发所需要的几乎一切功能.Yii是最有效率的PHP框架之一. yii框架里 ...
- Hybrid框架UI重构之路:一、师其长技以自强
这两年在支撑公司的Hybrid框架的运维发展,让人确认这种移动开发方式确实是一条不错的路.混合应用这种开发方式降低开发难度,极大的提高开发效率,最重要的一点效果可以接近原生应用.框架的本身是需要持续不 ...
随机推荐
- Jboss7或者wildfly部署war包的问题
如果在Jboss7或者wildfly中部署war包是遇到类似如下错误: "{"JBAS014671: Failed services" => {"jbos ...
- Windows Server 2012无法安装 .NET3.5-安装角色或功能失败,找不到源文件
新服务器搭建环境总是有很多问题.在安装MS SQL SERVER 2014 EXPRESS的时候提示无法安装成功,需要先安装 .NET Framework 3.5!但是新问题又来了,使用Windows ...
- 【MySQL 5.7 Reference Manual】15.4.2 Change Buffer(变更缓冲)
15.4.2 Change Buffer(变更缓冲) The change buffer is a special data structure that caches changes to se ...
- python模拟自动登录网站(urllib2)
不登录打开网页: import urllib2 request = urllib2.Request('http://www.baidu.com') response = urllib2.urlopen ...
- 转-python异步IO-asyncio
原文连接 http://blog.chinaunix.net/uid-190176-id-4223282.html 前言 异步操作在计算机软硬件体系中是一个普遍概念,根源在于参与协作的各实体处理速度上 ...
- 【2017下集美大学软工1412班_助教博客】团队作业3——需求改进&系统设计团队成绩公示
作业要求 团队作业3--需求改进&系统设计 团队评分结果 Total QH BZ GJ MS SXX WBS SJ JG SJK JH NR CS PHILOSOPHER 需求改进&系 ...
- pip安装python模块遇到一直出现retrying的问题
最近安装python模块,遇到这样的一个问题如图所示: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status= ...
- MySQL5.7通过压缩包方式安装与配置
首先下载MySQL5.7的压缩包:https://dev.mysql.com/downloads/mysql/5.7.html#downloads 1.解压缩到目标文件夹,解压后有许多文件,介绍一下用 ...
- mpvue使用vant Weapp运行npm run build命令打包后失效
最近在使用mpvue开发微信小程序,在开发过程中使用有赞的小程序ui框架—— vant Weapp ,至于如何使用在我个人博客中有一篇关于如何使用vant Weapp ,需要的同学请点进这里自行查看. ...
- apache出现You don’t have permission to access / on this server问题的解决
今天在部署一个系统时,在apache中新开了一个VirtualHost,然后设置了DocumentRoot,等访问时却提示“You don’t have permission to access / ...