Tell me about your understanding of SPI in Java

preface

Recently, I was asked about SPI in the interview, but I didn't answer. It was mainly for my own reason. I took myself to the ditch, because I talked about the parent delegation model of class loader. Later, I was asked about the scenarios that destroyed the parent delegation model. Then I said that the modularization of SPI, JNDI and JDK9 destroyed the parent delegation.
Then I was asked. Tell me about your understanding of SPI in Java. Then I was confused. I just knew that it would destroy the parental delegation and what was going on before, but I didn't have a deep understanding. So I'll sum up this knowledge this time.

What is SPI

SPI is the full name of Service Provider Interface, which literally means the interface to provide services. To explain in detail, it is a set of interfaces provided by Java to be implemented or extended by a third party, which realizes the dynamic expansion of the interface, so that the implementation classes of the third party can be embedded into the system like plug-ins.

Eh...
This explanation feels a little tongue twisty.
Let's talk about its essence.

Configure the fully qualified name of the implementation class of the interface in the file (the file name is the fully qualified name of the interface), and the service loader reads the configuration file and loads the implementation class. It realizes the dynamic replacement of implementation classes for interfaces at run time.

SPI example

Let's give an example.
We create a project and then create a module called SPI interface.

In this module, we define an interface:

/**
 * @author jimoer
 **/
public interface SpiInterfaceService {

    /**
     * Print parameters
     * @param parameter parameter
     */
    void printParameter(String parameter);
}

Define a module called SPI service one, POM XML relies on SPI interface.
Define an implementation class in SPI service one to implement SpiInterfaceService interface.

package com.jimoer.spi.service.one;
import com.jimoer.spi.app.SpiInterfaceService;

/**
 * @author jimoer
 **/
public class SpiOneService implements SpiInterfaceService {
    /**
     * Print parameters
     *
     * @param parameter parameter
     */
    @Override
    public void printParameter(String parameter) {
        System.out.println("I am SpiOneService:"+parameter);
    }
}

Then create the directory META-INF/services under the resources directory of SPI service one, create a file name in this directory, which is the fully qualified name of SpiInterfaceService interface, and write the file content into the fully qualified name of SpiOneService implementation class.
The effect is as follows:

Create another module named SPI service one, which also depends on SPI interface, and define an implementation class SpiTwoService to implement SpiInterfaceService interface.

package com.jimoer.spi.service.two;
import com.jimoer.spi.app.SpiInterfaceService;
/**
 * @author jimoer
 **/
public class SpiTwoService implements SpiInterfaceService {
    /**
     * Print parameters
     *
     * @param parameter parameter
     */
    @Override
    public void printParameter(String parameter) {
        System.out.println("I am SpiTwoService:"+parameter);
    }
}

The directory structure is as follows:

Next, create another module for testing, named SPI app.
pom.xml relies on SPI service one and SPI service two

<dependencies>
    <dependency>
        <groupId>com.jimoer.spi</groupId>
        <artifactId>spi-service-one</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.jimoer.spi</groupId>
        <artifactId>spi-service-two</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

Create test class

/**
 * @author jimoer
 **/
public class SpiService {

    public static void main(String[] args) {

        ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);
        Iterator<SpiInterfaceService> iterator = spiInterfaceServices.iterator();
        while (iterator.hasNext()){
            SpiInterfaceService sip = iterator.next();
            sip.printParameter("parameter");
        }
    }
}

Execution result:

I am SpiTwoService:parameter
 I am SpiOneService:parameter

Through the running results, we can see that all implementations of SpiInterfaceService interface have been loaded into the current project and the call has been executed.

From the whole code structure, we can see that SPI mechanism puts the assembly of modules outside the program, that is, the implementation of the interface can be outside the program, and only the specific implementation needs to be specified when it is used. And dynamically loaded into their own projects.
The main purpose of SPI mechanism:
One is to decouple the interface from the concrete implementation;
The second is to improve the scalability of the framework. In the past, when writing a program, the interface and implementation were written together. When using, the caller relied on the interface to call, and had no right to choose to use a specific implementation class.

Implementation of SPI

So let's take a look at how SPI is implemented?
From the above example, we can see that the core code of SPI mechanism is the following paragraph:

ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);

Let's take a look at serviceloader Source code of load () method:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

See thread currentThread(). getContextClassLoader(); I understand what's going on. This is the thread context class loader, because the thread context class loader is created to reverse the parent delegation model of class loading.

Using this thread context class loader to load the required SPI service code is a behavior that the parent class loader requests the child class loader to complete class loading. This behavior actually opens up the hierarchy of the parent delegation model to use the class loader in reverse, which has violated the general principles of the parent delegation model, but it is also helpless.
Deep understanding of Java virtual machine (Third Edition)

Although we know that it destroys parental delegation, we still need to look at the specific implementation.

Find the specific method to implement hasNext() in ServiceLoader, then continue to look at the implementation of this method.

The hasNext() method mainly calls the hasNextService() method.

// Fixed path
private static final String PREFIX = "META-INF/services/";

private boolean hasNextService() {
     if (nextName != null) {
         return true;
     }
     if (configs == null) {
         try {
         	// Fixed path + fully qualified name of interface
             String fullName = PREFIX + service.getName();
             // If the current thread context class loader is empty, the parent class loader (application class loader by default) will be used
             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());
     }
     // In the next() method, it is used to judge whether the current class has been materialized
     nextName = pending.next();
     return true;
 }

It is mainly to load the fully qualified name file of the interface under META-INF/services / path, and then find the class path of the implementation class to load the implementation class.

Continue to see how the iterator takes out each implementation object. It depends on the next() method of the iterator implemented in ServiceLoader.

The next() method is mainly implemented by nextService(), so continue to look at the nextService() method.

private S nextService() {
     if (!hasNextService())
         throw new NoSuchElementException();
     String cn = nextName;
     nextName = null;
     Class<?> c = null;
     try {
     // Load the class directly without initialization (because hasNext() above has already been initialized).
         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 the loaded class out of the object.
         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
 }

You can see here how to create objects. First, load the implementation class of the interface in hasNext() and judge whether there is an implementation class of the interface, and then instantiate the implementation class in the next() method.

In fact, there are many functions using SPI mechanism in Java, such as JDBC, JNDI, Spring, and even in RPC framework (Dubbo).

Tags: Java

Posted by kooza on Tue, 03 May 2022 12:03:44 +0300