我是 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 上,感兴趣的朋友可以下载代码:https://github.com/chenyurong/song-parser-spi-demo。如果想进一步掌握 Java SPI 的应用,建议下载项目并自行扩展一个歌曲解析器,这样可以最大程度上理解 Java SPI 机制。
很多朋友看到了这篇文章都说我好厉害啊,怎么能想出这么巧妙的方法,其实这些都是模仿 Java JDBC 的源码的。有兴趣的同学可以到我的博客看看这篇文章:带你一行行深入解析JDBC源码。
点击图片查看更多推荐内容
↓↓↓
分布式事务之TCC服务设计和实现注意事项!
2018整理最全的50道Redis面试题!
如何避免自己写的代码成为别人眼中的一坨屎!
为什么分布式一定要有Redis?