我正在参与「启航方案」

界说

SPI(Service Provider Interface) 是一种面向接口编程的技能,它能够让一个程序根据接口约好规范主动发现和加载对应的完成类。它是一种 Java 种的接口编程规范,它界说了接口和服务供给者之间的约好规范,使得在运行时动态加载完成该接口的类。SPI 机制是经过在服务供给者接口上界说注解和在装备文件种指定完成类的办法来完成的。

如何完成

当服务的供给者供给了一种接口的完成之后,需求在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容便是这个接口的详细完成类。当其他程序需求这个服务的时分,就会找到这个类而且实例化。JDK 中查找服务的完成的工具类是:java.util.ServiceLoader。

简单demo完成

界说一个接口,供给两个完成类:

public interface Plugin {
    void execute();
}
public class PluginA implements Plugin {
    @Override
    public void execute() {
        System.out.println("PluginA.execute is done");
    }
}
public class PluginB implements Plugin {
    @Override
    public void execute() {
        System.out.println("PluginB.execute is done");
    }
}

装备好文件:

什么是 SPI 机制

测试:

public class PluginTest {
    public static void main(String[] args) {
        ServiceLoader<Plugin> serviceLoader=ServiceLoader.load(Plugin.class);
        Iterator<Plugin> itre =  serviceLoader.iterator();
        while(itre.hasNext()){
            //itre.next() 这行代码会对 SPI 装备的完成类进行初始化
            itre.next().execute();
        }
    }
}
//打印结果:
//PluginA.execute is done
//PluginB.execute is done

扩展

许多功用都运用了 SPI 机制:比方 JDBC DriverManger 、Spring 、MyBatis、log 日志、Dubbo

下面简单介绍几种。

JDBC DriverManager

最开端的时分,咱们衔接数据库的时分,需求先加载驱动:Class.forName(“com.mysql.cj.jdbc.Driver”),然后再是获取衔接的操作。JDBC4.0 之后引入了 SPI 就不需求手动加载驱动了,直接获取衔接即可。

String url = "jdbc:mysql://localhost:3306/xxx?useUnicode=true";
Connection connection = DriverManager.getConnection(url, "root", "root");
System.out.println(connection);

依照上面界说的规范,能够看看 mysql jar 包中是否有对应的装备:

什么是 SPI 机制

在 META-INF/services/ 目录下有一个服务接口命名的文件(java.sql.Driver)以及里边的内容是 MySQL jar 包里边的详细完成类。

DriverManager 中的静态代码块:里边会有 ServiceLoader.load(Driver.class); 办法去加载装备类的,在循环的时分就会实例化装备的类:driversIterator.next(); 这儿才会真实实例化。

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

下面是 ServiceLoader#nextService() 办法,里边会加载类 Class.forName() 而且下面会调用 newInstance() 去初始化。

  private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                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
        }

Spring 中 SPI 机制

在 SpringBoot 主动装配的过程中,最终会加载 META-INF/spring.factories 文件,加载过程是由 SpringFactoriesLoader 完成。

整个代码便是去找到 spring.factories 文件,解析里边的内容,最终将装备的完成类实例化,并回来一个 List。

MyBatis 中的插件

MyBatis 中的插件是运用 SPI 机制思维,不是运用的 JDK SPI 。MyBatis 专门独立一个模块 plugin 来完成插件的扩展。MyBatis 里边只有四个接口可扩展:Executor、ParameterHandler、ResultSetHandler、StatementHandler 。如果要完成扩展一个 MyBatis 接口需求做以下作业:

  • 完成 Interceptor 接口,而且需求经过 @Intercepts 和 @Signature 接口指定阻拦什么接口的什么办法
  • 在装备文件中装备 标签,声明: 这样MyBatis 初始化的时分就会加载到 Configuration (MyBatis 装备目标)中。

当 MyBatis 履行 SQL 时,会依照插件的装备顺序依次调用插件的 intercept() 办法来对 SQL 语句进行处理,从而完成对 SQL 的阻拦和增强。

SPI 运用

  • 安排或许公司界说规范
  • 厂商各自完成
  • 开发者调用

比方便是 Java 界说 Driver 接口,MySQL 厂商完成 mysql.Driver 完成类,咱们经过 DriverManager 获取 mysql 的衔接。

总结

优点:SPI 核心思维便是解耦。我只界说规范,详细完成由不同的厂商完成。

缺陷:

  • 不能按需加载,有必要遍历一切完成并初始化,可是有点初始化可能会很耗时
  • 获取某个完成类的办法不行灵活,只能遍历获取
  • 多线程运用 ServiceLoader 不安全