Deep Understanding of SPI Mechanisms

Keywords: Java JDBC Database MySQL

I. What is SPI
SPI, called Service Provider Interface, is a service discovery mechanism. It automatically loads the classes defined in the file by looking up the file in the META-INF/services folder under the ClassPath path.

This mechanism provides the possibility for many framework extensions, such as the use of SPI mechanism in Dubbo and JDBC. Let's start with a very simple example to see how it works.

1. Chestnut
First, we need to define an interface, SPIService.

package com.viewscenes.netsupervisor.spi;
public interface SPIService {

void execute();

}
Then, define two implementation classes, no other meaning, just enter a sentence.

package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{

public void execute() {
    System.out.println("SpiImpl1.execute()");
}

}
------------------------------------------------------------------------------------------------------------------------------------------------------------
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{

public void execute() {
    System.out.println("SpiImpl2.execute()");
}

}
Finally, add a file to the configuration under the ClassPath path. The file name is the fully qualified class name of the interface, and the content is the fully qualified class name of the implementation class. Many implementation classes are separated by newline characters.
The file path is as follows:

SPI configuration file location

The content is the fully qualified class name of the implementation class:

com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2
2. Testing
Then we can get an instance of the implementation class through the ServiceLoader.load or Service.providers method. The Service.providers package is located in sun.misc.Service, while the Service Loader. load package is located in java. util. Service Loader.

public class Test {

public static void main(String[] args) {    
    Iterator<SPIService> providers = Service.providers(SPIService.class);
    ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);

    while(providers.hasNext()) {
        SPIService ser = providers.next();
        ser.execute();
    }
    System.out.println("--------------------------------");
    Iterator<SPIService> iterator = load.iterator();
    while(iterator.hasNext()) {
        SPIService ser = iterator.next();
        ser.execute();
    }
}

}
The output results of the two methods are identical:

SpiImpl1.execute()

SpiImpl2.execute()

SpiImpl1.execute()
SpiImpl2.execute()
Source code analysis
We see one in the sun.misc package, one in the java.util package, the source code under the sun package can not be seen. Let's take ServiceLoader.load as an example and see how it works from the source code.

1,ServiceLoader
First, let's look at Service Loader and its class structure.

public final class ServiceLoader<S> implements Iterable<S>

//Path of configuration file
private static final String PREFIX = "META-INF/services/";
//Loaded service classes or interfaces
private final Class<S> service;
//Loaded collection of service classes
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//classloader
private final ClassLoader loader;
//Internal classes, really loading service classes
private LazyIterator lookupIterator;

}
2,Load
The load method creates some attributes, and the important thing is to instantiate the inner class, LazyIterator. Finally, an instance of Service Loader is returned.

public final class ServiceLoader<S> implements Iterable<S>

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    //Interfaces to load
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //classloader
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    //access controller
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    //Clean up first
    providers.clear();
    //Instantiate internal classes 
    LazyIterator lookupIterator = new LazyIterator(service, loader);
}

}
3. Finding Implementation Classes
The process of finding and creating implementation classes is done in Lazy Iterator. When we call iterator.hasNext and iterator.next methods, we actually call the corresponding methods of Lazy Iterator.

public Iterator<S> iterator() {

return new Iterator<S>() {
    public boolean hasNext() {
        return lookupIterator.hasNext();
    }
    public S next() {
        return lookupIterator.next();
    }
    .......
};

}
So, we focus on the lookupIterator.hasNext() method, which eventually calls the hasNextService.

private class LazyIterator implements Iterator<S>{

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null; 
private boolean hasNextService() {
    //By the time of the second call, the parsing has been completed and returned directly.
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        //META-INF/services/plus the fully qualified class name of the interface is the file of the file service class.
        //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
        String fullName = PREFIX + service.getName();
        //Converting file paths to URL objects
        configs = loader.getResources(fullName);
    }
    while ((pending == null) || !pending.hasNext()) {
        //Parse the URL file object, read the content, and finally return
        pending = parse(service, configs.nextElement());
    }
    //Get the class name of the first implementation class
    nextName = pending.next();
    return true;
}

}
4. Create instances
Of course, when you call the next method, you actually call lookupIterator.nextService. It creates instances of implementation classes by reflection and returns them.

