Java SPI mechanism from principle to practice

Keywords: Java Back-end

1. What is SPI

1. Background

In the object-oriented design principle, it is generally recommended to program based on the interface between modules. Generally, the caller module will not perceive the internal implementation of the called module. Once the code involves specific implementation classes, it violates the opening and closing principle. If you need to replace an implementation, you need to modify the code.

In order to realize that there is no need to dynamically specify in the program during module assembly, a service discovery mechanism is needed. Java SPI provides such a mechanism: a mechanism to find a service implementation for an interface. This is a bit similar to the idea of IOC, which transfers the control of assembly outside the program.

SPI is Service Provider Interface in English, which literally means "Service Provider Interface". My understanding is that it is an interface specially provided for service providers or developers who extend framework functions.

SPI separates the service interface from the specific service implementation, decouples the service caller from the service implementer, and can improve the scalability and maintainability of the program. Modifying or replacing the service implementation does not require modifying the caller.

2. Usage scenario

Many frameworks use Java SPI mechanism, such as database loading driver, log interface, dubbo extension implementation and so on.

3. What is the difference between SPI and API

When it comes to SPI, we have to talk about API s. In a broad sense, they all belong to interfaces and are easy to be confused. Let's use a diagram to illustrate:

Generally, modules communicate through interfaces, so we introduce an "interface" between service callers and service implementers (also known as service providers).

When the implementer provides an interface and implementation, we can call the implementer's interface to have the capabilities provided by the implementer. This is API. This interface and implementation are placed on the implementer.

When the interface exists on the caller's side, it is SPI. The interface caller determines the interface rules, and then different manufacturers implement the rules to provide services. For example, company H is a technology company, which has newly designed a chip, and now it needs mass production, There are several chip manufacturing companies on the market. At this time, as long as company H specifies the chip production standard (defines the interface standard), these cooperative chip companies (service providers) will deliver their own characteristic chips according to the standard (provide the implementation of different schemes, but the results are the same).

2. Practical demonstration

The log service SLF4J provided by the Spring framework is actually just a log facade (Interface), but there are several specific implementations of SLF4J, such as Logback, Log4j, Log4j2, etc., and can be switched. When switching the log implementation, we do not need to change the project code, but only need to modify some pom dependencies in Maven dependencies.

This is implemented by relying on SPI mechanism. Next, we will implement a simple version of logging framework.

1. Service Provider Interface

Create a new Java project service provider interface directory structure as follows:

├─.idea
└─src
    ├─META-INF
    └─org
        └─spi 
            └─service
                ├─Logger.java
                ├─LoggerService.java
                ├─Main.java
                └─MyServicesLoader.java

Create a new Logger interface, which is SPI and service provider interface. Later service providers will implement this interface.

package org.spi.service;

public interface Logger {
    void info(String msg);

    void debug(String msg);
}

Next is the LoggerService class, which mainly provides specific functions for service consumers (callers). If you have doubts, you can look back first.

package org.spi.service;

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

public class LoggerService {
    private static final LoggerService SERVICE = new LoggerService();

    private final Logger logger;

    private final List<Logger> loggerList;

    private LoggerService() {
        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
        List<Logger> list = new ArrayList<>();
        for (Logger log : loader) {
            list.add(log);
        }
        // LoggerList is all serviceproviders
        loggerList = list;
        if (!list.isEmpty()) {
            // Logger only takes one
            logger = list.get(0);
        } else {
            logger = null;
        }
    }

    public static LoggerService getService() {
        return SERVICE;
    }

    public void info(String msg) {
        if (logger == null) {
            System.out.println("info Not found in Logger Service provider");
        } else {
            logger.info(msg);
        }
    }

    public void debug(String msg) {
        if (loggerList.isEmpty()) {
            System.out.println("debug Not found in Logger Service provider");
        }
        loggerList.forEach(log -> log.debug(msg));
    }
}

Create a new Main class (service consumer, caller) and start the program to view the results.

package org.spi.service;

public class Main {
    public static void main(String[] args) {
        LoggerService service = LoggerService.getService();

        service.info("Hello SPI");
        service.debug("Hello SPI");
    }
}

Program results:

No Logger service provider found in info
No Logger service provider was found in debug

Package the whole program directly into a jar package. You can package the project into a jar package directly through the IDEA.

2. Service Provider

Next, create a new project to implement the Logger interface

The directory structure of the new project service provider is as follows:

