Abstract

The original text is from The Quiet Place. This article aims to deepen the understanding of SPI by recording it. If you want to know more, please click the original link to view it. If there is infringement, the following message is deleted.

What is SPI

SPI, or Service Provider Interface, is a Service discovery mechanism. It automatically loads the classes defined in the files by looking for them in the META-INF/services folder in the ClassPath path.

This mechanism provides the possibility for many framework extensions, such as the SPI mechanism used in Dubbo, JDBC, and SpringBoot auto-assembly. Let’s start with a very simple example of how it works.

Second, demo

First, we need to define an interface, SPIService

package com.viewscenes.netsupervisor.spi;
public interface SPIService {
    void execute();
}
Copy the code

Then, define two implementation classes that simply output one sentence.

package com.viewscenes.netsupervisor.spi; public class SpiImpl1 implements SPIService{ public void execute() { System.out.println("SpiImpl1.execute()"); }} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- I'm clever line -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- package com.viewscenes.net supervisor. The spi. public class SpiImpl2 implements SPIService{ public void execute() { System.out.println("SpiImpl2.execute()"); }}Copy the code

Finally, add a file in the ClassPath configuration with the fully qualified class name of the interface, the content with the fully qualified class name of the implementation class, and multiple implementation classes separated by a newline character.

The file path is as follows:

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

com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2
Copy the code

Then we can get an instance of the implementation class through the Serviceloader. load or service. provider methods. Providers package is located at sun.misc.service, and Serviceloader.load is located at java.util.Serviceloader.

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(); }}}Copy the code

The output is the same either way:

SpiImpl1.execute()
SpiImpl2.execute()
--------------------------------
SpiImpl1.execute()
SpiImpl2.execute()
Copy the code

Third, source code analysis

We see one in the sun.misc package and one in the java.util package, where the source code is not visible. Let’s take serviceloader.load as an example and see what it does in the source code.

1, the ServiceLoader

First, let’s take a look at ServiceLoader and its class structure

Public final class ServiceLoader<S> implements Iterable<S> // Configuration file path private static final String PREFIX = "META-INF/services/"; Private final Class<S> service; Private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); Private final ClassLoader loader; // Inner class, the real load service class private LazyIterator lookupIterator; }Copy the code

2, the Load

The Load method creates some properties, but importantly instantiates the inner class, LazyIterator. Finally, the ServiceLoader instance is returned.

public final class ServiceLoader<S> implements Iterable<S> private ServiceLoader(Class<S> svc, RequireNonNull (SVC, "Service interface cannot be null") {// requireNonNull(SVC, "service interface cannot be null"); // class loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl; / / access controller acc = (System. GetSecurityManager ()! = null) ? AccessController.getContext() : null; Providers.clear (); // Providers.clear (); LazyIterator lookupIterator = new LazyIterator(service, loader); }}Copy the code

3. Find the implementation class

The process of finding and creating implementation classes is done in LazyIterator. When we call iterator.hasNext and iterator.next, we are actually calling the corresponding methods of LazyIterator.

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

So, we’ll focus on the lookupiterator.hasNext () method, which will eventually call 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() {return if (nextName! = null) { return true; } if (configs == null) {// meta-inf /services/ File is the service class supervisor / / META-INF/services/com.viewscenes.net. The spi. SPIService String fullName = PREFIX + service. The getName ();  // Convert the file path to the URL object configs = loader.getResources(fullName); } while ((pending == null) || ! Pending.hasnext ()) {return pending = parse(service, configs.nextelement ()); NextName = pending.next(); return true; }}Copy the code

4. Create an instance

Calls, of course, the next method, the actual call, lookuoIterator. NextServcie. It creates and returns an instance of the implementation class by reflection.

Private class implements Iterator<S>{private class implements Iterator<S>{private class implements Iterator<S>{private class implements Iterator<S>{private class implements Iterator<S>{private class implements Iterator<S>{ nextName = null; // Create the Class object Class<? > c = Class.forName(cn, false, loader); S p = service.cast(c.newinstance ()); Providers.put (cn, p); return p; }}Copy the code

Now, I think it’s pretty clear that, given the implementation of the class, we can do whatever we want with it.

Application in JDBC

We began by saying that the SPI mechanism makes it possible to extend many frameworks, and JDBC applies it. Recall the JDBC access to the database link, the process of in previous versions, need to set up the database drive connections, through DriverManager. GetConnection for a Connection.

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);Copy the code

In later versions, this step is no longer required to set up a database-driven connection, so how does it tell which database it is? The answer lies in SPI.

1, load,

Let’s look at the DriverManager class, which does one of the more important things in the static code block. It has clearly initialized the database-driven connection via the SPI mechanism.

public class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }}Copy the code

Also depends on the specific process loadInitialDrivers, it inside to find the Driver interface service class, so the file path is: meta-inf/services/Java, SQL. The Driver

public class DriverManager { private static void loadInitialDrivers() { AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {// It was clear that it needed to load the Driver interface service class, Driver interface package: Java. SQL. Driver / / so it is looking for meta-inf/services/Java SQL. The Driver file ServiceLoader < Driver > loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); While (driversiterator.hasnext ()) {driversiterator.next (); } } catch(Throwable t) { // Do nothing } return null; }}); }}Copy the code

So, where is this file? MySQL jar: com.mysql.cj.jdbc.driver

2. Create an instance

The com.mysql.cj.jdbc.drievr fully qualified class name was found in MySQL in the previous step. When the next method is called, an instance of this class is created, which completes the task of registering its own instance with DriverManager.

Public class Driver extends NonRegisteringDriver implements java.sql.Driver {static {try {// Register / / / / call the DriverManager class registration method to add instance registeredDrivers set 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() } }Copy the code

Create a Connection

The DriverManager. GetConnection () method is to create a connection, it through the loop has registered database driver, call the connect method, get connected and return.

private static Connection getConnection( String url, java.util.Properties info, Class<? < caller) throws SQLException {//registeredDrivers = com.mysql.cj.jdbc.driver; registeredDrivers) { if(isDriverAllowed(aDriver.driver, 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()); }}}Copy the code

4. Expand

Now that we know that JDBC creates database connections this way, can we extend it further? If we create a java.sql.Driver file ourselves and customize the implementation class MyDriver, we can dynamically modify some information before and after the connection is acquired.

Or under the project ClassPath create a file, the file content for custom driver classes: com.viewscenes.net supervisor. Spi. MyDriver

Our MyDriver implementation class inherits from MySQL’s NonRegisteringDriver and implements the java.sql.Driver interface. This class is called when the connect method is called, but the actual creation process is 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 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 created! +connection.toString()); return connection; }} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the output -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ready to create the database connection. Url: JDBC: mysql: / / / consult? ServerTimezone =UTC JDBC configuration information: {user=root, password=root} Database connection created! com.mysql.cj.jdbc.ConnectionImpl@7cf10a6fCopy the code