Deep understanding of SPI mechanism intrusion and deletion

Keywords: JDBC Database MySQL Java

Original Link: https://www.jianshu.com/p/3a3edbcd8f24

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


 

Posted by amma on Sat, 24 Aug 2019 22:32:13 -0700