Art is long, life is long

What is SPI

SPI is relative to API.

API refers to the interface provided by the application to the service caller, which is used to provide certain services and functions for the service caller.

SPI refers to the interface provided by the application to the service implementer to implement a certain service or function

Second, SPI mechanism

Service Provider Interface (SPI) is a built-in Service discovery mechanism in JDK.

We have already touched on this in our daily development, but we don’t really need it in general development, such as JDBC, Apache Logging, etc. SPI mechanism is used

Interfaces to these SPIs are provided by Java core libraries, and implementations of SPI are included in the CLASSPATH as jar packages that Java applications depend on. For example, the implementation of JDBC mysql is dependent on maven.

The problem, then, is that SPI’s interfaces are part of the Java core library and are loaded by the Bootstrap Classloader. SPI implementation classes are loaded by the System ClassLoader.

The bootloader cannot find the implementation class of SPI at load time because the parent delegate model states that the bootloader, BootstrapClassloader, cannot delegate the system classloader, AppClassLoader, to load it. At this point, how to solve the problem?

Thread-context class loading was born, which also broke the parent delegate model of class loaders and enabled programs to reverse class loading.

From the JDBC case study:

Connection conn = java.sql.DriverManager.getConnection(url, "name"."password");
Copy the code

Get the database connection as described above, then look at the DriverManager class

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

There is a static code block in the DriverManager class. It’s obviously going to call loadInitialDrivers, and then loadInitialDrivers

    private static void loadInitialDrivers(a) {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run(a) {
                    return System.getProperty("jdbc.drivers"); }}); }catch (Exception ex) {
            drivers = null;
        }
      
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run(a) {
                
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

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

The key piece of information is the ServiceLoader utility class, which the JDK provides for service implementation lookup: java.util.Serviceloader. It is used for service implementation using the SPI mechanism

The Driver described above is in the java.sql.Driver package, provided by the JDK as a specification, which is an interface. How do other vendors do that?

Maven’s introduction of mysql dependencies automatically adds jars to the CLASSPATH /CLASSPATH

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
Copy the code

It provides the SPI mechanism configuration file (must be the meta-INF /services path in the classpath)

The service implementation provided happens to be the Driver of the mysql package we introduced

The file name must be the same as the name of the fully qualified interface class java.sql.Driver

Com.mysql.cj.jdbc.driver needs to implement java.sql.Driver

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver(a) throws SQLException {}static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!"); }}}Copy the code

Thread context class loading

To get a database connection initially, the getConnection() method:

// Worker method called by the public getConnection() methods.
    private static Connection getConnection( String url, java.util.Properties info, Class
        caller) throws SQLException {
        /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ClassLoader callerCL = caller ! =null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); }}if(url == null) {
            throw new SQLException("The url cannot be null"."08001");
        }
 
        println("DriverManager.getConnection(\"" + url + "\")");
 
        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;
 
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println(" trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if(con ! =null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return(con); }}catch (SQLException ex) {
                    if (reason == null) { reason = ex; }}}else {
                println(" skipping: "+ aDriver.getClass().getName()); }}// if we got here nobody could connect.
        if(reason ! =null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }
 
        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
Copy the code

Emphasis: Thread. CurrentThread (). GetContextClassLoader ()

3. Use of SPI

3.1. Create a service interface

package com.jw.spi;

public interface Fruit {
    String getName(a);
}
Copy the code

3.2. Create multiple service implementations

package com.jw.spi;

public class Apple implements Fruit {
    @Override
    public String getName(a) {
        return "apple"; }}Copy the code
package com.jw.spi;

public class Banana implements Fruit {
    @Override
    public String getName(a) {
        return "Banana"; }}Copy the code

The two service implementation classes here are for two service implementation parties, one implementing Apple and the other implementing Banana.

3.3. Create a configuration file

Create a/meta-INF /services directory under resource and create a file named com.jw.spi.fruit under services with the fully qualified name of the service interface