├─.idea
├─lib
│   └─service-provider-interface.jar
└─src
    ├─META-INF
    │   └─services
    │       └─org.spi.service.Logger
    └─org
        └─spi
            └─provider
                 └─Logback.java

Create a new Logback class

package org.spi.provider;

import org.spi.service.Logger;

public class Logback implements Logger {

    @Override
    public void info(String msg) {
        System.out.println("Logback info Output of:" + msg);
    }

    @Override
    public void debug(String msg) {
        System.out.println("Logback debug Output of:" + msg);
    }
}

Import the jar of service provider interface into the project.
Create a new lib directory, then copy the jar package and add it to the project.


Then click OK.

Next, you can import some classes and methods in the jar package in the project, just like the JDK tool class guide package.

Implement the Logger interface, create a META-INF/services folder in the src directory, and then create a file org.spi.service.Logger (full class name of SPI). The content in the file is org.spi.provider.Logback (full class name of Logback, i.e. package name + class name of SPI implementation class).

This is the standard agreed by the JDK SPI mechanism ServiceLoader

Next, the service provider project is also packaged into a jar package, which is the implementation of the service provider. Generally, the pom dependency we import into maven is a bit like this, but we have not published this jar package to the maven public warehouse, so we can only manually add it to the project where it needs to be used.

3. Effect display

Next, return to the service provider interface project.

Import the service provider jar package and rerun the Main method.
The operation results are as follows:

Output of Logback info: Hello SPI
Output of Logback debug: Hello SPI

Note the implementation class imported into the jar package has taken effect.

By using SPI mechanism, we can see that the coupling between LoggerService and service provider is very low. If you need to replace one implementation (replace Logback with another implementation), you only need to change a jar package. Isn't this the SLF4J principle?

If the requirements change one day, you need to output the log to the message queue or do some other operations. At this time, you don't need to change the implementation of Logback at all. You only need to add a service provider. You can add an implementation in this project or import a new service implementation jar package from the outside. We can select a specific service provider in the loggerservice to complete the operations we need.

loggerList.forEach(log -> log.debug(msg));
perhaps
loggerList.get(1).debug(msg);
loggerList.get(2).debug(msg);

It needs to be understood here: when loading a specific service implementation, ServiceLoader will scan the contents of META-INF/services in the src directory under all packages, and then generate corresponding objects through reflection and save them in a list, so you can get the service implementation you need through iteration or traversal.

3. ServiceLoader

If you want to use the SPI mechanism of Java, you need to rely on ServiceLoader. Next, let's see how ServiceLoader does it:

ServiceLoader is a tool class provided by JDK, which is located in package java.util; Under the bag.

A facility to load implementations of a service.

This is the official note given by JDK: a tool for loading service implementation.

Looking down, we find that this class is a final type, so it cannot be inherited and modified. At the same time, it implements the Iterable interface. The iterator is implemented so that we can get the corresponding service implementation through iteration.

public final class ServiceLoader<S> implements Iterable<S>{ xxx...}

You can see a familiar constant definition:

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

The following is the load method: it can be found that the load method supports two overloaded input parameters;

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<S> service,
                                        ClassLoader loader) {
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

According to the calling order of the code, it is implemented through an internal class lazyitterer in the reload() method. Keep looking down first.

After the ServiceLoader implements the method of the iteratable interface, it has the ability of iteration. When this iterator method is called, it will first search in the Provider cache of ServiceLoader. If there is no hit in the cache, it will search in lazyitterer.

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String, S>> knownProviders
                = providers.entrySet().iterator();

        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext(); // Call lazyitrator 
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next(); // Call lazyitrator 
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

When calling lazyitterer, the specific implementation is as follows:

public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() {
                return hasNextService();
            }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //Obtain the corresponding configuration file through PREFIX (META-INF/services /) and class name to obtain the specific implementation class
            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;
}


public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() {
                return nextService();
            }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private S nextService() {
    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
}

4. Summary

In fact, it is not difficult to find that the specific implementation of SPI mechanism is essentially completed through reflection. That is, according to the regulations, the specific implementation classes to be exposed for external use are declared under META-INF/services /.

In fact, SPI mechanism is applied in many frameworks: the basic principle of Spring framework is a similar reflection. The dubbo framework also provides the same SPI extension mechanism.

The flexibility of interface design can be greatly improved through SPI mechanism, but SPI mechanism also has some disadvantages, such as:

  1. Traverse and load all implementation classes, so the efficiency is relatively low;
  2. When multiple service loaders load at the same time, there will be concurrency problems.

Posted by munchy on Thu, 25 Nov 2021 20:11:25 -0800