private class LazyIterator implements Iterator<S>{

private S nextService() {
    //Fully qualified class name
    String cn = nextName;
    nextName = null;
    //Create Class Objects of Classes
    Class<?> c = Class.forName(cn, false, loader);
    //Instance through newInstance
    S p = service.cast(c.newInstance());
    //Put in the collection and return the instance
    providers.put(cn, p);
    return p; 
}

}
Seeing this, I think it's clear. Getting an instance of a class, we can naturally do whatever we want with it!

Application of JDBC
We started by saying that the SPI mechanism makes it possible to extend many frameworks, which JDBC actually applies. Recall how JDBC acquires database connections. In earlier versions, you need to set up a database-driven Connection first, and then get a Connection through DriverManager.getConnection.

String url = "jdbc:mysql:///consult?serverTimezone=UTC";
String user = "root";
String password = "root";

Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, user, password);
In the latest version (which version is not validated by the author), setting up a database-driven connection is no longer necessary, so how does it distinguish which database it is? The answer is SPI.

1. Loading
Let's look back at the DriverManager class, which does something important in the static code block. Obviously, it has initialized the database-driven connection through the SPI mechanism.

public class DriverManager {

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

}
The specific process also depends on load Initial Drivers, which looks for the service class of the Driver interface, so its file path is META-INF/services/java.sql.Driver.

public class DriverManager {

private static void loadInitialDrivers() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            //Obviously, it loads the service class of the Driver interface, the package of which is: java.sql.Driver
            //So what it's looking for is the META-INF/services/java.sql.Driver file
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                //Create objects after finding them
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });
}

}
So where is this file? Let's take a look at MySQL's jar package, which is the file, com.mysql.cj.jdbc.Driver.
MySQL SPI file
2. Create instances
The last step has found the fully qualified class name of com.mysql.cj.jdbc.Driver in MySQL. When the next method is called, an instance of this class is created. It completes one thing by registering its own instance with DriverManager.

public class Driver extends NonRegisteringDriver implements java.sql.Driver {

static {
    try {
        //register
        //Call the registration method of DriverManager class
        //Add instances to the registered Drivers collection
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
public Driver() throws SQLException {
    // Required for Class.forName().newInstance()
}

}
3. Create Connection
The DriverManager.getConnection() method is where the connection is created. It loops through the registered database driver and calls its connect method to get the connection and return it.

private static Connection getConnection(

    String url, java.util.Properties info, Class<?> caller) throws SQLException {   
//The registered Drivers contains an instance of com.mysql.cj.jdbc.Driver
for(DriverInfo aDriver : registeredDrivers) {
    if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            //Call the connect method to create a connection
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                return (con);
            }
        }catch (SQLException ex) {
            if (reason == null) {
                reason = ex;
            }
        }
    } else {
        println("    skipping: " + aDriver.getClass().getName());
    }
}

}
4. Re-expansion
Now that we know how JDBC creates database connections, can we extend it a little bit? If we create a java.sql.Driver file ourselves and customize the implementation class MyDriver, we can dynamically modify some information before and after acquiring the connection.

Or create a file under the project ClassPath with the content of the custom driver class com.viewscenes.netsupervisor.spi.MyDriver

Custom Database Driver
Our MyDriver implementation class inherits from NonRegisteringDriver in MySQL and implements the java.sql.Driver interface. In this way, when the connect method is called, this class is called, but the actual creation process is still done by MySQL.

package com.viewscenes.netsupervisor.spi

public class MyDriver extends NonRegisteringDriver implements Driver{

static {
    try {
        java.sql.DriverManager.registerDriver(new MyDriver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
public MyDriver()throws SQLException {}

public Connection connect(String url, Properties info) throws SQLException {
    System.out.println("Prepare to create a database connection.url:"+url);
    System.out.println("JDBC Configuration information:"+info);
    info.setProperty("user", "root");
    Connection connection =  super.connect(url, info);
    System.out.println("Database connection creation completed!"+connection.toString());
    return connection;
}

}
Output results
Prepare to create a database connection. url: jdbc: mysql://consult? Server Timezone = UTC
JDBC configuration information: {user=root, password=root}
Create database connection! com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

Posted by slimsam1 on Fri, 06 Sep 2019 02:10:56 -0700