How does JavaSPI work?

1. The opening words

This article focuses on the implementation of a Java SPI-based demo and analysis of its implementation principle, that is, ServiceLoader class source code analysis.

In fact, the reason why I wanted to write this article is that I was asked a question about Java SPI by the interviewer in a previous interview, but failed to give him a satisfactory answer. Therefore, I wanted to sort out an article on SPI and consolidate my knowledge of parental delegation mechanism.

2. SPI briefly

SPI stands for Service Provider Interface, which translates to Service Provider Interface. It is a service provision discovery mechanism built into Java that simply adds the implementation of the corresponding interface to the environment variable and the program automatically loads the class and uses it.

SPI has good scalability. The framework sets the rules (interfaces), and specific vendors provide the implementation (implementation interfaces). If you want to switch implementations, you just need to put another vendor’s implementation in environment variables without changing the code, which is clearly a strategic pattern.

Let’s implement a simple SPI-based demo.

2. Steps for Java to implement SPI

2.1 Defining Interfaces

First of all, you need to define an interface, which is called the “standard”, and manufacturers are based on this standard interface implementation, such as the IMPLEMENTATION of MySQL and Oracle JDBC standard interface.

In this demo, the standard of the HelloSpi interface I defined is to implement the SAY method, whose core function is to output a paragraph of text based on the SAY method.

public interface HelloSpi {
    /** * spi interface method */
    void say(a);
}
Copy the code

2.2 Creating an implementation class for the interface

Create two implementation classes HelloInEnglish and HelloInChinese, each printing a line of hello statements.

public class HelloInChinese implements HelloSpi {
    @Override
    public void say(a) {
        System.out.println("From HelloInChinese: Hello"); }}public class HelloInEnglish implements HelloSpi {
    @Override
    public void say(a) {
        System.out.println("from HelloInEnglish: hello"); }}Copy the code

One thing to note here is that the implementation class must have a no-argument constructor, otherwise an error will be reported, because the ServiceLoader uses the no-argument constructor when creating an instance of the implementation class, as explained in the code below.

2.3 Creating a Fully qualified Interface Name Configuration meta-file

The next step is in the resources directory to create a meta-inf/services folder, and then create a name for HelloSpi interface fully qualified name of the file, in my project is org. Walker. Planes. Spi. HelloSpi.

The contents of the file are the fully qualified names of the two implementation classes you just created, with each line representing one implementation class.

org.walker.planes.spi.HelloInEnglish
org.walker.planes.spi.HelloInChinese
Copy the code

2.4 Using the ServiceLoader to load classes in the configuration file

Create a test Class with a main method, call ServiceLoader#load(Class) to load the corresponding Class, and execute.

public class SpiMain {
    public static void main(String[] args) {
        // Load the implementation class of the HelloSpi interface
        ServiceLoader<HelloSpi> shouts = ServiceLoader.load(HelloSpi.class);
        // Execute the say method
        for(HelloSpi s : shouts) { s.say(); }}} The command output is as follows: FROM HelloInEnglish: hello from HelloInChinese: HelloCopy the code

At this point, we’ve implemented a simple demo based on the Java SPI mechanism,

At this point, a simple demo implementation based on the Java SPI mechanism is complete. In this example, we can see that, in addition to the configuration file, the more important Class is the ServiceLoader, which is loaded into the interface implementation Class by calling the ServiceLoader#load(Class) method and running the SAY method.

JavaSPI is most likely based on the ServiceLoader class. Let’s look at how the ServiceLoader class loading interface implements classes.

3. ServiceLoader source code analysis

I think you already know how it works from the above demo implementing the Java SPI. Here’s how the ServiceLoader class finds and loads the implementation class based on the interface class.

3.1 ServiceLoader# load (Class)

Step 4: load(ServiceLoader#load(Class))

// Load method in the example main method
public static <S> ServiceLoader<S> load(Class<S> service) {
    // Get the thread context class loader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // Call an overloaded method to load the Service target class through the CL thread context classloader
    return ServiceLoader.load(service, cl);
}

// load overload methods
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
    return new ServiceLoader<>(service, loader);
}
Copy the code

As described in the comment lock in the code above, the thread context classloader is first obtained by calling the ServiceLoader#load(Class) method, and the target Class is then loaded through the thread context classloader.

