Deep understanding of ServiceLoader class and SPI mechanism

Keywords: Programming MySQL Java JDBC Dubbo

Recently, we are reconstructing projects ourselves. In order to meet the 82 principle (I hope 80% of businesses can be fixed by exhausting, and only 20% of businesses allow special definitions), after fixing some standard processes, for example, we have enlarged the capability of atomic services. When we enlarge the capability of atomic services, you will find that, although it is an abstract thing However, when it comes to actual implementation, it is found that they are still different.

Here to solve a different implementation, but the same process of the problem, as well as teamwork. SPI (Service Provider Interface) introduced by us.

Use cases

In general, service loader is used to implement SPI mechanism. The full name of SPI (Service Provider Interface) is a built-in service provision discovery mechanism in JDK. SPI is a dynamic replacement discovery mechanism. For example, there is an interface. If you want to add an implementation to it dynamically at runtime, you only need to add one.

SPI mechanism can be summarized as follows:

If you have read the source code or some blog articles, it is probably clear that SPI is widely used in some open source projects, such as MySQL connector Java, dubbo, etc.

Let's take a look at an SPI implementation of MySQL

The interface in JDBC is: java.sql.Driver

The implementation core class of SPI mechanism is: java.util.ServiceLoader

Provider: com.mysql.jdbc.Driver

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

Write a simple SPI

Code part, interface and implementation class definition:

package com.vernon.test.spi;

/**
 * Created with vernon-test
 *
 * @description:
 * @author: chenyuan
 * @date: 2020/4/2
 * @time: 11:08 morning
 */
public interface IRepository {
    void save(String data);
}

package com.vernon.test.spi.impl;

import com.vernon.test.spi.IRepository;

/**
 * Created with vernon-test
 *
 * @description:
 * @author: chenyuan
 * @date: 2020/4/2
 * @time: 11:09 morning
 */
public class MongoRepository implements IRepository {
    @Override
    public void save(String data) {
        System.out.println("Save " + data + " to Mongo");
    }
}

package com.vernon.test.spi.impl;

import com.vernon.test.spi.IRepository;

/**
 * Created with vernon-test
 *
 * @description:
 * @author: chenyuan
 * @date: 2020/4/2
 * @time: 11:08 morning
 */
public class MysqlRepository implements IRepository {

    @Override
    public void save(String data) {
        System.out.println("Save " + data + " to Mysql");
    }

}

Call function definition:

package com.vernon.test.spi;

import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * Created with vernon-test
 *
 * @description:
 * @author: chenyuan
 * @date: 2020/4/2
 * @time: 11:12 morning
 */
public class SPIMain {

    public static void main(String[] args) {
        ServiceLoader<IRepository> serviceLoader = ServiceLoader.load(IRepository.class);
        Iterator<IRepository> it = serviceLoader.iterator();
        while (it != null && it.hasNext()) {
            IRepository demoService = it.next();
            System.out.println("class:" + demoService.getClass().getName());
            demoService.save("tom");
        }
    }

}

Execution result:

Connected to the target VM, address: '127.0.0.1:58517', transport: 'socket'
class:com.vernon.test.spi.impl.MongoRepository
Save tom to Mongo
class:com.vernon.test.spi.impl.MysqlRepository
Save tom to Mysql
Disconnected from the target VM, address: '127.0.0.1:58517', transport: 'socket'

Internal implementation logic of ServiceLoader class

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);
}

The initialization of the SercviceLoader is finished after running the above code. But in fact, the relationship between the interface to be implemented and the class implementing the interface is not only completed in the process of constructing the ServiceLoader class, but also implemented in the iterator's method hasNext().

Implementation of dynamic call

The internal logic of the forEach statement written in the use case is the iterator. The important method of the iterator is hasNext():

ServiceLoader is a class that implements the Iterable interface.

Source code of hasNext() method:

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);
    }
}

Throw complex operations to ensure security. You can think of the above code as a call to a method: hasNextService

Source code of the hasNextService() method:

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            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;
}

The most important code blocks in the above codes are:

String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);

Here PREFIX is a constant string (used to specify the directory where the configuration file is placed, using a relative path, indicating that the upper directory is a folder named by the project name):

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

Then fullName will be assigned as: META-INF/services/com.vernon.test.spi.IRepository

Then call the method getSystemResources or getResources to regard the fullName parameter as URL and return the URL set of the configuration file.

pending = parse(service, configs.nextElement());

The parse method parses the configuration file with parameter 1: Class object of the interface and parameter 2: URL of the configuration file. The return value is an iterator that contains the contents of the configuration file, that is, an iterator that implements the full name (package name + Class name) string of the Class;

Finally, the following code is invoked to get the completed class path string and relative path of the class to load. In the use case, this value can be:

com.vernon.test.spi.impl.MongoRepository
com.vernon.test.spi.impl.MysqlRepository

This is just a way for the iterator to determine whether there is another iteration element, and the method to get each iteration element is: nextService() method.

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 {
        // instantiation
        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
}

summary

1. The idea of SPI: interface oriented programming is realized by dynamic loading mechanism, which improves the separation of framework and underlying implementation; 2. The SPI implementation method provided by ServiceLoader class can only obtain the instance object of Provider by traversing and iterating. If the implementation class of multiple interfaces is registered, it is inefficient; 3. Although it is returned by static method, every call of Service.load method will generate a ServiceLoader instance, which is not a single instance design pattern; 4. ServiceLoader is similar to ClassLoader, which can be responsible for certain class loading, but the former only loads specific classes, that is, it requires to implement specific implementation classes of Service interface; the latter can load almost all Java classes; 5. There are two key points to understand the mechanism of SPi:

  • Understand the process of dynamic loading, know how the configuration file is used, and finally find the class file under the relevant path, and load it;
  • Understand SPI design pattern: separation of interface framework and underlying implementation code;

6. The reason why the iterator object inside the ServiceLoader class is called LazyInterator is that when the ServiceLoader object is created, there is no reference to relevant elements inside the iterator. Only when it is truly iterated, can it parse, load and finally return relevant classes (Iterated elements);

Reference address

If you like my article, you can pay attention to the personal subscription number. Welcome to leave a message and communicate at any time. If you want to join the wechat group, please add the administrator's short stack culture - small assistant (lastpass4u), who will pull you into the group.

Posted by alexinjamestown on Sat, 04 Apr 2020 01:02:54 -0700