JDK-SPI机制

1 篇文章 0 订阅

JDK-SPI机制

#博学谷IT学习技术支持#

一、SPI概述

SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件。
Java SPI是一种以接口为基础,使用配置文件来加载(或称之为服务发现)的动态加载机制,主要使用JDK中ServiceLoader来实现。

在此引用一张随处可见的图
在这里插入图片描述

二、与API的不同之处

API

API(Application Programming Interface)的主要作用在于为调用方提供某个功能实现的调用入口,调用方不需关心该API的实现方式如何,它只需知道API可以提供特定的服务功能即可,具体实现由实现方负责。API提供特定的功能接口,以供调用;一般被应用开发人员使用,由实现方实现。

SPI

SPI(Service Provider Interface)主要作用在于为同一服务的提供不同实现的替换机制,服务可以由第三方提供实现,也可以由调用方提供实现。SPI提供特定的功能接口,以供实现;一般被框架开发人员使用,调用方与实现方都可提供SPI的实现。

三、使用案例

SPI的使用比较简单,前面已经说过是基于ServiceLoader来实现,可以来看下源码
首先找到ServiceLoader中的静态方法:load()

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

根据指引,会发现new了一个ServiceLoader的实例,并执行reload()

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
    reload();
}

在reload中创建了一个懒加载迭代器

public void reload() {
    providers.clear();
    // 懒加载迭代器
    lookupIterator = new LazyIterator(service, loader);
}

当这个迭代器被调用hasNext时,跟着代码走下去,会发现有个令人在意的PREFIX

// 迭代方法
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 由PREFIX + name获取了文件全路径
            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;
}

再点击这个PREFIX会发现,它指向了相对路径下的META-INF/services/

private static final String PREFIX = "META-INF/services/";

也就是说,在Iterator遍历时,通过PREFIX + service.getName(),使用ClassLoader.getSystemResources(fullName)获取了配置里的内容,然后通过读文件的方式加载到自己的缓存,然后再调用迭代器的next()时,就会将其实例化

public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

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
}

因此我们可以得出结论,使用SPI的步骤为
1.创建一个接口
2.在classPath下创建路径META-INF/services/
3.以接口的全路径做为名称,创建文件(注意不要带文件格式后缀)
4.在文件中写入实现类的全路径
5.在代码中调用ServiceLoader.load(接口.class)获取迭代器
6.遍历迭代器即可获取对应实现类的实例

三、一些框架的使用场景

在大部分框架中,SPI的使用是较为常见的,比如Mysql的com.mysql.jdbc.Driver、SpringMVC的org.springframework.web.SpringServletContainerInitializer,当然这些框架使用SPI的方法有些不同
比如com.mysql.jdbc.Driver,是由静态方法注入进DriverManager的

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

这样做有点绕,触发顺序为:
1.DriverManager在静态块中执行方法loadInitialDrivers()
2.loadInitialDrivers()中会有地方调用ServiceLoader.load(Driver.class)
3.触发hasNext()时,由于com.mysql.jdbc.Driver被ClassLoder扫到于是执行静态块,实例化一个自己并注册到DriverManager(这一块并不是很明白,因为迭代器也调用了next(),也就是说com.mysql.jdbc.Driver被创建了两个实例,不过loadInitialDrivers()中调用的ServiceLoader.load(Driver.class)并没有被加到注册,或许是因为ServiceLoader本身不是线程安全的所以才这样设计的,之后有时间去Oracle的驱动包验证一下自己的看法)

SpringMVC的则是实现了javax.servlet.ServletContainerInitializer,并没有像com.mysql.jdbc.Driver由静态方法注入


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值