This brings us to the JVM’s parent delegate mechanism and Thread Context ClassLoader.

3.2 JVM parent delegation mechanism

Believe everyone knows the JVM parents delegate mechanism, namely when the class loader receives the request of the class loading, it won’t go to try to load the class, but to delegate the request to parent to complete, only when the parent class loader feedback they can’t finish the load request, the child loader will try to load the classes.

Generally, we refer to class loaders including Bootstrap ClassLoader, Extension ClassLoader, AppClassLoader and Custom ClassLoader This).

The launcher class loader is responsible for loading Java’s core classes, including:

  • Classes in JAVA_HOME\lib
  • The path specified by the -xbootCLASspath parameter
  • A class library that the VM can recognize, such as rt.jar and tools.jar

There is also the application class loader, which loads the libraries specified on the user’s classpath.

Now that we know about the parent delegate mechanism and the startup class loader, let’s go back to SPI. As mentioned in the previous article, SPI is a set of standard interfaces provided by the JDK and implemented by the vendor. When the implementation is present in the environment variable, classes can be automatically loaded to use the vendor’s implementation.

It is worth noting that the JDK provides standard SPI interfaces that are typically bundled with the core codebase, such as the JDBC Driver driver.class interface, in the RT.jar package. That is to say, the SPI interface is to start the class loader to load, if the appointment mechanism, based on the traditional parents is by starting class loader to load the vendor implementation classes, this time you will find that manufacturer implementation class is in the classpath, should be the application class loader loads, and cannot be started class loader loads.

Thread context class loading was born out of this dilemma and is also a way to break the parent delegate mechanism.

3.3 Thread Context class loader

The Thread context classloader is a property of the Thread class that is used to cache the current Thread’s classloader.

In the ServiceLoader#load(Class) method, the thread context Class loader of the current thread is first obtained. In the example code, the thread executing this method is the main thread, and the Class loader loading the main thread is the application Class loader.

The application class loader is responsible for loading the class libraries specified on the classpath. The current project is of course in the Classpath path, so using the application class loader to load the implementation classes of the SPI interface can be successfully loaded.

Next, let’s look at how the ServiceLoader class loads the SPI interface implementation class based on thread context class loading.

3.4 Source tracing

In the load overload method, you create an instance of the ServiceLoader class, passing in parameters to the target interface Hellospi. class and the classloader AppClassLoader.

In the constructor of the ServiceLoader class, there are also some actions, as noted in the code below.

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    // Check if the target interface class is null and throw a NullPointerException if it is
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    
    // If cl is null, the system class loader (application class loader) is used by default.
    loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Java security management related, not detailed in this articleacc = (System.getSecurityManager() ! =null)? AccessController.getContext() :null;
    
    // Clear the cache provider providers and reload all SPI interface implementation classes
    reload();
}
Copy the code

The ServiceLoader#reload() method does a cleanup of the ServiceLoader class’s private providers member and creates a LazyIterator that takes the target interface class and the class loader.

// Cached providers, in instantiation order
// The cache provider actually stores the SPI interface implementation class object, with key as the implementation class name and value as the SPI interface implementation class instance
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
// We load iterators lazily. We create objects at the beginning of the iteration and then put them into providers
private LazyIterator lookupIterator;
    
public void reload(a) {
    / / clear the will
    providers.clear();
    // Create lazy-loaded iterators
    lookupIterator = new LazyIterator(service, loader);
}
Copy the code

At this point, the ServiceLoader class is ready to load the SPI implementation class. When the program iterates through the ServiceLoader object in the for loop, it actually calls the hasNext method of the Iterator interface. The final call is the LazyIterator#hasNext() method mentioned in the previous step, and if true is returned, the next method is called to begin the iteration.

public boolean hasNext(a) {
    // When the Java access control context is null, the hasNextService method is called
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run(a) { returnhasNextService(); }};returnAccessController.doPrivileged(action, acc); }}Copy the code

Acc == null will be true, and the LazyIterator#hasNextService() method will be called, which will parse files in the meta-inf /services directory. Generate the SPI interface implementation class iterator and set the nextName property, as described in the comments in the code below.

