Java中的SPI基础

什么是SPI

  SPI全称为Service Provider Interface,它是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制。比如,当一个接口想要运行时动态的给它添加实现,只需要添加一个实现。经常遇到的就是java.sql.Driver接口,不同的厂商针对同一接口进行不同的实现,而Java的SPI机制可以为某个接口寻找服务实现。Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

  类图中,接口对应定义的抽象SPI接口;实现方实现SPI接口;调用方依赖SPI接口。
  SPI接口的定义在调用方。实现位于独立的包中。

Java SPI应用实例

  当服务的提供者提供了一种接口的实现后,需要在classpath的META-INF/services目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以 通过查找这个jar包(一般都是以jar包作为依赖)的META-INF/services中的配置文件,配置文件中有接口具体的实现类名,可以根据这个类型进行实例化加载,就可以使用该服务了。JDK中查找服务实现的工具类是:java.util.ServiceLoader

SPI的用途

  数据库DriverManager、Spring、ConfigurableBeanFactory等都是用到了SPI机制,以数据库DriverManager为例。
  DriverManager是jdbc里管理和注册不同数据库Driver的工具类,针对一个数据库,可能会存在这不同的数据库驱动的实现。在使用特定的驱动时,不希望修改现有的代码,希望通过一个简单的配置就可以达到效果。在使用MySQL驱动时,使用Class.forName(“com.mysql.jdbc.Driver”)加载MySQL驱动,就会执行其中的静态代码把driver注册到DriverManager中,以便后续的使用。
在JDBC4.0以后,不在需要使用Class.forName来加载驱动,直接获取连接即可,使用了Java的SPI扩展机制来实现。在Java中定义了java.sql.Driver接口,并没有具体的实现,具体的实现是有不同的厂商进行实现的。
  在mysql-connector-java-5.1.45.jar中,META-INF/services目录下有一个名为java.sql.Driver的文件:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

  在postgresql-42.2.2.jar中,META-INF/services目录下有一个名为java.sql.Driver的文件:

org.postgresql.Driver

  在MySQL中获取数据库连接Connection的过程

Connection conn = DriverManager.getConnection(url, username, password);

  不需要使用Class.forName(“com.mysql.jdbc.Driver”)加载MySQL驱动

MySQL DriverManager的实现

  MySQL中的DriverManager使用了Java中的SPI扩展机制来查找相关的驱动。在DriverManager中有一个静态代码块,如下:

static {
	loadInitialDrivers();
	println("JDBC DriverManager initialized");
}

  在静态代码块中有一个loadInitialDrivers方法,loadInitialDrivers方法中用到了上文中提到的SPI工具类ServiceLoader

    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;
    }

  遍历使用SPI获取到的具体实现,实例化各个实现类。在遍历的时候,首先调用driversInterator.next()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字。

ServiceLoader原理解析

    // 特定的读取位置
    private static final String PREFIX = "META-INF/services/";

    // 代表被加载的类或者接口
    private final Class<S> service;

    // 用于定位,加载和实例化providers的类加载器
    private final ClassLoader loader;

    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;

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

    // 懒查询迭代器
    private LazyIterator lookupIterator;

  简单梳理一下ServiceLoader的源码,实现流程如下:

  1. 应用程序调用ServiceLoader.load方法,ServiceLoader.load方法会创建一个新的ServiceLoader实例,并实例化该类的成员变量。包括service、loader、acc、lookupIterator,并清除provider中已经缓存的内容。
  2. 应用程序通过迭代器接口获取Iterator实例,先判断成员变量providers对象中LinkedHashMap<String, S>是否已经缓存了实例对象,如果已经存在缓存,则直接返回。没有缓存,则执行类加载过程,具体实现如下:
    • 读取META-INF/services下的配置文件,获取所有能够被实例化的类的名称,ServiceLoader可以跨越jar包读取META-INF下的配置文件。
        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);
        }
    
    • 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
    • 把实例化后的对象缓存至providers中,然后返回实例对象
                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
    

总结

  SPI机制在实际开发中的使用场景很多。特别是统一标准的不同厂商的实现,当有关组织或者公司定义标准后,具体厂商或框架开发者开发实现,之后提供给开发者使用。

  • 优点:
    • 使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或者替换框架组件。
  • 缺点
    • 虽然ServiceLoader也算是使用的延迟加载,但是基本上只能通过遍历全部获取,也就是接口的实现类全部加载并实例化了一遍。无论想不想用某些实现类,它也被加载并实例化了,这就造成了浪费。
    • 获取某一个实现类的方法不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
    • 多个并发多线程使用ServiceLoader类实例是不安全的。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值