What is the JDK SPI mechanism?

Service Provider Interface (SPI) is a mechanism that decouples the Service Interface from the Service implementation to greatly improve program scalability. The import service provider is the implementer who introduces the SPI interface and obtains the specific implementation class through local registration discovery, which is easily pluggable.

When the service provider provides an implementation of the interface, a file named service interface is created in the META-INF/services/ directory of the Classpath. This file records the implementation classes of the service interface provided by the JAR package. When an application introduces the JAR package and needs to use the service, the JDK SPI mechanism can search the configuration file in the META-INF/services/ jar package to obtain the specific implementation class name, load and instantiate the implementation class, and finally use the implementation class to complete the service function.

Why was the JDK SPI introduced?

As described above, SPI in Java is designed to be used as a plug-in for service providers, based on a policy pattern, to implement dynamic loading mechanisms. For example, we define only one interface (specification) in the program, and the specific implementation is handed over to different service providers. During the startup of the program, the configured configuration file is read, and the configuration file decides which service implementation is loaded. This decouples the service interface from the implementation and improves the extensibility of the program.

For example: When accessing a database using Java language, we will use the java.sql.Driver interface. Different database products have different underlying protocols, and the Driver implementation is also different. Depending on which database the profile user chooses, the implementation of that database is invoked.

Simple implementation

To write a simple example that has an interface Log, define a method Log that is passed as a Log output parameter. There are two ways to implement interfaces, namely Log4j and Logback. See the configuration file for details about which method to use. That’s it. A little example

Here’s the code

The Log interface

public interface Log {
    void log(String info);
}
Copy the code

Two implementation classes

public class Logback implements Log {

    @Override
    public void log(String info) {
        System.out.println("Logback:"+ info); }}Copy the code
public class Log4j implements Log {

    @Override
    public void log(String info) {
        System.out.println("Log4jLog4j:"+ info); }}Copy the code

Service configuration file

service.impl.Log4j
service.impl.Logback
Copy the code

The test class

public class Main {

    public static void main(String[] args) throws IOException {
        ServiceLoader<Log> spiLoader = ServiceLoader.load(Log.class);
        Iterator<Log> iteratorSpi = spiLoader.iterator();
        while (iteratorSpi.hasNext()) {
            Log log = iteratorSpi.next();
            log.log("MRyan"); }}}Copy the code

Output:

Source code analysis

The load method of the ServiceLoader is analyzed as an entry point and the parameters passed in are the interface class

   public static <S> ServiceLoader<S> load(Class<S> service) {
   		// Get the ClassLoader bound to the current thread
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
Copy the code

It then calls overload Load (), passing in the interface class and the ClassLoader bound to the current thread

    public static <S> ServiceLoader<S> load(Class service, ClassLoader loader){
  		  // Create a new service loader for the given service type and class loader.
        return new ServiceLoader<>(service, loader);
    }
Copy the code
   // Represents the class or interface of the service being loaded
    private final Class<S> service;

    // Class loaders for finding, loading, and instantiating providers
    private final ClassLoader loader;

    // Access control context taken when creating the ServiceLoader
    private final AccessControlContext acc;

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
    	// Check whether the current interface class reference returns null
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // Check whether the current thread bound to the ClassLoader is empty if not, call SystemClassLoader
        loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Check whether AccessControlContext is emptyacc = (System.getSecurityManager() ! =null)? AccessController.getContext() :null;
        // Clear the provider cache for this loader and reload all providers
        reload();
    }
Copy the code

The core method reload, what does it do

    // Provider cache
    // This cache is used to record implementation objects created by the ServiceLoader, where Key is the full implementation class name and Value is the object of the implementation class.
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // lazily loading iterators
    private LazyIterator lookupIterator;

	// Clear the provider cache for this loader and reload all providers
  public void reload(a) {
  		// Clear the provider cache
        providers.clear();
        // Create a LazyIterator that reads the SPI configuration file and instantiates the implementation class object.
        lookupIterator = new LazyIterator(service, loader);
    }

Copy the code

A little curious what LazyIterator, see names like an iterator, a little familiar, we found that the test class iterator is called ServiceLoader. LazyIterator. The Iterator interface has two key methods: the hasNext() method and the next() method. The next() method in this LazyIterator ends up calling its nextService() method, and the hasNext() method ends up calling hasNextService()

	private static final String PREFIX = "META-INF/services/"; 
	Enumeration<URL> configs = null; 
	Iterator<String> pending = null; 
	String nextName = null; 

	 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;
        }

        private boolean hasNextService(a) {
            if(nextName ! =null) {
                return true;
            }
            if (configs == null) {
                try {
                    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;
        }

        private S nextService(a) {
            if(! hasNextService())throw new NoSuchElementException();
            String cn = nextName;
            nextName = null; Class<? > c =null;
            try {
                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 {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        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); }}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); }}public void remove(a) {
            throw newUnsupportedOperationException(); }}Copy the code

Focus on the hasNextService method

  private boolean hasNextService(a) {
            if(nextName ! =null) {
                return true;
            }
            if (configs == null) {
                try {
                PREFIX = "meta-inf /services/"
                    String fullName = PREFIX + service.getName();
                      // Load the configuration file
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x); }}// Walk through the contents of the configuration file in line SPI
            while ((pending == null) | |! pending.hasNext()) {if(! configs.hasMoreElements()) {return false;
                }
                  // Parse the configuration file
                pending = parse(service, configs.nextElement());
            }
            // Update the field
            nextName = pending.next();
            return true;
        }
Copy the code

After parsing the SPI configuration file in the hasNextService() method, look at the nextService() method, which instantiates the implementation classes read by the hasNextService() method, The instantiated objects are cached in the providers collection. The core implementation is as follows:

private S nextService(a) {
            if(! hasNextService())throw new NoSuchElementException();
            String cn = nextName;
            nextName = null; Class<? > c =null;
            try {
            // // loads the class specified by the nextName field
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            // // Detection type
            if(! service.isAssignableFrom(c)) { fail(service,"Provider " + cn  + " not a subtype");
            }
            try {
            //// Creates an object that implements the class
                S p = service.cast(c.newInstance());
                The implementation class name and corresponding instance object are added to the cache
                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

With all that said about the implementation of iterators, how is the iteration implemented in the test class?

public Iterator<S> iterator(a) {
        return new Iterator<S>() {
			// knownProviders iterates through the providers cache
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
			
			// If the cache fails to query, use LazyIterator to load
            public boolean hasNext(a) {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

			//// Query the cache. If the cache query fails, load the cache using LazyIterator
            public S next(a) {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

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

So now that we’re at the end of the article, we know what SPI is, what SPI does, what SPI does, so the question is,

What are his weaknesses?

In fact, it is obvious that the real implementation is the need to read the configuration class information, so if we need to add a new driver class, is it necessary to manually add a line in the configuration class, then delete a driver class also need to manually delete, then the scalability is not good?

If you like this article, follow me with your fingers.