/ / Github:https://github.com/Planeswalker23
private boolean hasNextService(a) {
    NextName == null the first time you enter this method
    if(nextName ! =null) {
        return true;
    }
    The configs attribute represents an Enumeration object of URL type, null the first time this method is entered
    if (configs == null) {
        try {
            // PREFIX = "meta-INF /services/" // PREFIX =" meta-inf /services/
            // The service attribute is the service object passed in when we create the LazyIterator, the SPI interface class hellospi.class
            // So the fullName variable is meta-INF /services/HelloSpi, which represents the file name created in that directory
            String fullName = PREFIX + service.getName();
            // If the classloader is null, the configuration file is loaded through the system classloader
            // If the value is not null, the configuration file is loaded through the class loader
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x); }}// The pending iterator is null the first time this method is entered
    while ((pending == null) | |! pending.hasNext()) {If there is no data in the configuration file, return false
        if(! configs.hasMoreElements()) {return false;
        }
        // Store the full path class name in the configuration file as an iterator in the pending attribute, with each line representing an element in the pending attribute
        pending = parse(service, configs.nextElement());
    }
    // Set the nextName attribute to the next element of the pending iterator
    nextName = pending.next();
    return true;
}
Copy the code

After executing the hasNextService method, the SPI configuration file has been parsed, the iterator of the SPI standard interface implementation class has been generated, and the assignment of the next implementation class name nextName has been completed. The final return is true, indicating that the iterator has the next value. Next calls the iterator’s next method, and finally LazyIterator#next(), which actually calls LazyIterator#nextService(). In this method, the class is loaded, instantiated, and so on. See the code comments below for details.

/ / Github:https://github.com/Planeswalker23
private S nextService(a) {
    // Not the first call to hasNextService, which assigns pending.next() to nextName
    if(! hasNextService())throw new NoSuchElementException();
    // Marks the name of the current class to load and instantiate
    String cn = nextName;
    // Then set the nextName attribute to null
    nextName = null; Class<? > c =null;
    try {
        // call Class#forName to load the class using cn, the false attribute, and the class loader
        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 successfully loaded class and force it to type HelloSpi
        S p = service.cast(c.newInstance());
        // Place the instantiated object in the providers property
        providers.put(cn, p);
        // Return the instantiated object, implementing the class logic in the for loop by calling the say method
        return p;
    } catch (Throwable x) {
        fail(service, "Provider " + cn + " could not be instantiated", x);
    }
    throw new Error();          // This cannot happen
}
Copy the code

This completes the source analysis of the entire process by which the ServiceLoader class implements the Java SPI mechanism based on the thread context classloader.

3.5 Why does the SPI interface implementation class need a no-parameter constructor

In 2.2 Creating An Interface implementation Class, I mentioned that an implementation class must have a parameter-free constructor, and I’ll look at why.

It’s actually quite simple. Instantiating loaded classes in the LazyIterator#nextService() method is done using the Class#newInstance() method, the source code for which is shown below.

@CallerSensitive
public T newInstance(a) {
    // omit most of the code...Class<? >[] empty = {};// Call getConstructor0 to get the constructor with no arguments. NoSuchMethodException is reported if no constructor is found
    final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
    
    // omit most of the code...
}

// get the no-argument constructor Class# gettor0
private Constructor<T> getConstructor0(Class<? >[] parameterTypes,int which) throws NoSuchMethodException {
    // Get all the constructors of the class to iterate over
    Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
    for (Constructor<T> constructor : constructors) {
        // Returns the no-argument constructor
        if (arrayContentsEq(parameterTypes, constructor.getParameterTypes())) {
            returngetReflectionFactory().copyConstructor(constructor); }}throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
}
Copy the code

As the code above shows, when instantiating a class, we get the constructor by calling Class# gettor0, which gets the no-argument constructor, which is why implementing a class must have a no-argument constructor.

4. Summary

This article implements a simple demo based on the Java SPI mechanism, and then analyzes the source code of the ServiceLoader class based on the thread context classloader to implement the Java SPI.

This is the conclusion of the section on Java SPI. If you have any other supplementary information, please let me know. Let’s study together.

I hope I can help you.

5. Reference materials

  • SPI mechanisms in Java that must be understood by advanced development
  • In-depth understanding of the Java virtual machine

Finally, this article is included in the Personal Speaker Knowledge Base: Back-end technology as I understand it, welcome to visit.