Java SPI

1,651 阅读7分钟

为了更好的理解Dubbo SPI机制,我们首先应该去了解下Java本身自带的SPI机制到底是个什么东西。明白了这个之后,那么Dubbo自己实现的SPI机制无非就是提供了更好的解决方案,以及一些额外的功能而已。其核心目的都是大同小异的。

1.1.那么SPI机制到底是什么呢?

先粘一段官方点的描述:SPI全称Service Provider Interface,从Java6开始被引入,是一种基于ClassLoader来发现并加载服务的机制。

这里我来简单描述下,SPI实际上就是JDK提供一个抽象规范,这里可以简单理解为接口。之后用户可以根据自己的需要来进行定制化实现并且在特定目录下配置好实现的入口,最后通过ClassLoader来进行加载、执行配置文件中配置的对应实现类。以此来达到灵活拓展或者替换对应的实现。类似于插件。常用于数据库框架,日志框架等...

1.2.这里SPI机制解决了什么问题?

这里就以比较常见的数据库驱动来举例吧,因为市面上有很多不同数据库的厂商。我们知道连接数据库需要数据库驱动,如果任由其各自实现,那么就会五花八门难以进行统一的管理。所以我们需要指定一个统一的规范,各厂商得按照这个规范来开发。

再来想想我们最开始学习JDBC的时候是如何加载数据库驱动的,没记错的话是通过”Class.forName("com.mysql.cj.jdbc.Driver");“来进行加载的吧,那么有没有什么办法能不写这种死代码呢?很简单,就是利用SPI机制呀~所以,SPI机制也给我们带来了便利性,我们只需要引入对应数据库的jar包就可以直接获取数据库的连接了。就像这样”Connection connection = DriverManager.getConnection("url");“,是不是很方便。

其实这就是类似于插件的形式来进行解耦。

1.3.接下来看下SPI是如何实现的?

1.3.1.Java SPI都有哪几个重要的组成:

  1. 接口
  2. 对应的实现类
  3. META-INF.services下的配置文件
  4. ServiceLoader(重中之重)

接口和对应的实现类就不用说了吧,这是最基本的。META-INF.services下的配置文件,这里为什么是这个路径下呢?并且名字和内容有什么要求吗,这些答案都在ServiceLoader中,并且ServiceLoader负责着解析加载类的功能,so~这是我们关注的重点。

ServiceLoader的大体来讲主要流程如下:

1.3.2.ServiceLoader源码分析

既然ServiceLoader的是比较重要的,那么我们就简单来看一下。以一个小demo开始~

public static void main(String[] args) throws ClassNotFoundException, SQLException {
    // 获取ServiceLoader
    ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
    Iterator<Driver> iterator = load.iterator();
    // 解析配置文件中的类
    if (iterator.hasNext()) {
        // 加载
        iterator.next();
    }
}
ServiceLoader.load(Driver.class);

其实这几个方法比较简单实际上就是去创建一个属于Driver接口的一个ServiceLoader

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器ContextClassLoader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // ...
}

下面迭代器中的方法就是解析与加载的逻辑

// 缓存
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            // 如果缓存中存在,那么返回true
            if (knownProviders.hasNext())
                return true;
            // 否则就要去进行查询
            return lookupIterator.hasNext();
        }

        public S next() {
            // 这里去缓存中查询是否有具体的实例,有就返回
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            // 没有的话依旧得去进行查询
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}
lookupIterator.hasNext()

这个方法主要是负责加载、解析出配置文件,具体代码就不进行分析了,感兴趣的可以自行查看

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 {
            // private static final String PREFIX = "META-INF/services/";
            // 这里就解释了为什么配置文件要配置在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;
}
lookupIterator.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();
    // 这里就是hasNext中解析出来的名字
    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
}

1.4.MySQL驱动中的运用

首先,JDK提供了一个java.sql.Driver接口,不同厂商就会对这个Driver接口来进行实现。这里就以MySQL来举例了

有了接口以及对应的实现了,那么接下来就该看下是如何找到他俩之间的关系,并且加载到JVM中最后执行这个实现类。我们打开MySQL jar包下的META-INF.services目录,可以看到java.sql.Driver文件中配置着mysql驱动的入口类。

接着我们看下如,接下来我们看下如何加载的MySQL驱动,首先在DriverManager类中,有一个静态代码块,因为DriverManager类是在rt.jar下的所以随着JVM启动就会去加载DriverManager,之后便会执行静态代码块中的代码,正是在这里通过了ServiceLoader来进行加载的,因为前面已经分析过了,这里不再赘述。

static {
    // 这里来加载数据库驱动
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    // ...
    // 这里省略了些与SPI不相干的代码
    
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();

    try{
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
    } catch(Throwable t) {
        // Do nothing
    }
    return null;
    // ...
}

我们再来看下MySQL的实现。显而易见,其实就是会在DriverManager类中的类变量存入一份,就此我们就完成了数据库驱动的加载。

// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    // ...
    DriverManager.registerDriver(new Driver());
}

public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
    registerDriver(driver, null);
}

public static synchronized void registerDriver(java.sql.Driver driver,DriverAction da) throws SQLException {

    /* Register the driver if it has not already been added to our list */
    if(driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
    } else {
        // This is for compatibility with the original DriverManager
        throw new NullPointerException();
    }

    println("registerDriver: " + driver);

}

1.5.为什么打破了双亲委派机制

到这里补充一点,也是面试中常会被问到的一个问题,什么情况会打破双亲委派机制?

Java SPI机制就是一个很好的案例,首先Java核心的API(比如rt.jar包)是使用Bootstrap ClassLoader类加载器加载的,而用户提供的Jar包是由AppClassLoader加载的。如果一个类由类加载器加载,那么这个类依赖的类也是由相同的类加载器加载的。

用来搜索开发商提供的SPI 扩展实现类的API 类(ServiceLoader)是使用Bootstrap ClassLoader 加载的,那么ServiceLoader 里面依赖的类应该也是由Bootstrap ClassLoader加载的。而上面说了用户提供的包含SPI实现类的Jar包是由AppClassLoader加载的,所以这就需要一种违反双亲委派模型的方法,线程上下文类加载器ContextClassLoader就是用来解决这个问题的。

1.6.小结

说了这么多,我们应该也大致了解了Java SPI是做什么的了,不过Java SPI或多或少还是存在着一些缺点的。这也可能是Dubbo自己实现了一套SPI的原因吧

缺点:

  • JDK标准的SPI会一次性实例化扩展点的所有实现,如果有些扩展实现初始化很耗时,但又没用上,那么加载就很浪费资源。
  • 如果扩展点加载失败,是不会友好地向用户通知具体异常的。比如:对于JDK 标准的ScriptEngine来说,如果Ruby ScriptEngine因为所依赖的jruby.jar不存在,导致Ruby ScriptEngine 类加载失败,那么这个失败原因就被隐藏了,当用户执行Ruby脚本时,会报空指针异常,而不是报Ruby ScriptEngine不存在。