Recently we himself in the reconstruction project, the system in order to conform to the principle of 82 (hope is 80% of the business can be fixed by means of exhaustion, only 20% of the allows the definition of special), then after some fixed standard process, such as our ability to enlarge the atomic services, when the amplification atomic service ability, you will find that, Although abstractly seen as a matter of the same meaning, but to the actual implementation of the discovery is still different.

Here to solve a different implementation, but the same process of the problem, as well as the problem of team collaboration. We introduced the Service Provider Interface (SPI).

Use case

Typically, a ServiceLoader is used to implement the SPI mechanism. SPI (Service Provider Interface) is a built-in Service discovery mechanism in JDK. SPI is a mechanism for dynamic substitution discovery. For example, if you have an interface and you want to dynamically add an implementation to it at runtime, you just need to add an implementation.

The SPI mechanism can be summarized as follows:

If you’ve read the source code or read some blog posts, you probably know that SPI is used extensively in open source projects such as mysql-connector-Java, Dubbo, etc.

Let’s briefly look at an SPI implementation of MySQL

The JDBC interface is java.sql.Driver

The core implementation class of the SPI mechanism is java.util.Serviceloader

Provider: com.mysql.jdbc.driver

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
Copy the code

Let me just write an SPI

Code section, interface and implementation class definitions:

package com.vernon.test.spi;

/**
 * Created with vernon-test
 *
 * @description:
 * @author: chenyuan
 * @date: 2020/4/2
 * @time11:08 am */
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
 * @time11:09 am */
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
 * @time11:08 am */
public class MysqlRepository implements IRepository {

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

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
 * @time11:12 am */
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"); }}}Copy the code

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'
Copy the code

The internal implementation logic of the 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(a) {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}
Copy the code

The initialization of SercviceLoader is complete. But the actual relationship between the interface to be implemented and the class that implements it is not just done during the construction of the ServiceLoader class, but is implemented in the iterator’s method hasNext().

Implementation of dynamic invocation

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

ServiceLoader is a class that implements the Iterable interface.

HasNext () method source:

public boolean hasNext(a) {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run(a) { returnhasNextService(); }};returnAccessController.doPrivileged(action, acc); }}Copy the code

To throw a complex security operation, think of the above code as calling the method hasNextService.

HasNextService () method

private boolean hasNextService(a) {
    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;
}
Copy the code

The more important code blocks in the above code are:

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

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

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

So fullName will be assigned to: meta-inf/services/com. Vernon. Test. The spi. IRepository

The method getSystemResources or getResources is then called to treat the fullName parameter as a URL and return a collection of urls for the configuration file.

pending = parse(service, configs.nextElement());
Copy the code

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

Finally, the following code is called to get the completion classpath string, relative path, of the following class to be loaded. In a use case, this value could be:

com.vernon.test.spi.impl.MongoRepository
com.vernon.test.spi.impl.MysqlRepository
Copy the code

This is simply a way for the iterator to determine if there are any elements for the next iteration, and the nextService() method is used to get the elements for each iteration.

public S next(a) {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run(a) { returnnextService(); }};returnAccessController.doPrivileged(action, acc); }}private S nextService(a) {
    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 {
        / / instantiate
        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
}
Copy the code

conclusion

1. Philosophy of SPI: Interface oriented programming is implemented through dynamic loading mechanism, which improves the separation between framework and underlying implementation; 2. The SPI implementation method provided by the ServiceLoader class can only be achieved by iterating through the method to obtain the instance object of the Provider. If multiple interface implementation classes are registered, it is inefficient; 3. Each call to the service. load method generates an instance of ServiceLoader, although it is returned by a static method, outside the singleton design pattern. A ServiceLoader is similar to a ClassLoader in that it can be used to load certain classes. However, a ServiceLoader simply loads specific classes that implement the Service interface. The latter can load almost any Java class; 5. There are two main points to understand about the SPi mechanism:

  • Understand the dynamic loading process, know how configuration files are used, and finally find the relevant path of the class file, and load;
  • understandSPIDesign pattern: separation of interface framework and underlying implementation code;

6. LazyInterator: The iterator inside the ServiceLoader class is called LazyInterator because when the ServiceLoader object is created, there are no reference elements inside the iterator. The iterator is parsed, loaded, and returned only when the iterator is iterated.

Refer to the address

  • Blog.csdn.net/li_xunhuan/…
  • Dubbo.apache.org/zh-cn/blog/…

If you like my article, you can follow the individual subscription number. Welcome to leave messages and communicate at any time. If you want to join the wechat group to discuss with us, please add the administrator to simplify the stack culture – little Assistant (lastpass4U), he will pull you into the group.