Suddenly see the new vocabulary, understand! The word feels very high end, actually is not what high technology.

The SPI, or Service Provider Interface, is a built-in Java Service discovery mechanism (this Service discovery mechanism is not the Service discovery mechanism in the microservice registry).

In simple terms, Java’s SPI mechanism means that an external implementation of an interface needs to be loaded. As long as the implementation is configured in the meta-INF/Services folder in the classPath, the user can automatically load the classes defined in the file.

There are three important roles in SPI:

  • interface
  • The configuration file
  • ServiceLoader Reflection obtain

We can intuitively understand: SPI is a function provided by the JDK. The ServiceLoader in the JDK can read a file configured by a third party in the Meta-INF/Services folder and automatically load the classes defined in the file, so that when we introduce a third party package, Implementation classes provided in third-party packages can be used without any hard coding.

See JDBC Case for details on why this mechanism exists.

SPI feels a bit like Spring’s IOC, where you specify an interface and automatically assemble the implementation classes for that interface through the JDK’s ServiceLoader. Control of the assembly is moved outside the program (in a third-party package), and the implementation is not dynamically specified in the program when the module is assembled. So the core idea of SPI is decoupling, which is especially important in modular design.

SPI makes it possible for many framework extensions, such as Dubbo, Spring, SpringBoot related starter components, and JDBC. Note the key word: frame! We generally use SPI is also used in the framework, because some things in the framework only need to define standards, and then the specific implementation needs to be selected according to different scenarios, in this case, the framework can use SPI interface to extend its own functionality.

BTW, JDK’s SPI mechanism has some disadvantages, frameworks like Dubbo have their own SPI implementation.

The JDK SPI of actual combat

  1. You need an interface

    public interface SpiService {
        void exe(a);
    }
    Copy the code
  2. Defining multiple implementations

    public class SpiServiceImplA implements SpiService {
        public void exe(a) {
            System.out.println("I am A..."); }}Copy the code
  3. Configuration/meta-inf/services/work. Lollipops. Tutorial.. Java SpiService

    work.lollipops.tutorial.java.SpiServiceImplA
    work.lollipops.tutorial.java.SpiServiceImplB
    Copy the code
  4. test

    ServiceLoader<SpiService> spiServices = ServiceLoader.load(SpiService.class);
    spiServices.forEach(SpiService::exe);
    Copy the code

Output:

I am A...
I am B...
I am A...
I am B...
Copy the code

Analysis of ServiceLoader source code

Sun.misc. Service source code belongs to Sun, we can not see, so analyze ServiceLoader.

The implementation of this class is simple and can be divided into three large chunks:

public final class ServiceLoader<S> implements 可迭代<S>{
    / / property values
    private static final String PREFIX = "META-INF/services/";
    // The class or interface representing the service being loaded
    private final Class<S> service;
    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;
    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
    // Initialize the Loader
  	public static <S> ServiceLoader<S> load(Class<S> service) {... }private ServiceLoader(Class<S> svc, ClassLoader cl) {... }// The LazyIterator class
    private class LazyIterator implements Iterator<S> {... }// Iterator
  	public Iterator<S> iterator(a) {...}
}
Copy the code

Initialize the

Serviceloader.load (spiservice.class) essentially calls:

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    // The interface to be loaded
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // Configure the classloader
    loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Access controlacc = (System.getSecurityManager() ! =null)? AccessController.getContext() :null;
    // Clear lookupIterator and initialize LazyIterator: new LazyIterator(service, loader);
    reload();
}
Copy the code

The class objects to be loaded by the SpiService. Class interface are lazily loaded. After initialization, the ServiceLoader internal lookupIterator holds the lazy-loaded iterator.

Call the iteration

