文章目录
- Java SPI机制解析
- 什么是SPI
- 使用场景
- Java常见场景
- 功能开发步骤
- 代码示例
- 制定统一的接口
- DriverManager的作用
- 服务提供者根据统一的接口,做出具体实现
- 服务提供者暴露服务
- 调用方根据需要引用特定的服务提供者jar包
- 测试
- 源码分析
- 总结
- 参考
- 项目地址
Java SPI机制解析
什么是SPI
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制。
整体机制如图
JAVA SPI 实际上是**“接口编程+策略模式+配置文件”**组合实现的动态加载机制
例如jdbc编程,往往由社区制定规范,然后各大厂商根据规范制定对应的功能,在JDBC中就出现了mysql、oracle等,通过SPI可实现可拔插的实现机制
使用场景
适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
Java常见场景
- 数据库驱动加载接口实现类的加载 JDBC加载不同类型数据库的驱动
- 日志接口SLF4J加载不同提供商的日志实现类
- Dubbo框架中大量使用如DubboFilter、LoadBalance等SPI实现
功能开发步骤
- 制定统一的接口
- 服务提供者根据统一的接口,做出具体实现
- 服务提供者暴露服务(JAVA SPI 通过在Resouce下META-INF/services目录下创建一个以接口全限定类名为命名的文件,文件内容为具体实现接口全限定类名,可多个)
- 调用方根据需要引用特定的服务提供者jar包(主程序将通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件,找到需装载的类)
代码示例
以下是代码示例,视频播放的SPI,功能如下,我们按着上述一步步来实现这个播放器管理吧
制定统一的接口
- 根据对应的URL,播放器驱动获取对应的播放器。(类比,java.sql.Driver)
/**
* Description
*
* @author Created by 叶半仙 on 2019/7/3.
*/
public interface VideoPlayerDriver {
/**
* 获取对应的播放器
* @param url
* @return
* @throws IllegalAccessException
*/
VideoPlayer getPlayer(String url) throws IllegalAccessException;
/**
* 是否支持对应的url
* @param url
* @return
*/
boolean support(String url);
}
- 播放器接口,暂定以下接口,播放接口,获取时长接口,播放器名称,播放器版本等接口,实现如下(类比,java.sql.Connection)
/**
* Description 播放器接口方法
*
* @author Created by 叶半仙 on 2019/7/3.
*/
public interface VideoPlayer {
/**
* 播放
* @param url
* @return
*/
String play(String url);
/**
* 获取对应时长
*/
long duration(String url);
/**
* 播放器名称
* @return
*/
String playerName();
/**
* 播放器版本
*/
String playerVersion();
}
以上,我们完成了我们开发步骤的第一步,定义好了我们所需要的接口,但是,大家想想,我们在使用jdbc驱动的时候,我们是不是还有一个DriverManager去管理所有的驱动?所以我们这里也需要一个VideoPlayerDriverManager
DriverManager的作用
- 服务发现
- 服务注册
- 服务调用适配
针对以上三点,我对VideoPlayerDriverManager的实现大致有了以下三个方法
-
loadInitialPlayers()
服务发现 -
registered(VideoPlayerDriver videoPlayerDriver)
服务注册 -
VideoPlayer player(String url)
根据对应的URL,自动适配选用合适的播放器
以上三个方法已可在java.sql.DriverManager找到对比
loadInitialDrivers();
registerDriver(Driver driver);
Connection getConnection(String url,String user, String password)
其中,一般服务发现,是在DriverManager加载的时候就会去做,所有代码实现中,会把该方法放在静态代码块内
static {
loadInitialPlayers();
logger.debug("加载驱动完成");
}
其中loadInitialPlayers()
是我们的重中之重,java.util.ServiceLoder去自动扫描所有我们需要动态装载的实现类
private static void loadInitialPlayers(){
ServiceLoader<VideoPlayerDriver> serviceLoader = ServiceLoader.load(VideoPlayerDriver.class);
// 获取所有在META-INF/services下注册的VideoPlayerDriver接口实现
Iterator<VideoPlayerDriver> iterator = serviceLoader.iterator();
while (iterator.hasNext()){
//驱动类加载
iterator.next();
}
}
其实上面几个接口也对应了 Service Provider Framework 的四个概念:
- Service Interface 服务接口,这里对应
VideoPlayer
接口。 - Provider Registration API 用户注册接口,这里对应
VideoPlayerDriverManager.registerDriver()
方法。 - Service Access API 获取服务实例方法,这里对应
VideoPlayerDriverManager.player(url)
方法。 - Service Provider Interface 创建服务实现的接口,这里对应
VideoPlayerDriver
接口。
所有借助 Java SPI 机制实现的框架,除了Service Interface 服务接口不是必须的之外,其他三个都是必须要有的。
VideoPlayerDriverManager源码
/**
* Description 播放器驱动管理类
*
* @author Created by 叶半仙 on 2019/7/3.
*/
public class VideoPlayerManager {
/**
* 处理器集合
*/
private final static CopyOnWriteArrayList<VideoPlayerDriver> registeredDrivers = new CopyOnWriteArrayList<>();
private static final Logger logger = LoggerFactory.getLogger( VideoPlayerManager.class );
static {
loadInitialPlayers();
logger.debug("加载驱动完成");
}
/**
* 初始化
*/
private static void loadInitialPlayers(){
ServiceLoader<VideoPlayerDriver> serviceLoader = ServiceLoader.load(VideoPlayerDriver.class);
// 获取所有在META-INF/services下注册的VideoPlayerDriver接口实现
Iterator<VideoPlayerDriver> iterator = serviceLoader.iterator();
while (iterator.hasNext()){
//驱动类加载
iterator.next();
}
}
/**
* 获取对应的播放器
* @param url
* @return
*/
public static VideoPlayer player(String url){
VideoPlayer videoPlayer = null;
for (VideoPlayerDriver driver:registeredDrivers){
try {
videoPlayer = driver.getPlayer(url);
break;
}catch (Exception e){
logger.debug("驱动器:{},处理发生异常",driver.getClass(),e);
}
}
return videoPlayer;
}
/**
* 获取所有的驱动器
* @return
*/
public static List<VideoPlayerDriver> getDrivers(){
List<VideoPlayerDriver> videoPlayerDrivers = new LinkedList<>();
registeredDrivers.forEach(e->videoPlayerDrivers.add(e));
return videoPlayerDrivers;
}
/**
* 注册驱动
* @param videoPlayerDriver
*/
public static synchronized void registered(VideoPlayerDriver videoPlayerDriver){
logger.info("注册驱动:{}",videoPlayerDriver.getClass());
registeredDrivers.addIfAbsent(videoPlayerDriver);
}
public static void main(String[] args) {
logger.info("测试驱动加载");
}
}
服务提供者根据统一的接口,做出具体实现
我这里新建了个tencent 腾讯视频播放器的module,并针对VideoPlayer
和VideoPlayerDriver
做出对应版本的TencentVideoPlayer
和 TencentVideoPlayerDriver
/**
* Description 腾讯视频播放驱动
*
* @author Created by 叶半仙 on 2019/7/3.
*/
public class TencentVideoPlayerDriver implements VideoPlayerDriver {
/**
* 如果url的格式是http://www.vip.qq.com/的格式,代表需要使用腾讯视频的播放器
*/
public static final String REGEX = "^http(s)?://www\\.vip\\.qq\\.com/[A-Za-z0-9\\.]*";
public static final Pattern PATTERN = Pattern.compile(REGEX);
@Override
public VideoPlayer getPlayer(String url) {
if (support(url)){
// 此处可扩展成工厂或单例模式,直接new也是不太符合规范的
return new TencentVideoPlayer();
}
throw new RuntimeException("该url不支持");
}
@Override
public boolean support(String url) {
return PATTERN.matcher(url).matches();
}
static{
// 向VideoPlayerDriver注册驱动
VideoPlayerManager.registered(new TencentVideoPlayerDriver());
}
}
这样,一个新的播放器驱动我们就写好了,其中。千万不要忘记加个静态块,向我们的DriverManager做驱动注册哟。
/**
* Description 腾讯播放器
*
* @author Created by 叶半仙 on 2019/7/3.
*/
public class TencentVideoPlayer implements VideoPlayer {
/**
* 播放
*
* @param url
* @return
*/
@Override
public String play(String url) {
return playerName()+": "+url;
}
/**
* 获取对应时长
*
* @param url
*/
@Override
public long duration(String url) {
return url.length();
}
/**
* 播放器名称
*
* @return
*/
@Override
public String playerName() {
return "腾讯视频";
}
/**
* 播放器版本
*/
@Override
public String playerVersion() {
return "1.0.0-SNAPSHOT";
}
}
我这边还写了一个youku的播放器,驱动方面仅支持 http://www.youku.com
/**
* Description 优酷播放器驱动
*
* @author Created by 叶半仙 on 2019/7/3.
*/
public class YoukuVideoPlayerDriver implements VideoPlayerDriver {
/**
* 如果url的格式是http://www.youku.com/的格式,代表需要使用优酷视频的播放器
*/
public static final String REGEX = "^http(s)?://www\\.youku\\.com/[A-Za-z0-9\\.]*";
public static final Pattern PATTERN = Pattern.compile(REGEX);
@Override
public VideoPlayer getPlayer(String url) {
if (support(url)){
return new YoukuVideoPlayer();
}
throw new RuntimeException("该url不支持");
}
@Override
public boolean support(String url) {
return PATTERN.matcher(url).matches();
}
static{
VideoPlayerManager.registered(new YoukuVideoPlayerDriver());
}
youku的player我就不贴了,可能在优酷播放器里面就主要是打印的文字不太一样,主要是为了吐槽优酷视频的广告太长。哈哈哈
服务提供者暴露服务
这里我们需要严格按照SPI要求的格式
- 在resources目录下新建META-INF/service目录
- 该目录下新建以 接口全限定类名 文件,以VideoPlayerDriver为例,
com.zhongkk.spi.video.driver.VideoPlayerDriver
- 在上面文件中写上实现的接口全限定类名
com.zhongkk.spi.video.tencent.driver.TencentVideoPlayerDriver
调用方根据需要引用特定的服务提供者jar包
在maven/gradle项目中引用对应jar包
//驱动接口包,管理类包
compile project(':manager')
// 腾讯视频驱动包
compile project(':spi_video_tencent_player')
// 优酷视频驱动包
compile project(':spi_video_youku_player')
以上类似于,我们如果需要使用mysql,我们就引用
compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'
如果我们要使用oracle,我们就引入
compile group: 'com.oracle', name: 'ojdbc14', version: '10.2.0.4.0'
好了,以上我们就完成了我们需要做的前置工作。现在,我们开始测试看看效果吧
测试
测试代码如下
/**
* Description 播放器驱动测试类
*
* @author Created by 叶半仙 on 2019/7/3.
*/
public class Main {
public static void main(String[] args) {
testTencent();
testYouku();
}
public static void testTencent() {
String url = "http://www.vip.qq.com/zhongkk.mp4";
VideoPlayer player = VideoPlayerManager.player(url);
System.out.println("===== "+player.playerName()+"播放器欢迎您 =====");
System.out.println(player.play(url));
System.out.println("时长: "+player.duration(url)+"分钟");
}
public static void testYouku() {
String url = "http://www.youku.com/zhongkk.mp4";
VideoPlayer player = VideoPlayerManager.player(url);
System.out.println("===== "+player.playerName()+"播放器欢迎您 =====");
System.out.println(player.play(url));
System.out.println("时长: "+player.duration(url)+"分钟");
}
}
我们在驱动管理器初始化的服务发现中,发现tencent和youku的驱动都会自动发现到,调用next方法,试对应驱动加载,调用对应的静态块,完成驱动注册。注册后的驱动如下
看看两个打印下的数据
完美 perfect !
源码分析
首先看ServiceLoader类的签名类的成员变量:
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
其中 load方法作用为,新建ServiceLoader,并实例化该类中的成员变量
- 读取变量
PREFIX = "META-INF/services/"
目录和对应的service.getName()类名称(注意:ServiceLoader可以跨越jar包获取META-INF下的配置文件) - 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
- 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象
总结
优点: 使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点:
- 延迟加载,但会被所有的实现类加载一遍,某些不需要使用的类也被加载,导致浪费。不够灵活
- 多个并发多线程使用ServiceLoader类的实例是不安全的