zoukankan      html  css  js  c++  java
  • JDK中的SPI机制

    前言

    最近学习类加载的过程中,了解到JDK提供给我们的一个可扩展的接口:java.util.ServiceLoader
    之前自己不了解这个机制,甚是惭愧...

    什么是SPI

    SPI全称为(Service Provider Interface),是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。

    首先放个图:我们在“调用方”和“实现方”之间需要引入“接口”,可以思考一下什么情况应该把接口放入调用方,什么时候可以把接口归为实现方。

    先来看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:
    1.是对实现的说明(我可以给你提供什么)
    2.组织上位于实现方所在的包中
    3.实现和接口在一个包中

    当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:
    1.是对实现的约束(要提供这个功能,实现者需要做那些事情)
    2.组织上位于调用方所在的包中
    3.实现位于独立的包中(也可认为在提供方中)

    简而言之
    API会告诉您特定的类/方法为您执行什么操作,而SPI则告诉您必须执行哪些操作才能符合要求。通常,API和SPI是分开的。例如,在JDBC中,Driver类是SPI的一部分:如果只想使用JDBC,则不需要直接使用它,但是实现JDBC驱动程序的每个人都必须实现该类。但是,有时它们会重叠。Connection接口既是SPI,又是API:您在使用JDBC驱动程序时通常会使用它,并且需要由JDBC驱动程序的开发人员来实现。

    JDK SPI使用说明及示例

    要使用SPI比较简单,只需要按照以下几个步骤操作即可:

    1.在jar包的META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名
    2.接口实现类所在的jar包在classpath下
    3.主程序通过java.util.ServiceLoader动态状态实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
    4.SPI的实现类必须带一个无参构造方法

    举例1

    下例是使用maven引入了mysql的依赖后执行的,MySQL驱动内的截图:

    java.sql.Driver文件全部内容:

    com.mysql.jdbc.Driver
    com.mysql.fabric.jdbc.FabricMySQLDriver
    
    public class SpiTest {
    
        public static void main(String[] args) {
            ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
            Iterator<Driver> iterator = loader.iterator();
            while(iterator.hasNext()) {
                Driver driver = iterator.next();
                System.out.println("driver is " + driver.getClass() + ", classLoader is " + driver.getClass().getClassLoader());
            }
            System.out.println("当前上下文类加载器是:" + Thread.currentThread().getContextClassLoader());
            System.out.println("ServiceLoader的类加载器是:" + ServiceLoader.class.getClassLoader());
        }
    }
    

    运行结果:

    driver is class com.mysql.jdbc.Driver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
    driver is class com.mysql.fabric.jdbc.FabricMySQLDriver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
    当前上下文类加载器是:sun.misc.Launcher$AppClassLoader@18b4aac2
    ServiceLoader的类加载器是:null
    

    SPI相关的类加载的逻辑

    因为ServiceLoader位于java.util.ServiceLoader,所以这个类是会被启动类加载器所加载,然后我们分析一下ServiceLoader.load(Driver.class)的源码。
    根据类加载的原理:如果一个类由类加载器A加载,那么这个类的依赖类也会被类加载器A加载(前提是这个依赖类尚未被加载过)。
    当执行ServiceLoader.load(Driver.class),如果不使用线程上下文类加载器来打破双亲委托模型,那么该方法的关联类也会被启动类加载器加载。

    333    public static <S> ServiceLoader<S> load(Class<S> service) {
    334        ClassLoader cl = Thread.currentThread().getContextClassLoader();
    335        return ServiceLoader.load(service, cl);
    336    }
    

    所以我们看到334行获取了线程上下文类加载器,然后调用另一个重载的load方法去加载Driver(即所谓的Service)。
    继续跟踪ServiceLoader.load(service, cl)的代码,会发现它依次执行了如下动作:
    1.初始化了一个:ServiceLoader对象,new ServiceLoader<>(service, loader)
    2.执行reload()
    3.new LazyIterator(service, loader)
    这里所有的loader都是线程上下文类加载器,它默认为系统类加载器。

    这时,SpiTest中的ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);就执行完了,
    跟着执行SpiTest中的Iterator<Driver> iterator = loader.iterator();
    这里当执行iterator.hasNext()的时候,就会进入到刚才初始化的LazyIterator类中,执行其中的下列方法,
    所以这也是为什么"在jar包的META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名"

            private boolean hasNextService() {
                if (nextName != null) {
                    return true;
                }
                if (configs == null) {
                    try {
                        //这里的PREFIX是一个常量:META-INF/services/
                        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;
            }
    

    仔细阅读内部类LazyIterator类的源码,就可以知道:
    1.JDK是怎么读取META-INF/services目录下的内容
    2.JDK是怎么加载这些SPI的类的

    JDK SPI的不足

    JDK SPI的使用很简单。也做到了基本的加载扩展点的功能。但JDK SPI有以下的不足:
    1.需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。
    2.配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
    3.扩展如果依赖其他的扩展,做不到自动注入和装配
    4.不提供类似于Spring的IOC和AOP功能
    5.扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的JDK SPI不支持

  • 相关阅读:
    通过C#代码调用Dynamics 365 Web API执行批量操作
    Dynamics 365 CE Update消息PostOperation阶段Image的尝试
    sql注入100种姿势过waf(二):过安全狗
    sql注入100种姿势过waf(一):waf 了解
    双文件上传突破利用
    渗透实例(一):点后缀突破上传文件
    IIS6.0使用冒号上传漏洞利用
    利用3389端口监视管理员登录
    Windows突破远程连接最大数去掉限制登录
    MIME格式解析
  • 原文地址:https://www.cnblogs.com/1626ace/p/13779327.html
Copyright © 2011-2022 走看看