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

时间:2017-10-19 16:17:18

标签: java jdbc service-provider

我想了解this

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

Project structure:

Service provider interface(SPI)

主要

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 \服务\ java.sql.Driver中

它包含:

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

请澄清connectir如何使用SPI以及为什么?它只是通过imlement的方式吗?

1 个答案:

答案 0 :(得分:2)

服务定义允许动态发现接口(或类)的实现。例如,在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);
        }
    }
}

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

所以回答后续问题:

  

阐明connectir如何使用SPI以及为什么

Connector / J本身不使用服务加载器机制,Java本身用于加载驱动程序。 Connector / J实现了这一点,因为自JDBC 4以来JDBC规范需要这样做。

JDBC有一个警告:驱动程序需要位于要自动加载的初始(系统)类路径上。位于其他类路径上的驱动程序(例如Web应用程序内的驱动程序)可能仍需要明确加载。

如果您在Java内部进行搜索,您会发现ServiceLoader被广泛使用,例如:

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

工具和库可以使用服务加载器机制来支持插件/扩展。在您自己的示例中,另一个库,甚至是您库的用户,可以实现自己的ReportRenderer,并让您的工具/库自动​​发现和使用它。