对Java中SPI的理解

什么是SPI

SPI全称Service Provider Interface,字面意思是提供服务的接口,再解释详细一下就是Java提供的一套用来被第三方实现或扩展的接口,实现了接口的动态扩展,让第三方的实现类能像插件一样嵌入到系统中。

咦。。。

这个解释感觉还是有点绕口。

那就说一下它的本质。

将接口的实现类的全限定名配置在文件中(文件名是接口的全限定名),由服务加载器读取配置文件,加载实现类。实现了运行时动态为接口替换实现类。

SPI示例

还是举例说明吧。

我们创建一个项目,然后创建一个module叫spi-interface。

在这个module中我们定义一个接口:

/**

* @author jimoer

**/publicinterfaceSpiInterfaceService{/**

    * 打印参数

    * @param parameter 参数

    */voidprintParameter(String parameter);}

再定义一个module,名字叫spi-service-one,pom.xml中依赖spi-interface。

在spi-service-one中定义一个实现类,实现SpiInterfaceService 接口。

packagecom.jimoer.spi.service.one;importcom.jimoer.spi.app.SpiInterfaceService;/** *@authorjimoer **/publicclassSpiOneServiceimplementsSpiInterfaceService{/**    * 打印参数    *    *@paramparameter 参数    */@OverridepublicvoidprintParameter(String parameter){        System.out.println("我是SpiOneService:"+parameter);    }}

然后在spi-service-one的resources目录下创建目录META-INF/services,在此目录下创建一个文件名称为SpiInterfaceService接口的全限定名称,文件内容写入SpiOneService这个实现类的全限定名称。

效果如下:

再创建一个module,名称为:spi-service-one,也是依赖spi-interface,并且定义一个实现类SpiTwoService 来实现SpiInterfaceService 接口。

packagecom.jimoer.spi.service.two;importcom.jimoer.spi.app.SpiInterfaceService;/** *@authorjimoer **/publicclassSpiTwoServiceimplementsSpiInterfaceService{/**    * 打印参数    *    *@paramparameter 参数    */@OverridepublicvoidprintParameter(String parameter){        System.out.println("我是SpiTwoService:"+parameter);    }}

目录结构如下:

下面再创建一个用来测试的module,名为:spi-app。

pom.xml中依赖spi-service-one和spi-service-two

com.jimoer.spispi-service-one1.0-SNAPSHOTcom.jimoer.spispi-service-two1.0-SNAPSHOT

创建测试类

/** *@authorjimoer **/publicclassSpiService{publicstaticvoidmain(String[] args){        ServiceLoader spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);        Iterator iterator = spiInterfaceServices.iterator();while(iterator.hasNext()){            SpiInterfaceService sip = iterator.next();            sip.printParameter("参数");        }    }}

执行结果:

我是SpiTwoService:参数我是SpiOneService:参数

通过运行结果我们可以看到,已经将SpiInterfaceService接口的所有实现都加载到了当前项目中,并且执行了调用。

这整个代码结构我们可以看出SPI机制将模块的装配放到了程序外面,就是说,接口的实现可以在程序外面,只需要在使用的时候指定具体的实现。并且动态的加载到自己的项目中。

SPI机制的主要目的:

一是为了解耦,将接口和具体实现分离开来;

二是提高框架的扩展性。以前写程序的时候,接口和实现都写在一起,调用方在使用的时候依赖接口来进行调用,无权选择使用具体的实现类。

SPI的实现

那么我们来看一下SPI具体是如何实现的呢?

通过上面的例子,我们可以看到,SPI机制的核心代码是下面这段:

ServiceLoader spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);

那么我们来看一下ServiceLoader.load()方法的源码:

publicstatic ServiceLoader load(Classservice){    ClassLoader cl = Thread.currentThread().getContextClassLoader();returnServiceLoader.load(service, cl);}

看到Thread.currentThread().getContextClassLoader();我就明白是怎么回事了,这个就是线程上下文类加载器,因为线程上下文类加载器就是为了做类加载双亲委派模型的逆序而创建的。

使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了,双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。

《深入理解Java虚拟机(第三版)》

虽然知道了它是破坏双亲委派的了,但是具体实现,还是需要具体往下看的。

在ServiceLoader里找到具体实现hasNext()的方法了,那么继续来看这个方法的实现。

hasNext()方法又主要调用了hasNextService()方法。

// 固定路径privatestaticfinalString PREFIX ="META-INF/services/";privatebooleanhasNextService(){if(nextName !=null) {returntrue;    }if(configs ==null) {try{// 固定路径+接口全限定名称String fullName = PREFIX + service.getName();// 如果当前线程上下文类加载器为空,会用父类加载器(默认是应用程序类加载器)if(loader ==null)                configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);        }catch(IOException x) {            fail(service,"Error locating configuration files", x);        }    }while((pending ==null) || !pending.hasNext()) {if(!configs.hasMoreElements()) {returnfalse;        }        pending = parse(service, configs.nextElement());    }// 后面next()方法中判断当前类是否已经出现化的时候要用nextName = pending.next();returntrue; }

主要就是去加载META-INF/services/路径下的接口全限定名称的文件然后去里面找到实现类的类路径将实现类进行类加载。

继续看迭代器是如何取出每一个实现对象的。那就要看ServiceLoader中实现了迭代器的next()方法了。

next()方法主要是nextService()实现的,那么继续看nextService()方法。

privateS nextService() {if(!hasNextService())thrownewNoSuchElementException();Stringcn = nextName;    nextName =null;    Class c =null;try{// 直接加载类,无需初始化(因为上面hasNext()已经初始化了)。c = Class.forName(cn,false, loader);    }catch(ClassNotFoundException x) {        fail(service,"Provider "+ cn +" not found");    }if(!service.isAssignableFrom(c)) {        fail(service,"Provider "+ cn  +" not a subtype");    }try{// 将加载好的类实例化出对象。S p = service.cast(c.newInstance());        providers.put(cn, p);returnp;    }catch(Throwable x) {        fail(service,"Provider "+ cn +" could not be instantiated",              x);    }thrownewError();// This cannot happen}

看到这里就可以明白了,是如何创建出对象的了。先在hasNext()将接口的实现类进行加载并判断是否存在接口的实现类,然后在next()方法中将实现类进实例化。

Java中使用SPI机制的功能其实有很多,像JDBC、JNDI、以及Spring中也有使用,甚至RPC框架(Dubbo)中也有使用SPI机制来实现功能。

作者:纪莫

链接:https://www.cnblogs.com/jimoer/p/14095489.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容