1

I want to know about Service provider interface(SPI)

I have wrote example which uses this technique:

Project structure:

enter image description here

Main:

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 {...

content of META-INF/services/my.spi.renderer.ReportRenderer:

my.spi.renderer.FileReportRenderer

I've created jar and started application:

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

This is clear. But I don't understand how can I use this trick in my real application. Which benefits can I get?

Wiki says that JDBC uses this technique.

I've found that

mysql-connector-java-5.1.16-bin.jar has something related inside:

mysql-connector-java-5.1.16-bin.jar\META-INF\services\java.sql.Driver

and it contains:

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

Please, clarify how does connectir uses SPI inside and why? Is it only way to imlement JDBC?

gstackoverflow
  • 36,709
  • 117
  • 359
  • 710

1 Answers1

4

A services definition allows for dynamic discovery of implementations of an interface (or class). As an example, in JDBC, the services mechanism is used by java.sql.DriverManager to find and load implementations of java.sql.Driver.

Specifically it does (code from 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);
        }
    }
}

Your code doesn't need to know which driver is going to be used (so, no need for Class.forName(<nameOfDriverClass>)), it just needs to be present at runtime. So there is no need to explicitly load or configure the driver class to be used. The only thing you really need is a JDBC url (which might be provided by configuration or other means), and DriverManager will try all loaded driver until a connection is established by one of the drivers. This means that if you have a different driver that supports the same URL, then you can swap drivers. Or if you want to use a different database you can just put its driver on the class path and configure the right URL (I'm ignoring potential problems with SQL dialects).

So to answer the follow-up question:

clarify how does connectir uses SPI inside and why

Connector/J itself doesn't use the service loader mechanism, it is Java itself that uses to load the drivers. And Connector/J implements this, because that is required by the JDBC specification since JDBC 4.

There is a caveat with JDBC: the driver needs to be on the initial (system) class path to be loaded automatically. Drivers located on other class paths (eg drivers inside web applications), may still need to be loaded explicitly.

If you search inside Java itself, you'll find the ServiceLoader is used extensively, some examples:

  • java.awt.Toolkit uses it to load javax.accessibility.AccessibilityProvider implementations, which allows Java to load platform-specific support for accessibility features
  • java.net.URL uses it to load java.net.spi.URLStreamHandlerProvider implementations, which allows you to add support for different network protocols (eg http, ftp, etc)
  • java.nio.charset.Charset uses it to load java.nio.charset.spi.CharsetProvider, which allows you to add extra character sets to Java
  • etc, etc

Tools and libraries can use the service loader mechanism to support plugins/extensions. In your own example, another library, or maybe even a user of your library, could implement their own ReportRenderer, and have it automatically discovered and used by your tool/library.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • what the profit? I don't need to write class.forName but I must know url. – gstackoverflow Oct 20 '17 at 13:03
  • **DriverManager will try all loaded driver until a connection is established by one of the drivers** I could not find where **loadInitialDrivers** method fill collection of drivers. I see that it loads service: **ServiceLoader.load(Driver.class)** and then just iterate over the itarable(**while(driversIterator.hasNext()) { driversIterator.next(); }**) – gstackoverflow Oct 20 '17 at 13:11
  • The 'profit' is automatic discovery of the available drivers, which can be very handy. If your program always connects to the same database type with the same driver, that may not seem relevant, but if you do need dynamic connections, or deployment specific configurations that can be very handy, because the **code** is decoupled from this. – Mark Rotteveel Oct 20 '17 at 13:12
  • That is because the JDBC specification **requires** that drivers register themselves with `DriverManager` when their class is loaded, so simply iterating all drivers through `ServiceLoader` is sufficient to load the driver class and then the driver class registers itself. – Mark Rotteveel Oct 20 '17 at 13:14
  • Thus each driver has static initializer where it call DriverManager to register it by call **java.sql.DriverManager#registerDriver(java.sql.Driver)** ? – gstackoverflow Oct 20 '17 at 13:19
  • @gstackoverflow Yes, as documented in the apidoc of [`java.sql.Driver`](http://docs.oracle.com/javase/8/docs/api/java/sql/Driver.html). – Mark Rotteveel Oct 20 '17 at 13:20