java中spi有什么作用,spi实现原理
在面向对象编程中,模块之间的交互是通过接口来编程的。通常调用者不需要知道被调用者的内部实现细节,因为一旦涉及到具体的实现,如果需要另一个实现就需要修改代码,这就违背了编程的‘开闭原则’。所以我们一般有两个选择:一个是使用API(应用编程接口),一个是SPI(服务提供者接口)。API通常由应用程序开发人员使用,而SPI通常由框架扩展人员使用。
在进入下面的学习之前,我们先加深一下对API和SPI的印象:
API:实现者制定接口标准,完成接口的不同实现。这种模式的服务接口在概念上更接近于实现者;
SPI:调用方制定接口标准,实现方为接口提供不同的实现;从前半句可以看出,SPI其实是一种‘为接口找实现’的服务发现机制;在这种模式下,服务接口组织在调用者所在的包中,实现位于单独的包中。
和SPI:
看了上面的简单示意图,相信你对API和SPI的区别有了大致的了解。现在我们使用SPI机制来实现一个简单的日志框架:
第一步是创建一个名为spi-interface的maven项目,并定义一个spi外部服务接口,稍后将提供给调用者。
包cn.com.wwh;
/**
*
* @文件名Logger.java
* @版本:1.0
* @Description:服务提供商接口
* @作者:wwh
* @日期:2022年9月19日上午10点31分53秒
*/
公共接口记录器{
/**
*
* @描述:(功能描述)
* @param msg
*/
公共void信息(字符串消息);
/**
*
* @描述:(功能描述)
* @param msg
*/
public void debug(字符串消息);
}包cn.com.wwh;
导入Java . util . ArrayList;
导入Java . util . iterator;
导入Java . util . list;
导入Java . util . service loader;
/**
*
* @文件名LoggerService.java
* @版本:1.0
* @Description:为服务调用方提供特定的功能是SPI的核心功能。
* @作者:wwh
* @日期:2022年9月19日上午10:33:30
*/
公共类记录器服务{
private static final LoggerService INSTANCE=new LoggerService();
私有最终记录器记录器;
private final List loggers=new ArrayList();
私有LoggerService(){
//ServiceLoader是实现SPI的核心类。
service loader Logger sl=service loader . load(Logger . class);
迭代器记录器it=sl . iterator();
while (it.hasNext()) {
loggers . add(it . next());
}
如果(!loggers.isEmpty()) {
logger=loggers . get(0);
}否则{
logger=null
}
}
/**
* @描述:(功能描述)
* @返回
*/
公共静态LoggerService getLoggerService(){
返回实例;
}
/**
*
* @描述:(功能描述)
* @param msg
*/
公共无效信息(字符串消息){
if (logger==null) {
System.err.println(在info方法中找不到Logger的实现类.);
}否则{
logger . info(msg);
}
}
/**
*
* @描述:(功能描述)
* @param msg
*/
公共void调试(字符串消息){
if (logger==null) {
System.err.println(在调试方法中找不到记录器的实现类.);
}否则{
logger . info(msg);
}
}
}把上面的项目做成spi-interface.jar包。
第二步。创建一个新的maven项目,并导入在步骤1中键入的spi-interface.jar包。这个项目用于提供服务的实现,定义一个类,实现步骤1中定义的cn.com.wwh.Logger接口。示例代码如下:
包cn.com.wwh;
导入cn . com . pep . logger;
/**
*
* @文件名Logback.java
* @版本:1.0
* @Description:服务接口实现类
* @作者:wwh
* @日期:2022年9月19日上午10点50分31秒
*/
公共类回退实现记录器{
@覆盖
公共void调试(字符串消息){
System.err.println(调用Logback的调试方法,输出日志为: msg );
}
@覆盖
公共无效信息(字符串消息){
System.err.println(调用Logback的info方法,输出日志为: msg );
}
}同时,在当前项目的类路径下建立META-INF/services/folder(至于为什么要这样建立目录,后面会解释),新建一个名为cn.com.wwh.Logger的文件,内容为cn.com.wwh.Logback这一步是关键(具体功能后面会详细解释)。然后,将上面第二步中的这个项目制作成spi-provider.jar包,供以后使用。我目前的开发工具是Eclipse,目录结构如下图所示:
第三步:编写一个测试类,创建一个新的maven项目,将其命名为spi-test,导入前两步中制作的两个jar包spi-interface.jar和spi-provider.jar,编写测试代码。例子如下:
包cn.com.wwh;
导入cn . com . pep . loggerservice;
/**
*
* @文件名SpiTest.java
* @版本:1.0
* @描述:
* @作者:wwh
* @日期:2022年9月19日上午10点56分31秒
*/
公共类spite {
公共静态void main(String[] args){
logger service logger=logger service . getloggerservice();
Logger.info(我是中国人);
Logger.debug(白菜多少钱一斤);
}
}通过SPI,我们可以轻松地将服务与服务提供者分离。如果未来某一天我们需要将日志保存到数据库或者通过网络发送,只需要为服务接口替换实现类,其他什么都不需要修改,更符合编程中的“开闭原则”。
spi的一般原理是:应用启动时,扫描classpath下的所有jar包,将jar包下的/META-INF/services/目录下的文件加载到内存中,进行一系列的分析(文件名是spi接口的全路径名,文件内容应该是SPI接口实现类的全路径名,可以用多个实现类保存,然后判断当前类和当前接口是否是同一类型)。如果结果为真,则通过反射生成指定类的实例对象,保存在映射集中,可以通过遍历或迭代取出。
SPI的本质是一个加载服务实现的工具。核心类是ServiceLoader。事实上,知道了SPI的原理,我们在JDK探索源代码并不那么费力。让我们开始源代码分析。
loader类是在java.util包下定义的。最终定义用来禁止子类的继承和修改,实现Iterable接口,通过迭代或遍历得到SPI接口的不同实现。
从上面的例子我们知道SPI的入口是service loader . load(class s service)方法。让我们看看它做了什么。
一般来说,以上四个步骤是用指定的类型和绑定到当前线程的classLoader实例化一个LazyIterator对象,赋给引用lookupIterator,清除原来providers列表中缓存的服务的实现。接下来,我们调用ServiceLoader实例的iterator()方法,用下面的代码获得一个迭代器:
1公共迭代器S iterator(){
2 //迭代器由匿名内部类提供。
3返回新的迭代器S () {
4 //获取缓存服务实现者的迭代器
5迭代器映射。条目字符串,S known providers=providers . entry set()。迭代器();
六
7 //确定迭代器中是否还有其他元素
8 public boolean hasNext(){
9 //缓存的服务实现者的迭代器中没有元素。
10 if (knownProviders.hasNext())
11返回true
12返回lookupiterator . has next();//确定延迟加载的迭代器中是否有元素。
13 }
14
15 //获取迭代中的下一个元素
16 public S next(){
17 if (knownProviders.hasNext())
18返回knownProviders.next()。getValue();
19返回lookupiterator . next();//获取迭代器中延迟加载的下一个元素
20 }
21
22公共void remove(){
23抛出新的UnsupportedOperationException();
24 }
25 };
20}然后我们调用上一步中获得的迭代器的hasNext()方法。因为我们实际上在ServiceLoader.load()的过程中清除了providers列表中的缓存服务实现,所以我们实际上调用了lookupIterator.hasNext()方法,如下所示:
1个公共布尔hasNext(){
2 if (nextName!=null) {//下一个元素存在
3返回true
4 }
5(configs==null){//配置文件为空。
6尝试{
7 String fullName=前缀service . getname();//获取配置文件路径
8 if (loader==null)
9 configs=class loader . get system resources(全名);
10其他
11 configs=loader.getResources(全名);//加载配置文件
12 } catch (IOException x) {
13失败(服务,“定位配置文件时出错”,x);
14 }
15 }
16 //遍历配置文件的内容
17 while ((pending==null) !pending.hasNext()) {
18如果(!configs.hasMoreElements()) {
19返回假;
20 }
21 pending=parse(service,configs . nextelement());//配置文件的内容解析
22 }
23 next name=pending . next();//获取服务实现类的完整路径名
24返回true
25 }
26如果上面的判断为真,那么我们调用迭代器it的next()方法。同样,我们也调用lookupIterator.next()方法。源代码如下:
1 public S next(){
2如果(!hasNext()) {
3抛出新的NoSuchElementException();
4 }
5字符串cn=nextName//文件中保存的服务接口实现类的完整路径名
6 nextName=null
7类?c=空;
8尝试{
9 //获取完全限定名的类对象
10 c=Class.forName(cn,false,loader);
11 } catch(ClassNotFoundException x){
12失败(找不到服务、“提供商”cn);
13 }
14 //判断实现类和服务接口是否属于同一类型。
15如果(!service.isAssignableFrom(c)) {
16失败(服务,“提供者”cn不是子类型);
17 }
18试{
9//通过反射生成服务接口的实现类,判断这个实例是否是接口的实现。
20s p=service . cast(c . new instance());
1//缓存服务接口的实现并返回
22 providers.put(cn,p);
23返回p;
24 } catch(可投掷x) {
25失败(服务,提供程序 cn 无法实例化,x);
26 }
27 throw new Error();//这不可能发生
28}其实spi实现的主要过程是:扫描类路径下所有jar包下的/META-INF/services/目录(也就是我们需要将服务接口的具体实现类暴露给这个目录。我们之前提到过需要在实现类的类路径下建立一个/META-INF/services/文件夹,这就是原因。),找到对应的文件,读取文件名找到对应的SPI接口,然后通过InputStream流读出文件内容,得到实现类的全路径名,并得到这个全路径名所代表的类对象,判断是否与服务接口是同一类型,然后通过反射生成服务接口的实现,保存在providers列表中备用。
SPI的这种设计方法为我们的应用扩展提供了极大的便利,但其缺点也是显而易见的。Java SPI遍历SPI的配置文件,并在查找扩展实现类时实例化所有实现类。假设实现类的初始化过程是消耗资源和时间的,但是你不能在你的代码中使用它,这导致了资源的浪费。所以Java SPI不能按需加载实现类。
另外,SPI机制在很多框架中都有应用:slf4j log框架和Spring框架的基本原理都是类似的反映。还有Dubbo框架也提供了同样的SPI扩展机制,但是Dubbo和spring框架中SPI机制的具体实现和我们今天所学的略有不同(**Dubbo可以实现按需加载实现类* *),但是总体原理是一样的。今天先简单了解一下SPI,相信以今天的基础也不难理解剩下的。
版权归作者所有:原创作品来自博主小二上九8,转载请联系作者取得转载授权,否则将追究法律责任。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。