在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则"。所以我们一般有两种选择:一种是使用API(Application Programming Interface),另一种是SPI(Service Provider Interface),API通常被应用程序开发人员使用,而SPI通常被框架扩展人员使用。

在进入下面学习之前,我们先来再加深一下API和SPI这两个的印象:

API:由实现方制定接口标准并完成对接口的不同实现,这种模式服务接口从概念上更接近于实现方;

SPI:由调用方制定接口标准,实现方来针对接口提供不同的实现;从前半句话我们来看,SPI其实就是"为接口查找实现"的一种服务发现机制;这种模式,服务接口组织上位于调用方所在的包中,实现位于独立的包中。

API和SPI简略图示:

  

看完上面的简单图示,相信大家对API和SPI的区别有了一个大致的了解,现在我们使用SPI机制来实现我们一个简单的日志框架:

第一步,创建一个maven项目命名为spi-interface,定义一个SPI对外服务接口,用来后续提供给调用者使用;

package cn.com.wwh;
/**
 * 
 * @FileName Logger.java
 * @version:1.0
 * @Description: 服务提供者接口
 * @author: wwh
 * @date: 2022年9月19日 上午10:31:53
 */
public interface Logger {
    
    /**
     * 
     * @Description:(功能描述)
     * @param msg
     */
    public void info(String msg);
    
    /**
     * 
     * @Description:(功能描述)
     * @param msg
     */
    public void debug(String msg);
}
package cn.com.wwh;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

/**
 * 
 * @FileName LoggerService.java
 * @version:1.0
 * @Description: 为服务的调用者提供特定的功能,是SPI的核心功能
 * @author: wwh
 * @date: 2022年9月19日 上午10:33:30
 */
public class LoggerService {

    private static final LoggerService INSTANCE = new LoggerService();

    private final Logger logger;                          

    private final List<Logger> loggers = new ArrayList<>();

    private LoggerService() {
     //ServiceLoader是实现SPI的核心类
        ServiceLoader<Logger> sl = ServiceLoader.load(Logger.class);
        Iterator<Logger> it = sl.iterator();
        while (it.hasNext()) {
            loggers.add(it.next());
        }

        if (!loggers.isEmpty()) {
            logger = loggers.get(0);
        } else {
            logger = null;
        }
    }

    /**
     * @Description:(功能描述)
     * @return
     */
    public static LoggerService getLoggerService() {
        return INSTANCE;
    }

    /**
     * 
     * @Description:(功能描述)
     * @param msg
     */
    public void info(String msg) {
        if (logger == null) {
            System.err.println("在info方法中没有找到Logger的实现类...");
        } else {
            logger.info(msg);
        }
    }

    /**
     * 
     * @Description:(功能描述)
     * @param msg
     */
    public void debug(String msg) {
        if (logger == null) {
            System.err.println("在debug方法中没有找到Logger的实现类...");
        } else {
            logger.info(msg);
        }
    }
}

将上面这个这个项目打成spi-interface.jar包。

第二步,新建一个maven项目并导入第一步中打出来的spi-interface.jar包,这个项目用来提供服务的实现,定义一个类,实现第一步中定义的cn.com.wwh.Logger接口,示例代码如下:

package cn.com.wwh;

import cn.com.pep.Logger;

/**
 * 
 * @FileName Logback.java
 * @version:1.0
 * @Description: 服务接口的实现类
 * @author: wwh
 * @date: 2022年9月19日 上午10:50:31
 */
public class Logback implements Logger {

    @Override
    public void debug(String msg) {
        System.err.println("调用Logback的debug方法,输出的日志为:"   msg);
    }

    @Override
    public void info(String msg) {
        System.err.println("调用Logback的info方法,输出的日志为:"   msg);
    }

}

同时在当前项目的classpath路径下建立META-INF/services/文件夹(至于为什么这么建立目录,我们一会儿再解释),并且新建一个名称为cn.com.wwh.Logger内容为cn.com.wwh.Logback的文件,这一步是关键(具体作用后面再详细说明),然后将上面第二步这个这个项目打成spi-provider.jar包,供给之后使用,我目前使用的开发工具是Eclipse,目录结构如下图所示:

第三步,编写测试类,新建一个maven项目,命名为spi-test,导入前面两个步骤打的spi-interface.jar和spi-provider.jar这两个jar包,并编写测试代码,示例如下:

package cn.com.wwh;

import cn.com.pep.LoggerService;

/**
 * 
 * @FileName SpiTest.java
 * @version:1.0
 * @Description: 
 * @author: wwh
 * @date: 2022年9月19日 上午10:56:31
 */
public class SpiTest {
    
    public static void main(String[] args) {
        LoggerService logger = LoggerService.getLoggerService();
        logger.info("我是中国人");
        logger.debug("白菜多少钱一斤");
    }
}

有了SPI我们可以将服务和服务提供者轻松地解耦,假如将来的某一天我们需要将日志保存到数据库,或者通过网络发送,我们直接只需要替换针对服务接口的实现类即可,别的地方都不用修改,这更符合程序设计中的“开闭原则”。

SPI的大致原理是:应用启动的时候,扫描classpath下面的所有jar包,将jar包下的/META-INF/services/目录下的文件加载到内存中,进行一系列的解析(文件的名称是spi接口的全路径名称,文件内容应该是spi接口实现类的全路径名,可以用多个实现类,在文件中换行保存),之后判断当前类和当前接口是否是同一类型?结果为true,则通过反射生成指定类的实例对象,保存到一个map集合中,可以通过遍历或者迭代的方式拿出来使用。

