1. What is SPI
SPI, fully known as Service Provider Interface, is a service discovery mechanism.It automatically loads the classes defined in the file by looking for the file in the META-INF/services folder under the ClassPath path.
This mechanism makes it possible to extend many frameworks, such as the SPI mechanism used in Dubbo and JDBC.Let's start with a simple example of how it works.
1. Chestnuts
First, we need to define an interface, SPIService
package com.viewscenes.netsupervisor.spi; public interface SPIService { void execute(); }
Then, define two implementation classes, which means nothing more, just type a sentence.
package com.viewscenes.netsupervisor.spi; public class SpiImpl1 implements SPIService{ public void execute() { System.out.println("SpiImpl1.execute()"); } } ----------------------I'm a clever splitter---------------------- 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. The content is the fully qualified class name of the implementation class. Multiple implementation classes are separated by line breaks.
The file path is as follows:
SPI Profile Location
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 methods.The Service.providers package is located in sun.misc.Service, while the ServiceLoader.load package is located in 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(); } } }
The output of both methods is consistent:
SpiImpl1.execute() SpiImpl2.execute() -------------------------------- SpiImpl1.execute() SpiImpl2.execute()
2. Source Code Analysis
We see one in the sun.misc package, one in the java.util package, and the source code under the sun package is not visible.Let's take ServiceLoader.load as an example and see what's going on inside it from the source code.
1,ServiceLoader
First, let's take a look at ServiceLoader and see its class structure.
public final class ServiceLoader<S> implements Iterable<S> //Path to configuration file private static final String PREFIX = "META-INF/services/"; //Loaded service class or interface private final Class<S> service; //Loaded collection of service classes private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); //classloader private final ClassLoader loader; //Internal class, really loading service class private LazyIterator lookupIterator; }
2,Load
The load method creates some properties, and it is important to instantiate the internal class, LazyIterator.Finally, return an instance of ServiceLoader.
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; //Empty first providers.clear(); //Instantiate Internal Class LazyIterator lookupIterator = new LazyIterator(service, loader); } }
3. Find Implementation Classes
Finding implementation classes and creating them are all done in LazyIterator.When we call the iterator.hasNext and iterator.next methods, we are actually calling the corresponding LazyIterator methods.
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 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() { //On the second call, the parsing is complete and the call is 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(); //Convert File Path to URL Object configs = loader.getResources(fullName); } while ((pending == null) || !pending.hasNext()) { //Parse URL file object, read content, and return pending = parse(service, configs.nextElement()); } //Get the class name of the first implementation class nextName = pending.next(); return true; } }
4. Create an instance
Of course, when you call the next method, you actually call lookupIterator.nextService.It creates instances of implementation classes and returns them through reflection.
private class LazyIterator implements Iterator<S>{ private S nextService() { //Fully qualified class name String cn = nextName; nextName = null; //Create Class Object of Class Class<?> c = Class.forName(cn, false, loader); //Instantiate through newInstance S p = service.cast(c.newInstance()); //Put in collection, return instance providers.put(cn, p); return p; } }
Looking at this, I think it's clear.Getting an instance of a class naturally allows us to do whatever we want!
3. Application of JDBC
We started by saying that the SPI mechanism makes it possible to extend many frameworks, and JDBC actually applies this mechanism.Recall how JDBC acquired a database Connection.In earlier versions, you needed to set up a database-driven Connection before getting one 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 newer version (which version the author did not verify), setting up a database-driven connection is no longer necessary, so how does it tell which database it is?The answer is in SPI.
1. Loading
Let's look back at the DriverManager class, which does something important in the static code block.Clearly, it has already initialized the database-driven connection through the SPI mechanism.
public class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } }
The process also involves loadInitialDrivers, 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() { //Clearly, it loads a service class for the Driver interface, which packages java.sql.Driver //So it's looking for the META-INF/services/java.sql.Driver file ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ //Create object after finding while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); } }
So, where is this file?Let's look at the jar package for MySQL, which is this file: com.mysql.cj.jdbc.Driver.
MySQL SPI File
2. Create an instance
The last step has found the fully qualified class name of com.mysql.cj.jdbc.Driver in MySQL, and an instance of this class is created when the next method is called.It does one thing, registering its own instance with DriverManager.
public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { //register //Call the registration method of the DriverManager class //Add an instance to the registeredDrivers 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, calls its connect method, gets the connection, and returns.
private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { //The registeredDrivers 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 more?If we also create a java.sql.Driver file to customize the implementation class MyDriver, we can dynamically modify some information before and after getting a connection.
Or create a file under the project ClassPath with the custom driver class com.viewscenes.netsupervisor.spi.MyDriver
Custom Database Driver
Our MyDriver implementation class inherits from the NonRegisteringDriver in MySQL and implements the java.sql.Driver interface.This way, when the connect method is called, this class is called, but the actual creation 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("Preparing 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--------------------- //Ready to create a database connection.url:jdbc:mysql://consult?ServerTimezone=UTC JDBC Configuration information:{user=root, password=root} //Database connection creation complete! com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f