SPI解释的实际用法(例如JDBC)?

real usage of SPI explanation(JDBC for example)?

我想了解 Service provider interface(SPI)

我写了一个使用这种技术的例子:

Project structure:

主要:

ReportRenderer reportRenderer = ReportRenderer.getInstance();
System.out.println(reportRenderer.getClass());

ReportRenderer:

public class ReportRenderer {
    public static ReportRenderer getInstance() {
        final Iterator<ReportRenderer> providers = ServiceLoader.load(ReportRenderer.class).iterator();
        if (providers.hasNext()) {
            return providers.next();
        }
     return new ReportRenderer();
    }

FileReportRenderer:

public class FileReportRenderer extends ReportRenderer {...

META-INF/services/my.spi.renderer.ReportRenderer的内容:

my.spi.renderer.FileReportRenderer

我已经创建了 jar 并启动了应用程序:

D:\work\SPI_test\build\libs>java -jar SPI_test.jar
class my.spi.renderer.FileReportRenderer

这很清楚。但我不明白如何在我的实际应用程序中使用这个技巧。我可以获得哪些福利?

Wiki 说 JDBC 使用了这种技术。

我发现

mysql-connector-java-5.1.16-bin.jar里面有相关的东西:

mysql-连接器-java-5.1.16-bin.jar\META-INF\services\java.sql.Driver

它包含:

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

请说明connectir内部是如何使用SPI的,为什么?这是实现 JDBC 的唯一方法吗?

服务定义允许动态发现接口的实现(或class)。例如,在 JDBC 中,java.sql.DriverManager 使用服务机制来查找和加载 java.sql.Driver.

的实现

具体来说(来自 Java 9 的代码):

/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

// ...

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

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

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

您的代码不需要知道将使用哪个 driver(因此,不需要 Class.forName(<nameOfDriverClass>)),它只需要在运行时出现。因此无需显式加载或配置要使用的 driver class。您真正需要的唯一东西是 JDBC url(可能通过配置或其他方式提供),并且 DriverManager 将尝试所有已加载的 driver 直到建立连接通过 driver 之一。这意味着如果您有不同的 driver 支持相同的 URL,那么您可以交换 driver。或者如果你想使用不同的数据库,你可以将它的 driver 放在 class 路径上并配置正确的 URL (我忽略了 SQL 方言的潜在问题).

所以要回答 follow-up 问题:

clarify how does connectir uses SPI inside and why

Connector/J 本身不使用服务加载器机制,它是 Java 本身用来加载 driver 的。 Connector/J 实现了这一点,因为自 JDBC 4.

以来,JDBC 规范要求这样做

JDBC 有一个警告:driver 需要在初始(系统)class 路径上才能自动加载。位于其他 class 路径(例如 Web 应用程序中的 drivers)的驱动程序可能仍需要显式加载。

如果你在 Java 内部搜索,你会发现 ServiceLoader 被广泛使用,一些例子:

  • java.awt.Toolkit 使用它来加载 javax.accessibility.AccessibilityProvider 实现,这允许 Java 加载 platform-specific 对辅助功能的支持
  • java.net.URL 使用它来加载 java.net.spi.URLStreamHandlerProvider 实现,它允许您添加对不同网络协议的支持(例如 http、ftp 等)
  • java.nio.charset.Charset 使用它来加载 java.nio.charset.spi.CharsetProvider,这允许您向 Java
  • 添加额外的字符集
  • 等等,等等

工具和库可以使用服务加载器机制来支持plugins/extensions。在您自己的示例中,另一个图书馆,甚至可能是您图书馆的用户,可以实现他们自己的 ReportRenderer,并让您的 tool/library.

自动发现和使用它