SPI实质就是一个加载服务实现的工具,核心类是ServiceLoader,其实了解了SPI的原理,我们再接着探究JDK中的源码就没有那么费力了,下面我们开始源码分析吧。

ServiceLoader类是定义在java.util包下的,使用final定义禁止子类继承和修改,实现了Iterable接口,使得可以通过迭代或者遍历的方式获取SPI接口的不同实现。

从上面的我们所举的例子中,我们知道SPI的入口是ServiceLoader.load(Class<S> service)方法,我们来看看它都干了什么?   

上面的这4步总的来说,就是使用指定的类型和当前线程绑定的classLoader实例化了一个LazyIterator对象赋值给lookupIterator这个引用,并且清除了原来providers列表中缓存的服务的实现。接下来我们调用了ServiceLoader实例的iterator()方法获取了一个迭代器,代码如下:

public Iterator<S> iterator() {
        //通过匿名内部类方式提供了一个迭代器
        return new Iterator<S>() {
            //获取缓存的服务实现者的迭代器
            Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator();

            //判断迭代器中是否还有元素
            public boolean hasNext() {
                //缓存的服务实现者的迭代器中已经没有元素了
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();//判断延迟加载的迭代器中是否还有元素
            }

            //获取迭代其中的下一个元素
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();//获取延迟加载的迭代器中的下一个元素
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
   }

我们接着调用上步获取的迭代器it的hasNext()方法,因为我们在ServiceLoader.load()过程中其实是清除了providers列表中的缓存服务实现的,所以其实调用的是lookupIterator.hasNext()方法,如下:

public boolean hasNext() {
        if (nextName != null) {//存在下一个元素
            return true;
        }
        if (configs == null) {//配置文件为空
            try {
                String fullName = PREFIX   service.getName();//获取配置文件路径
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);//加载配置文件
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        //遍历配置文件内容
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());//配置文件内容解析
        }
        nextName = pending.next();//获取服务实现类的全路径名
        return true;
    }

假如上部判断为true,紧接着我们又调用了迭代器it的next()方式,同理也调用的是lookupIterator.next()方法,源码如下:

public S next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        String cn = nextName;//文件中保存的服务接口实现类的全路径名
        nextName = null;
        Class<?> c = null;
        try {
            //获取全限定名的Class对象
            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);
            return p;
        } catch (Throwable x) {
            fail(service, "Provider "   cn   " could not be instantiated", x);
        }
        throw new Error(); // This cannot happen
    }

其实spi实现的主要流程是:扫描classpath路径下的所有jar包下的/META-INF/services/目录(即我们需要将服务接口的具体实现类暴露在这个目录下,之前我们提到需要在实现类的classpath下面建立一个/META-INF/services/文件夹就是这个原因。),找到对应的文件,读取这个文件名找到对应的SPI接口,然后通过InputStream流将文件内容读出来,获取到实现类的全路径名,并得到这个全路径名所表示的Class对象,判断其与服务接口是否是同一类型,然后通过反射生成服务接口的实现,并保存在providers列表中,供给后续的使用。

SPI这种设计方式为我们的应用扩展提供了极大的便利,但是它的短板也是显而易见的,Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。所以说 Java SPI 无法按需加载实现类。

另外,SPI 机制在很多框架中都有应用:slf4j日志框架、Spring 框架的基本原理也是类似的反射。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别(Dubbo可以实现按需加载实现类),不过整体的原理都是一致的,我们今天先对SPI有个简单的了解,相信有了今天的基础理解剩下的那几个也不是什么难事。

好了,今天就到这儿了,文章中有说的不对的地方还请各位大佬批评指正,一起学习,共同进步,谢谢。

到此这篇关于浅析Java中的SPI原理的文章就介绍到这了,更多相关Java SPI原理内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

浅析Java中的SPI原理的更多相关文章

  1. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  2. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  3. VUE响应式原理的实现详解

    这篇文章主要为大家详细介绍了VUE响应式原理的实现,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助

  4. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  5. Java实现世界上最快的排序算法Timsort的示例代码

    Timsort 是一个混合、稳定的排序算法,简单来说就是归并排序和二分插入排序算法的混合体,号称世界上最好的排序算法。本文将详解Timsort算法是定义与实现,需要的可以参考一下

  6. Java日期工具类的封装详解

    在日常的开发中,我们难免会对日期格式化,对日期进行计算,对日期进行校验,为了避免重复写这些琐碎的逻辑,我这里封装了一个日期工具类,方便以后使用,直接复制代码到项目中即可使用,需要的可以参考一下

  7. Java设计模式之模板方法模式Template Method Pattern详解

    在我们实际开发中,如果一个方法极其复杂时,如果我们将所有的逻辑写在一个方法中,那维护起来就很困难,要替换某些步骤时都要重新写,这样代码的扩展性就很差,当遇到这种情况就要考虑今天的主角——模板方法模式

  8. Java 中 Class Path 和 Package的使用详解

    这篇文章主要介绍了Java 中 Class Path和Package的使用详解,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下

  9. java SpringBoot 分布式事务的解决方案(JTA+Atomic+多数据源)

    这篇文章主要介绍了java SpringBoot 分布式事务的解决方案(JTA+Atomic+多数据源),文章围绕主题展开详细的内容介绍,具有一定的参考价值,感兴趣的小伙伴可以参考一下

  10. 五分钟理解keep alive用法及原理

    这篇文章主要为大家介绍了keep alive用法及原理示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部