demand

The SPI mechanism is actually derived from the Service Provider Framework. It is a mechanism that separates Service interfaces and Service implementations to achieve decoupling and greatly improves the extensibility of programs.

The introduction of a service provider is an implementer who introduces an SPI interface, obtains a concrete implementation class through local registry discovery, and can easily plugable scenarios: for some services to be provided to a third party implementation or extension, which can enhance the framework extension or replace some components

design

features

  • Advantages:

    The advantage of using the Java SPI mechanism is decoupling, so that the assembly control logic of the third-party service module is separated from the caller’s business code, rather than coupled together. Applications can enable framework extensions or replace framework components depending on the actual business situation.

  • Disadvantages:

    • Although ServiceLoader is used lazily, it can only be acquired by traversal, where the implementation classes of the interface are loaded and instantiated once.
    • If you don’t want to use an implementation class, it’s also loaded and instantiated, which is wasteful. The method of obtaining an implementation class is not flexible enough. It can only be obtained in the form of Iterator, and the corresponding implementation class cannot be obtained according to a certain parameter.
    • It is unsafe for multiple concurrent threads to use instances of the ServiceLoader class.

Usage scenarios

In a nutshell, this applies to: the implementation strategy of the framework is enabled, extended, or replaced by the caller depending on the actual usage needs

Common examples:

  • The database driver loading interface implements the loading of classes for JDBC to load drivers of different types of databases

  • SLF4J loads logging implementation classes from different vendors

  • Spring

    Spring in the heavy use of SPI, such as: the implementation of ServletContainerInitializer servlet3.0 specification, automatic Type Conversion Type Conversion SPI (Converter SPI, the Formatter SPI), etc

  • Dubbo

    Dubbo also uses the SPI approach heavily to extend the framework, but it wraps the native SPI provided by Java, allowing users to extend the Filter interface

Conventions for the SPI mechanism

    1. Create a file in the Meta-INF /services/ directory in the Java /main/resources directory of the Maven project. This file contains the fully qualified name of the Api implementation class
    1. Use the ServiceLoader class to dynamically load the implementation classes in the Meta-INF
    1. If the SPI implementation class is Jar, it needs to be placed in the main program classPath
    1. There must be an Api concrete implementation classA constructor with no parameters

Usage

  • Define interfaces and implementation classes
//1 Define an interface: IOperation

 public interface IOperation {
        public int operation(int numberA, int numberB);
    }

//2. Define the addition implementation
 public class PlusOperationImpl implements IOperation {
        public int operation(int numberA, int numberB) {
            returnnumberA + numberB; }}//3. Define the division implementation
 public class DivisionOperationImpl implements IOperation{
        public int operation(int numberA, int numberB) {
            returnnumberA / numberB; }}Copy the code
  • Defining test Classes
public class MainTest {
    public static void main(String[] args) {
        ServiceLoader<IOperation> operations = ServiceLoader.load(IOperation.class);
        Iterator<IOperation> operationIterator = operations.iterator();
        while (operationIterator.hasNext()) {
            IOperation operation = operationIterator.next();
            System.out.println(operation.operation(6.3)); }}}Copy the code
  • Create a new file under the directory meta-INF /services and name it com.jd.spi.IOperation with the fully qualified name of the interface. Written to realize the fully qualified implementation class (package name + class name) com. Ns. Spi. DivisionOperationImpl

implementation

The code analysis

  • ServiceLoad Load process

ServiceLoader implements Iterable, and calls to its static method load create a ServiceLoader object and return it

public static <S> ServiceLoader<S> load(Class<S> var0) {//1. Call the load method
    ClassLoader var1 = Thread.currentThread().getContextClassLoader();// Thread contextClassLoader is used by default
    return load(var0, var1);
}
public static <S> ServiceLoader<S> load(Class<S> var0, ClassLoader var1) {
    return new ServiceLoader(var0, var1);//2. Instantiate ServiceLoader object
}
private ServiceLoader(Class<S> var1, ClassLoader var2) {
    this.service = (Class)Objects.requireNonNull(var1, "Service interface cannot be null");
    this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2;
    this.reload();//3. Invoke the reload method
}
public void reload(a) {
    this.providers.clear();
    this.lookupIterator = new ServiceLoader.LazyIterator(this.service, this.loader);//4. Instantiate the internal lazy-loaded lookupIterator
}
Copy the code