The content of the file is the fully qualified name of the service implementer class for the current service implementation

com.jw.spi.Apple
Copy the code

3.4. Create a test class

public class Test {

    public static void main(String[] args) {
        ServiceLoader<Fruit> s = ServiceLoader.load(Fruit.class);
        Iterator<Fruit> it = s.iterator();
        while(it.hasNext()){ System.out.println(it.next().getName()); }}}Copy the code

The execution result is as follows:

 apple
Copy the code

4. Implementation principle of SPI

The main implementation of SPI relies on the ServiceLoader class. Use this class to load interface types (e.g. Fruit.class)

ServiceLoader<Fruit> s = ServiceLoader.load(Fruit.class);
Copy the code

This is a load method, but it does not load the specified service implementation class. Here are some preparations for loading the service implementation class:

The ServiceLoader class is a ServiceLoader class.

public final class ServiceLoader<S>
    implements 可迭代<S>
{
	// Scan directory prefixes
    private static final String PREFIX = "META-INF/services/";

    // The loaded class or interface
    private final Class<S> service;

    Class loaders used to locate, load, and instantiate classes implemented by the implementer
    private final ClassLoader loader;

    // Context object
    private final AccessControlContext acc;

    // Cache instantiated classes in the order in which they are instantiated
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // lazy search iterator
    private LazyIterator lookupIterator;
    
    
    / / create the ServiceLoader
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class service, ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

   private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // Assign a value to service
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
       	// Assign to loader
        loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Assign accacc = (System.getSecurityManager() ! =null)? AccessController.getContext() :null;
        reload();
    }

    public void reload(a) {
        // Clear the providers cache
        providers.clear();
        // Assign a lookupIterator to create a LazyIterator delay iterator.
        lookupIterator = new LazyIterator(service, loader);
    }

    // A private inner class that provides the loading and instantiation of all service classes
	private class LazyIterator implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader; }... }Copy the code

Iterator

it = s.iterator();

public Iterator<S> iterator(a) {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext(a) {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next(a) {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove(a) {
            throw newUnsupportedOperationException(); }}; }Copy the code

The iterator method uses an anonymous inner class to define a new iterator, each of which is done by calling lookupIterator, a deferred iterator created earlier

The last step is iterative loading.

while(it.hasNext()){
    System.out.println(it.next().getName());
}
Copy the code
public boolean hasNext(a) {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run(a) { returnhasNextService(); }};returnAccessController.doPrivileged(action, acc); }}private boolean hasNextService(a) {
    if(nextName ! =null) {
        return true;
    }
    if (configs == null) {
        try {
            // Get all the classes in the directory
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x); }}while ((pending == null) | |! pending.hasNext()) {if(! configs.hasMoreElements()) {return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
Copy the code

The hasNext method calls the hasNext method of the deferred iterator and internally calls the hasNextService method, in which it tries to find the resource file with the specified name (meta-INF /services/+ interface fully qualified name). And finish reading the contents of the file.

It then executes it.next(), which in turn calls hasNext, the corresponding method of the deferred iterator, internally calling the nextService method, whose main function is to load the class represented by the fully qualified name read from the file above. Generate instances and save them to providers.

public S next(a) {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run(a) { returnnextService(); }};returnAccessController.doPrivileged(action, acc); }}private S nextService(a) {
    if(! hasNextService())throw new NoSuchElementException();
    String cn = nextName;
    nextName = null; Class<? > c =null;
    try {
        // reflection load class
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if(! service.isAssignableFrom(c)) { fail(service,"Provider " + cn  + " not a subtype");
    }
    try {
        / / instantiate
        S p = service.cast(c.newInstance());
        // Save the instance to providers
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}
Copy the code

Five, the summary

That concludes the detailed explanation of SPI and summarizes the benefits it can bring:

  • It can be extended and decoupled without changing the source code.
  • Implementing extensions is almost non-intrusive to the original code.
  • You only need to add configuration to achieve the extension, in accordance with the open closed principle.