1.什么是SPI

动态服务发现机制。它的主要实现是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制。

Java SPI机制,图解如下:

java spi机制和原理 spi java源码解析_java spi机制和原理

2.SPI机制设计思想       

       系统设计的各个抽象,往往有很多不同的实现方案,在面向对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

      Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦

3.Java SPI使用场景

    1.JDBC数据库驱动实现

       JDBC4.0以前, 开发人员还需要基于Class.forName("xxx")的方式来装载驱动;JDBC4.0也是基于Java SPI机制来发现驱动服务,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.

    2.common-logging 日志实现统一接口

       Apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现,发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提供商的实现类。只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory工厂接口的实现类即可。 

    3.Dubbo 框架很多组件使用到SPI机制

       Dubbo框架中大量使用了SPI技术,不过它对Java提供的原生SPI做了封装,并不是又有JDK的实现,但是它们的思想仍然是一样的。里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现。方便了接口的各种实现灵活应用。例如:Dubbo中发布不同协议的Protocol接口

4.Java SPI使用规范

     遵循"约定优于配置"原则,要使用Java SPI,需要遵循如下约定:

     1.当服务提供者提供了接口的一种具体实现后,在resources/META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名;

     2.主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

     3.如果SPI的实现类为Jar包,则需要放在主程序的ClassPath路径中

     4.接口的具体实现类,必须有一个无参构造方法

5.Java SPI示例

   本例以JDBC为Demo,来介绍Java SPI的具体实现。

  ①定义一组接口,作为官方提供的门面接口,只有接口,没有实现。具体方案由各提供商实现

package com.demo.spi;

public interface DataBaseDriver {
    //数据库驱动接口规范
    public void connect(String host);
}

   ②MySql数据库厂商,实现该接口(如果使用Maven,则需要依赖官方提供的接口。这就是为什么我们引入log4j包时,为什么必须引入common-logging包,因为common-log包是提供的接口包,log4j是具体的厂商实现包)

<dependency>
    <groupId>com.demo.spi</groupId>
    <artifactId>DataBaseDriver</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
package com.demo.spi;

public class MySqlDriver implements DataBaseDriver {
    /**
     * MySql实现数据库规范
     */
    public void connect(String host) {
        System.out.println("This is MySql DataBase impl,connect to " + host);
    }
}

   ③约定实现。在resources目录下,新建META-INF/services目录。然后新建一个以"接口全限定名"为文件名的文件,本例为com.demo.spi.DataBaseDriver;文件内容为实现类的全限定名,本例为com.dmeo.spi.MySqlDriver

java spi机制和原理 spi java源码解析_加载_02

    ④在另一个新的工程中,如果需要连接数据库MySQL,则引入MySQL的Maven依赖,使用 ServiceLoader 类来加载配置文件中指定的实现

<dependency>
    <groupId>com.demo.spi</groupId>
    <artifactId>MySQLDriver</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
public class DataBaseConnect {
    public static void main(String[] args) {
        //ServiceLoader,通过反射获取对应类实例
        ServiceLoader<DataBaseDriver> serviceLoader = ServiceLoader.load(DataBaseDriver.class);
        
        for(DataBaseDriver driver : serviceLoader){
            driver.connect("127.0.0.1");
        }
    }
}

    输出结果:

This is MySql DataBase impl,connect to 127.0.0.1

    ⑤如需使用OracleDriver,只需要将Maven依赖修改,即可在不改变代码的情况下,由MySql实现替换为Oracle实现

6.ServiceLoader类源码解析

      SPI机制,主要用到ServiceLoader这个类,ServiceLoader通过读取resources/META-INF/services/com.xxx.xxx.xxxService文件下的xxxService的spi实现类,通过反射获取对应类实例,并调用对应方法。

1.我们先瞅瞅一下ServiceLoader类

       我们可以看到,该类中几个变量PREFIX、service、loader、acc、providers、lookupIterator,还有一些方法,我们也可以看到我们使用的ServiceLoader.load()方法,该方法是一个重载方法,不同区别在于使用自定义的或者是默认的ClassLoader。

java spi机制和原理 spi java源码解析_java spi机制和原理_03

public final class ServiceLoader<S> implements Iterable<S>{

    //约定文件路径
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    // 代表被加载的类或者接口
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    // 用于定位,加载和实例化providers的类加载器
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    // 懒查找迭代器
    private LazyIterator lookupIterator;

    ......

}

2.我们来分析ServiceLoader的load()方法

//步骤1.
public static <S> ServiceLoader<S> load(Class<S> service) {
    //根据上下文信息,来获取ClassLoader加载类
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    //执行load方法
    return ServiceLoader.load(service, cl);
}
//步骤2.
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
    //重新new一个带ClassLoader的ServiceLoader类
    return new ServiceLoader<>(service, loader);
}
//步骤3.完成ServiceLoader类一些变量的初始化操作
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    //ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载,
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

3.通过迭代器接口,来获取对象实例

       读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,会执行hasNextService()方法。然后再执行hasNext()方法,最后执行nextService()方法,通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。

//步骤1.
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //获取实现类全路径(eg:META-INF/services/com.demo.spi.DataBaseDriver)
            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;
}
//步骤2.
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<
            //执行hasNextService()方法
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}
//步骤3.
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //通过反射方法Class.forName()加载类对象
        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 {
        //使用instance()方法将类实例化
        S p = service.cast(c.newInstance());
        //把实例化后的类缓存到providers对象中(LinkedHashMap<String,S>类型),然后返回实例对象
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,"Provider " + cn + " could not be instantiated",x);
    }
    throw new Error();          // This cannot happen
}

7.总结

    优点

实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离。例如数据库驱动,更改数据库,只需修改Maven依赖或者替换引入的jar包即可。

    缺点:

        1.ServiceLoader加载,只能通过遍历获取,导致获取实现类的方式不灵活,只能用过Iterator形式来获取,不能根据指定的需求来获取需要的实现类,有点局限;

        2.多线程并发编程中,使用ServiceLoader类来加载实例,线程不安全。

END