Call the serviceLoader. The iterator (). HasNext, execute serviceLoader. LazyIterator hasNext method

public boolean hasNext(a) {
    if (this.nextName ! =null) {
        return true;
    } else {
        if (this.configs == null) {
            try {
                String var1 = "META-INF/services/" + this.service.getName();//1. Find a configuration file whose address is meta-INF /services/$ServiceClassName
                if (this.loader == null) {
                    this.configs = ClassLoader.getSystemResources(var1);
                } else {
                    this.configs = this.loader.getResources(var1); }}catch (IOException var2) {
                ServiceLoader.fail(this.service, "Error locating configuration files", var2); }}while(this.pending == null || !this.pending.hasNext()) {
            if (!this.configs.hasMoreElements()) {
                return false;
            }
            this.pending = ServiceLoader.this.parse(this.service, (URL)this.configs.nextElement());//2. Read the serviceImpl name written in the configuration file address
        }
        this.nextName = (String)this.pending.next();
        return true; }}Copy the code

If hasNext returns true, next is called to get the instantiated object of the interface implementation

public S next(a) {
    if (!this.hasNext()) {
        throw new NoSuchElementException();
    } else {
        String var1 = this.nextName;//1. Obtain the name of the serviceImpl class for next
        this.nextName = null;
        Class var2 = null;
        try {
            var2 = Class.forName(var1, false.this.loader);//2. Reflect to get the Class object
        } catch (ClassNotFoundException var5) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " not found");
        }

        if (!this.service.isAssignableFrom(var2)) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " not a subtype");
        }

        try {
            Object var3 = this.service.cast(var2.newInstance());//3. NewInstance Obtains the Object
            ServiceLoader.this.providers.put(var1, var3);
            return var3;
        } catch (Throwable var4) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " could not be instantiated: " + var4, var4);
            throw newError(); }}}Copy the code

The principle of

Break the parent delegate loading mechanism

    1. The first “break” to the parent delegate model is overwriting the custom loader loadClass(), which the JDK does not recommend. Typically, you just override findClass() so that the parent delegate mechanism is preserved. The loadClass method is defined by its own rules, so you can load classes as you like
    1. Parents delegation model for the second time “destroyed” is the ServiceLoader and Thread setContextClassLoader ()

    The parent delegate model has some drawbacks. The parent delegate model does a good job of solving the problem of uniform base classes across class loaders (the more basic classes are loaded by the more upper-level loaders). Base classes are called “base” because they are always called as apis by the calling code. But what if the base class calls the user’s code again?

    This is not impossible. A typical example is the JNDI service, whose code is loaded by a bootstrap class loader (put in rt.jar at JDK1.3), but the purpose of JNDI is to centrally manage and lookup resources. What if it calls the JNDI Interface Provider (SPI) code deployed by the independent vendor implementation under the application’s CLASspath, but it’s impossible to “recognize” the code by starting the classloader?

    To solve this dilemma, the Java design team had to introduce a less elegant design: the Thread Context ClassLoader. This classloader can be set via the setContextClassLoader() method of the java.lang.Thread class. If it was not set when the Thread was created, it will inherit one from the parent Thread. If none is set globally for the application, the classloader defaults to the application classloader. With thread context class loader, JNDI service using the thread context class loader for the need of SPI code, which is the parent class loader requested subclasses loader to finish class loading action, this kind of behavior is actually created parents delegation model hierarchy to reverse the use of class loaders, has violated the parents delegation model, But there’s nothing you can do about it. This is basically how all the SPI loading actions in Java, such as JNDI,JDBC,JCE,JAXB, JBI, etc.

// Traditional loading mode 1
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("JDBC: mysql: / / 127.0.0.1:33061 / XXX? useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"."root"."123456");

// Traditional loading mode 2
System.setProperty("jdbc.drivers"."com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("JDBC: mysql: / / 127.0.0.1:33061 / XXX? useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"."root"."123456");

//SPI loading mode
//DriverManager has a static block loadInitialDrivers(). This method calls serviceloader.load (Class service, Look for the java.sql.Driver implementation class in the ClassPath: Meta-INF/Services folder
Connection connection = DriverManager.getConnection("JDBC: mysql: / / 127.0.0.1:33061 / XXX? useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"."root"."123456");
Copy the code

The sample code

Github.com/ns7381/java…