1. 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.

For example, if you have an interface, now this interface has multiple implementation classes, how do you know which implementation class to choose when the system is running? SPI allows you to specify a default configuration, load a specific implementation class based on the configuration, and then use an instance object of that implementation class.

The SPI mechanism is generally used in plug-in extension scenarios. For example, if you develop an open source framework for others to use, and you want others to write a plug-in to use in your open source framework to extend a function, the SPI idea is used. This mechanism provides the possibility for many framework extensions, such as the USE of SPI mechanisms in Dubbo and JDBC.

2. The embodiment of SPI idea in Java

SPI is the embodiment of classic ideas, such as JDBC. Java defines a set of JDBC interfaces, but Java does not provide JDBC implementation classes. But which implementation classes of the JDBC interface will actually be used when the project runs? Typically, depending on the database you’re using, such as mysql, you’ll import mysql-jdbC-connector.jar; Oracle, you will introduce oracle-jDBC-connector.jar. Therefore, in order to facilitate management, it is necessary to customize a unified interface, so that callers can easily program for the unified interface when calling the database. But the question is, which implementation do you use? Where do I find the implementation classes? This is where the SPI mechanism comes in.

The most common java.sql.Driver interface we use when accessing a database:

The Java SPI specifies that you create a file named after the service interface in the META-INF/services/ directory of the CLASspath, which then records the fully qualified name of the specific implementation class provided by the JAR package. When we reference a jar package, we can go to the meta-INF /services/ directory of the jar package, find the file according to the interface name, and then read the contents of the file to load and instantiate the implementation class.

3. Java SPI example

Let’s write a small example of our own to better understand the SPI mechanism.

First write an interface and two corresponding implementation classes:

public interface SPIService {
	void say(a);
}

public class Nihao implements SPIService {
	public void say(a) {
		System.out.println("Hello, funny."); }}public class Hello implements SPIService{
	public void say(a) {
		System.out.println("Hello"); }}Copy the code

I then created a file in the meta-INF /services/ directory with the fully qualified name of the interface and the fully qualified class name of the implementation class, separated by newlines. The details are as follows:

Com.demo.spi. Nihao com.demo.spi.Hello then we can get an instance of the implementation class via serviceloader. load or Service. Write a mian method to test it:

public class Main {
	public static void main(String[] args) { ServiceLoader< SPIService> serviceLoader = ServiceLoader.load(SPIService); Iterator< SPIService> iterator = serviceLoader.iterator();while(iterator.hasNext()) { SPIService next = iterator.next(); next.say(); }}}Copy the code

Running results:

Hello Hello!

Process finished with exit code 0

4. Java SPI source analysis

ServiceLoader: ServiceLoader: ServiceLoader: ServiceLoader: ServiceLoader

public final class ServiceLoader<s> implements 可迭代<s> // Path to the configuration fileprivate static final String PREFIX = "META-INF/services/";
    // The loaded service class or interface
    private final Class<s> service;
    // Loaded service class cache collection (in instance order)
    privateLinkedHashMap&lt; String,S&gt; providers =newLinkedHashMap&lt; &gt; (a);// Class loader
    private final ClassLoader loader;
    // Inner class, which actually loads the service class
    private LazyIterator lookupIterator;
}
Copy the code

From serviceloader.load () :

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 newServiceLoader&lt; &gt; (service, loader); }private ServiceLoader(Class<s> svc, ClassLoader cl) {
	// The interface to load
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // Class loader
    loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Access the controlleracc = (System.getSecurityManager() ! =null)? AccessController.getContext() :null;
    reload();
}

public void reload(a) {
	// Now clear the collection cache
    providers.clear();
    // Instantiate the inner class to get a LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}
Copy the code

If not, use SystemClassLoader, then clear the cache, and instantiate the inner class: LazyIterator. Finally, the ServiceLoader instance is returned. LazyIterator is an implementation of Iterator. Let’s take a look at its specific method:

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

HasNext () is actually hasNextService(); Next () actually calls nextService(). Take a look at hasNextService() :

private boolean hasNextService(a) {
    // The second time it is called, the parsing is complete, and it returns directly
    if(nextName ! =null) {
        return true;
    }
    if (configs == null) {
        // meta-inf /services/ add the fully qualified class name of the interface to the file service class file
        String fullName = PREFIX + service.getName();
        // Convert the file path to a URL object
        configs = loader.getResources(fullName);
    }
    while ((pending == null) | |! pending.hasNext()) {// Parses the URL file object, iterates through the file contents line by line, and returns
        pending = parse(service, configs.nextElement());
    }
    // Get the name of the first implementation class
    nextName = pending.next();
    return true;
}
Copy the code

This method is to find the file corresponding to the interface according to the agreed path, and load and parse the file content.

Take a look at the next() method:

private S nextService(a) {
    if(! hasNextService())throw new NoSuchElementException();
    // Fully qualified class name
    String cn = nextName;
    nextName = null; Class&lt; ? &gt; c =null;
    try {
        // Create a Class object for the 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 through newInstance
        S p = service.cast(c.newInstance());
        // Put into the collection, return the instance
        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

This method basically loads the class with the fully qualified class name, creates an instance, puts the instance into the cache collection and returns the example.

Java SPI source code implementation process, the idea is very simple: 1) first agree a directory 2) go to that directory according to the interface name to find the file 3) parse the file to get the fully qualified name of the implementation class 4) then loop load implementation class and create an example

5. Java SPI summary

The BUILT-IN SPI mechanism in the JDK has its own advantages, but it also has some disadvantages due to its simplicity of implementation.

advantages

The advantage of using the Java SPI mechanism is decoupling, so that the definition of an interface is separated from the concrete business implementation, rather than coupled together. Application processes can enable or replace specific components based on actual services.

disadvantages

1) Cannot be loaded on demand. Although ServiceLoader does lazy loading, it is basically only possible to fetch it all by traversing it, i.e. the implementation classes of the interface are loaded and instantiated once. If you don’t want to use some implementation class, or if instantiation of some class is time consuming, it gets loaded and instantiated, and that’s a waste. 2) The method of obtaining an implementation class is not flexible enough. It can only be obtained in the form of Iterator. The corresponding implementation class cannot be obtained according to a certain parameter. 3) It is not safe for multiple concurrent threads to use instances of the ServiceLoader class.

Given SPI’s shortcomings, many systems implement a class loading mechanism of their own, such as Dubbo. In addition to the ability to load implementation classes on demand, Dubbo SPI adds IOC and AOP features, as well as an adaptive extension mechanism. I’ll share this with you in the future.