public Iterator<S> iterator(a) {
    return new Iterator<S>() {
        / /...
          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 boolean hasNext(a) {
            if (knownProviders.hasNext())
                return true;
            // Call the method of the LazyIterator
            return lookupIterator.hasNext();
        }
        public S next(a) {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        / /...
    };
}
Copy the code

This is a forward, actually calling the iteration method of the LazyIterator.

Start iterative acquisition (core)

LazyIterator hasNext and next actually call hasNextService and nextService respectively.

  private boolean hasNextService(a) {
      // nextName is used to store the fully qualified class name of the interface implementation class
      if(nextName ! =null) {
          return true;
      }
      if (configs == null) {
          try {
              String fullName = PREFIX + service.getName();
              // If the class loader is not obtained
              if (loader == null)
                  / / from the specified path < / / meta-inf/services/work. Lollipops. Tutorial. Java SpiService > below loading configuration
                  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 is also an iterator. Parse is parsing a file and returning what is read from the file
          pending = parse(service, configs.nextElement());
      }
      // The implementation class of the interface is fully qualified
      nextName = pending.next();
      return true;
  }
Copy the code

HasNextService is read our configuration / / meta-inf/services/work. Lollipops. Tutorial.. Java SpiService files, iterative reads the configuration of the fully qualified class name inside.

   private S nextService(a) {
       if(! hasNextService())throw new NoSuchElementException();
       String cn = nextName;
       nextName = null;
       Class < ? > c = null;
       try {
           // Reflect the loading 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 {
           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
   }
Copy the code

NextService creates a concrete implementation class through reflection and returns it.

In essence, a class configured in a fixed location is specified by reflection loading

SPI Application Scenarios

JDBC is used as an example.

JDBC

Before JDBC4.0, you need to write class.forname (” XXX “) to load the mysql driver implementation. After JDBC4.0, you need to discover the driver provider based on spi. The JDK by METAINF/services/Java. SQL. Driver file specifies the implementation class to load the Driver implementation class.

  • JDBC4.0 before
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
//....
Copy the code

The forName is hard coded, so it’s not very elegant. What we pursue is interface oriented programming, like this:

  • After JDBC4.0
//Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
//....
Copy the code

The main load logic is in DriverManager. We can see that DriverManager has a static code block:

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

The loadInitialDrivers method loads the driver implementation class through the SPI mechanism:

private static void loadInitialDrivers(a) {
/ /...
  					. / / spi call load Driver class in METAINF/services/Java, SQL. The Driver specified class
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
/ /...
    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

Our mysql-connector-java.jar specifies

In essence, DriverManager helps us to configure the driver class from the mysql-connector-java.jar.

thinking

The JDK is the standardiser, and it specifies that anything that uses Java programs to read databases, for example, needs to comply with the JDBC standard. When the standard comes out, the major data manufacturers need to provide their own database implementation for this standard. Mysql-connector-java. jar is the mysql database manufacturers to implement JDK JDBC standard mysql implementation. (It is impossible for the JDK to provide a Driver implementation, it can only provide a specification, otherwise it is impossible to put various database vendors implementation in the JDK!)

Before THE SPI mechanism, there was no way to write a Java program to automatically discover the implementation that mysql was providing us, or when we wanted to load the mysql driver, We need to know what the fully qualified class name of the Driver provided by mysql (i.e. Com.mysql.cj.jdbc.driver) is in order to load it.

With the SPI mechanism, we don’t need to specify manually on the JDK side. This is done by the mysql provider JAR package. We just need to agree that ** specified configuration must be placed under METAINF/services/. ** This way, the JDK itself can load the implementation classes for that directory.

So SPI brings us benefits: by introducing third-party packages such as mysql-connector-java.jar, we can do without any hard coding such as class.forname (” com.mysql.cj.jdbc.driver “); You can use com.mysql.cj.jdbc.driver. The JDK itself can find that implementation class.

Break parental delegation?

Remember what a parent delegate is: when a classloader needs to load a.class file, it first delegates the task to its parent classloader, recurses the operation, and only loads the class itself if the parent classloader doesn’t.

Take JDBC again as an example: Driver and DriverManager are both in the JDK. They are loaded by the BootstrapClassLoader, while com.mysql.cJ.jdbc. Driver is a third-party implementation. It is loaded by the AppClassLoader system class loader.

We can implement:

System.out.println(DriverManager.class.getClassLoader());
System.out.println(Driver.class.getClassLoader());
System.out.println(Connection.class.getClassLoader());
System.out.println(conn.getClass().getClassLoader());
// Return the result
null
null
null
sun.misc.Launcher$AppClassLoader@18b4aac2
Copy the code

You can see that the Driver and DriverManager and Connection are loaded through the BootstrapClassLoader (Java could not get the loader so null is returned). But the classloader for Conn.getClass () is AppClassLoader.

The JVM first receives a class load request from DriverManager and delegates it up to the BootstrapClassLoader, which loads the Connection and Driver. However, the Driver implementation classes are provided by various vendors. If these implementation classes are placed in the JDK, it is not a problem. The BootstrapClassLoader will load them. However, these are not under the JDK lib and BootstrapClassLoader cannot load them.

When The BootstrapClassLoader loads DriverManager, DriverManager internally uses classes in the JDK directory. All DriverManager classes should be loaded by the BootstrapClassLoader (that is, the entire process of loading DriverManager should be done by the BootstrapClassLoader, because these classes are in the JDK lib). But the Connection we get is loaded by the AppClassLoader. This means that the BootstrapClassLoader delegates its child AppClassLoader to load the third party driver class while loading DriverManager. So SPI breaks the parent delegation mechanism (the parent can only delegate to the parent, and the parent can not be loaded by the child, but in this case, the parent delegated the App loader to load the third party package, i.e. the parent delegated the child!). .

However, we output:

System.out.println(conn.getClass());
/ / output
class com.mysql.cj.jdbc.ConnectionImpl
Copy the code

It turns out that the Connection class is essentially a com.mysql class! “Loading a third party class by the AppClassLoader does not seem to violate the model”, which is the point of contention that SPI breaks delegation. That makes sense.

But from a classloading point of view, in terms of parent delegation, I think it’s broken! Here’s why:

  • DriverManager is a JDK thing that is loaded by BootstrapClassLoader. It is not possible for the DriverManager loaded by the BootstrapClassLoader to get the implementation class loaded by the AppClassLoader. It is not visible to the BootstrapClassLoader.
  • We can imagine that these class loaders are like nesting dolls, with the BootstrapClassLoader at the very bottom, and the DriverManager classes at the very bottom. When it loads DriverManager, it only loads what it can see in JDK /lib. But SPI is different. When DriverManager is loaded, the third party package is loaded, which is the outermost part of the doll. This clearly does not conform to the rules of nesting dolls.
  • The purpose of the parent delegate is to repeatedly load the class and prevent the core class from being overwritten. In appearance, the CORE Driver and Connection of the JDK seem to be overwritten by an outer third party implementation.
  • And mainly because the parent delegate model is not a mandatory model, Java passes through a thread context classloadersetContextClassLoader()The default is the application class loader and thenThread.current.currentThread().getContextClassLoader()Get the class loader to load.
public static <S> ServiceLoader<S> load(Class<S> service) {
    // Set the context classloader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
Copy the code

We can load classes by getting a ClassLoader from the ThreadContext. This is the equivalent of a hole in the nesting doll. The hole is a pipe and the only thing inside the pipe is the loader (usually the AppClassLoader).

“Reference”

  • www.zhihu.com/question/49…
  • Segmentfault.com/a/119000002…