Java中的SPI与双亲委派模型

Posted by RussXia on April 15, 2021

什么是SPI机制

SPI全称是Service Provider Interface,由JDK提供接口定义,第三方实现或扩展API。

第三方在/META-INF/services下,以要实现的SPI接口为文件名,具体的实现类为文件内容,创建一个配置文件。

Java中ServiceLoader 会去加载配置文件,并加载对应的具体实现类。

如果同一个SPI接口有多个实现,可以通过ServiceLoader.iterator去遍历获取所有具体实现类。

在一个项目中,引入mysql-connector-java.jar实现,然后在自定义一个实现了 java.sql.Driver SPI接口的 com.mybatis.demo.spi.DriverImpl实现类,并在resources下,定义好/META-INF/services/java.sql.Driver文件,其内容为DriverImpl的全限定名,这样我们就有了针对Driver

public static void main(String[] args) {
  ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
  load.iterator().forEachRemaining(action -> {
    System.out.println(action.getClass());
  });
}

输出结果如下:

class com.mybatis.demo.spi.DriverImpl
class com.mysql.cj.jdbc.Driver

SPI实现类的类加载器是什么

ServiceLoader通过线程上下文获取加载实现类的classloader,一般情况下是 application classloader,当然也可以自定义class loader。

public static <S> ServiceLoader<S> load(Class<S> service) {
        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);
    }

Connect是jdk中定义的一个SPI接口,我们以mysql-connector-java.jar为具体实现类引入classpath下,然后建立连接,看看各个类对应的实际类加载器。

public static void main(String[] args) throws SQLException {
    Connection connection =
            DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useSSL=false", "root", "123456");

    System.out.println(connection.getClass().getClassLoader());
    System.out.println(connection.getClass().getClassLoader().getParent());
    System.out.println(connection.getClass().getClassLoader().getParent().getParent());
    System.out.println(java.sql.Connection.class.getClassLoader());
    System.out.println(String.class.getClassLoader());
}

输出结果

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3ab39c39
null
null
null

为什么应该输出的Bootstrap ClassLoader会输出为null呢,可以参考 Java API

Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.

大意就是有些实现,在获取class loader时,会用null代替获取bootstrap class loader。

SPI是否有破坏双亲委派模型

以Connection和mysql-connect-java.jar为例,Bootstrap本身就没有办法加载在classpath下的mysql-connector-java.jar,所以虽然Connection是定义在rt.jar中,且其本身由Bootstrap ClassLoader加载,但是具体的实现类,Bootstrap ClassLoader是无法加载的。

我的理解是,首先双亲委派模型,本身并不是强制的,双亲委派模型保证了”安全”(越基础的类由越基础的类加载器加载,且只会被加载一次),但是却不够灵活。

如果加载时拿到的class loader是application class loader,那么是没有破坏双亲委派模型的;但是如果拿到的是自定义的class loader,且自定义class loader没有遵守双亲委派模型,那么就SPI就破坏了双亲委派模型。

所以SPI给破坏双亲委派模型留了口子,但是具体有没有破坏,还是要看实际加载的class loader。

另一种看法

当然,也有另一种说法是,SPI中,接口是由Bootstrap ClassLoader 加载的,具体的实现类却是由当前线程上下文的ClassLoader(一般是Application ClassLoader)加载的,而基于双亲委派的可见性原则(子类加载器可以看到父类加载器,父类加载器看不到子类加载器),SPI 调用方无法看到或拿到具体的实现类的。

双亲委派模型中,class loader的可见性

Visibility principle allows child class loader to see all the classes loaded by parent ClassLoader, but parent class loader can not see classes loaded by child.