文章首发于【陈树义的博客】,点击跳转到原文《我是 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<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;@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 机制。

很多朋友看到了这篇文章都说我好厉害啊,怎么能想出这么巧妙的方法,其实这些都是模仿 Java JDBC 的源码的。有兴趣的同学可以到我的博客看看这篇文章:带你一行行深入解析JDBC源码

文章首发于【陈树义的博客】,点击跳转到原文《我是 SPI,我让框架更加优雅了!》

转载于:https://www.cnblogs.com/chanshuyi/p/deep_insight_java_spi.html

我是SPI,我让框架更加优雅了!相关推荐

  1. postmapping注解参数说明_从零搭建后端框架:优雅的参数校验Validator

    前两天项目群里发生了关于参数校验的问题讨论,很多开发团队没有对这些做硬性规范时,还是有很多童鞋本着"不多事"的原则,产品文档里没有特别说明就不写.对于2B的产品经理来说,因为一次新 ...

  2. css框架和js框架_优雅设计的顶级CSS框架

    css框架和js框架 Brief discussion: 简要讨论: Well, who doesn't want their website or web page to look attracti ...

  3. 框架 go_Colly - 优雅极速的Go语言爬虫框架

    写爬虫,Python 是目前的第一选择,但总觉得 Python 太慢了,而且缺乏静态类型.能不能换 Go 语言来试试呢?Colly,这个既优雅又快速的 Go 语言爬虫框架,是你的不二选择. 爬虫框架 ...

  4. platform框架--Linux MISC杂项框架--Linux INPUT子系统框架--串行集成电路总线I2C设备驱动框架--串行外设接口SPI 设备驱动框架---通用异步收发器UART驱动框架

    platform框架 input. pinctrl. gpio 子系统都是 Linux 内核针对某一类设备而创建的框架, input子系统是管理输入的子系统 pinctrl 子系统重点是设置 PIN( ...

  5. linux spi不使用框架,Linux spi驱动框架之执行流程

    Linux spi驱动架构由三部分构成:SPI核心层.SPI控制器驱动层.和SPI设备驱动程序. 1.SPI核心层: SPI核心层是Linux的SPI核心部分,提供了核心数据结构的定义.SPI控制器驱 ...

  6. php返回json套数组_教你PHP怎么不用框架写优雅的中小网站

    php这种语言本来就是函数化为主的语言,讲究的是简单实用.但是现在市面上现在流行的框架大多臃肿,复杂,学习难度大,同时有大量用不着的东西,做个小网站或者小项目还是太笨重.所以 这里我提供一点用纯php ...

  7. 从事前端多年,我是这样看待三大框架的

    Vue.Angular.React对比 前端在复杂性日增的今天,三个框架被我们熟知,Vue.React.Angular,三个经常经常被我们拿来讨论,对比,比如学习哪个? 前端框架解决的核心问题 在我入 ...

  8. web前端学习(四):基于koa的EggJs框架,优雅而又完美的Nodejs框架

    **前言: ** Egg.js为企业级开发应用,而产生的一门Node.js框架 它使用模块化渐进式开发,内置多进程管理(Node是单进程,无法使用多核CPU的能力),具有高度可扩展的插件机制. 基于K ...

  9. RK3399平台开发系列讲解(SPI子系统)4.36、SPI子系统驱动框架详解

    平台 内核版本 安卓版本 RK3399 Linux4.4 Android7.1

  10. 谈谈我的框架设计经验

    不出意外,很多技术人应该都有写个框架给别人用的想法,自己会不会用暂且不说,肯定用过别人写的,比如Spring这种主流的web框架,如果debug Spring源码,一步步跳来跳去,看着眼花缭乱,可能会 ...

最新文章

  1. 基于OHCI的USB主机 —— 结束语
  2. r语言 整理、处理数据步骤_R语言万能数据清洗整理包Tidyverse(一)
  3. 概率论 第三章 多维随机变量及其分布
  4. java mysql servlet_Java--用户登录(JDBC,MYSQL,Servlet)
  5. Java多线程之多线程之间按顺序调用
  6. 计算机系学生mac,大学生选择苹果电脑之后的经验之谈
  7. 加餐:Redis 的可视化管理工具
  8. AduSkin - UI 追求极致,永臻完美
  9. sql字符串函数_SQL字符串函数概述
  10. Linux中的的虚拟WEB主机的几点总结
  11. 代码质量 权威精选植根于开发实践的最佳读物
  12. java 调度池_定时任务调度池 - 南郭先生Official的个人空间 - OSCHINA - 中文开源技术交流社区...
  13. Unity IOS微信登录
  14. 比UUID更快更安全NanoID到底是怎么实现的?(荣耀典藏版)
  15. 双击计算机图标无法打开,我的电脑图标打不开_我的电脑双击打不开了
  16. usb3.0 驱动安装方法
  17. n维椭球体积公式_洛氏硬度、布氏硬度、维氏硬度区别与对照
  18. 广告和游戏广告变现全面解析
  19. 品牌大促“抗洪”:限量版球鞋怎样给到真爱粉?
  20. IT人物之搜狗公司COO茹立云 听学霸分享成长故事

热门文章

  1. 快速原型VS敏捷、迭代
  2. 邮件该如何发送html代码
  3. 企业微信openid转userid失败问题
  4. uni-app获取微信openid及其他信息
  5. 常见色彩表(RGB)
  6. 人脸识别的十个关键技术组成及原理
  7. matlab GUI学习笔记4 如何添加并设置下拉菜单以及GUI解决不用直接用load的问题
  8. kaggle入门titanic分析
  9. easyui datebox控件点击今天按钮不触发onSelect事件的解决方法
  10. 三阶段最小二乘法 回归分析 3SLS python实现