What is the operating principle of Java SPI mechanism?

Keywords: Java Spring

What is SPI technology

The full name of SPI is Service Provider Interface, which means Service Provider Interface. If we don't write framework code or develop plug-ins, we may not be so familiar with SPI mechanism, but if we read such as Dubbo and JDBC database Driver package, Spring and the recently popular Spring Boot related starter component source code, you will find that SPI mechanism and its ideas have a large number of applications in these frameworks.

We know that each abstract module in the system often has many different implementation schemes, such as our common log module scheme, xml parsing module, JDBC driver module, etc. In the object-oriented design idea, we generally recommend the docking between modules based on interface oriented programming, rather than direct implementation oriented hard coding. Because once the coupling of specific implementation classes is involved in the code, it violates the principles of pluggable, closed and open. If you need to replace an implementation method, you need to modify the code. If we want to realize that the module assembly can not be specified in the program hard coding, we need a service discovery mechanism (PS: don't confuse it with the current micro service service service discovery mechanism).

SPI technology in JAVA provides such a mechanism to find service implementation classes for an interface, which is similar to the IOC idea in the Spring framework, that is, to move the control of program loading and assembly outside the program. This mechanism is very important in component modular design! So how is the SPI mechanism specifically agreed in JAVA?

In the JAVA SPI mechanism, it is agreed that when the service provider (such as a new log component) provides a certain implementation of the service interface, a file named after the service interface is created in the META-INF/services / directory of the jar package, and the fully qualified class name of the specific implementation class implementing the service interface is filled in the file. In this way, when the external program assembles the module, you can find the specific implementation class path through the configuration file in the META-INF/services / directory in the jar package, so that you can load and instantiate the module through the reflection mechanism, so as to complete the injection of the module. For example, when we introduce the Dubbo framework, in addition to the core dependency jar, there are many extension components, such as Dubbo monitor. If we need to introduce this component, we only need to simply introduce it without additional integration. The main reason is that this component is integrated in SPI mechanism.

To sum up, SPI mechanism is actually a dynamic loading mechanism realized by the combination of "interface based programming + policy mode + configuration file". A tool class: "java.util.ServiceLoader" is provided in JDK to realize service search.

JDK SPI mechanism implementation example

The built-in support of SPI mechanism in JDK mainly involves the use of "java.util.ServiceLoader" class. Next, let's understand the implementation of SPI mechanism in JAVA through a simple code example! Let's take a look at what specifications need to be followed when using JAVA SPI mechanism through a diagram:

└── src/main/java
├── cn
│   └── wudimanong
│       └── spi
│           ├── SPIService.java
│           ├── impl
│           │   ├── SpiImpl1.java
│           │   └── SpiImpl2.java
│           └── SPIMain.java
└── META-INF
    └── services
        └── com.wudimanong.spi.SPIService

1. We need to create the directory META-INF\services in the project

2. Define an interface service provider, such as SpiService

public interface SpiService {
    void execute();
}

3. Define two service interface implementation classes, such as SpiImpl1 and SpiImpl2

public class SpiImpl1 implements SPIService {
    @Override
    public void execute() {
        System.out.println("SpiImpl1 Hello.");
    }
}
//#############################################
public class SpiImpl2 implements SPIService {
    @Override
    public void execute() {
        System.out.println("SpiImpl2 Hello.");
    }
}

4. We add a configuration file under the ClassPath path. The name of the file is the fully qualified class name of the interface, and the content is the fully qualified class name of the implementation class. If there are multiple implementation classes, it is separated by a newline character. The file path is as follows

 

The contents of the document are as follows:

cn.wudimanong.spi.impl.SpiImpl1
cn.wudimanong.spi.impl.SpiImpl2

In this way, we basically follow the mechanism of JAVA SPI and define the basic structure of components. Finally, we write test classes to see how the client code should be written if the SPI mechanism is used:

 public class SPIMain {
    public static void main(String[] args) {

        ServiceLoader<SPIService> loaders =
                ServiceLoader.load(SPIService.class);

        Iterator<SPIService> spiServiceIterator = loaders.iterator();
        System.out.println("classPath:" + System.getProperty("java.class.path"));
        while (spiServiceIterator.hasNext()) {
            SPIService spiService = spiServiceIterator.next();
            System.out.println(spiService.execute());
        }
    }
}

It can be seen that the importing party loads the component module through the ServiceLoader class. If we open the source code of this class, we can see that its main implementation logic is actually to find the implementation class in the META-INF/services / directory and perform relevant instantiation operations. Some key source codes of this class are as follows:

public final class ServiceLoader<S> implements Iterable<S>{
    //Path to configuration file
    private static final String PREFIX = "META-INF/services/";
    //Loaded service class or interface
    private final Class<S> service;
    //Loaded service class collection
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //Class loader
    private final ClassLoader loader;
    //Internal class, real load service class
    private LazyIterator lookupIterator;

    public void reload() {
        //Empty first
        providers.clear();
        //Instantiate inner class
        lookupIterator = new LazyIterator(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //Interface to load
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //Class loader
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //access controller
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //reload method is as above
        reload();
    }
}

The later process of finding classes and creating implementation classes is completed in the lazyitterer class. Due to the space, interested friends can click the source code to read it by themselves!

Analysis of SPI mechanism in JDBC database driver package

Through the above description, I believe you should have a basic understanding of the implementation of Java SPI mechanism. Next, let's look at the real application scenario of Java SPI mechanism with JDBC database driven design. We know that usually each big data Library vendors (such as Mysql and Oracle) will develop their own driver implementation logic according to a unified specification, such as java.sql.Driver.

When we use JDBC, the client does not need to change the code. We can directly introduce different SPI interface services. For example, take Mysql's jdbc driver jar as an example:

In this way, after introducing the mysql driver package, the jdbc connection code java.sql.DriverManager will use the SPI mechanism to load the specific jdbc implementation. The key source code is as follows:

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
}

You can see the pink code. The JDBC driver management code loads the specific Mysql JDBC driver through the native Java SPI mechanism.

Analysis of SPI mechanism in Dubbo framework

It should be noted that although Java provides support for the default implementation of SPI mechanism, it does not mean that all frameworks will use this Java built-in logic by default. SPI mechanism is more an implementation idea, and the specific implementation logic can be defined by itself. For example, we say that SPI technology is widely used in the Dubbo framework, but Dubbo does not use the JDK native ServiceLoader, but implements the ExtensionLoader to load extension points. Therefore, when we look at the source code of the Dubbo framework, we must not be confused by the configuration directory being / META-INF/dubbo/internal instead of META-INF/services /.

Accordingly, if other frameworks also use custom SPI mechanism implementation, don't be confused. They just redefine the loading logic similar to ServiceLoader class, and the design idea and principle behind them are the same! For example, take the code of uninstalling the protocol in Dubbo as an example:

 private void destroyProtocols() {
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

The configuration path scanned in ExtensionLoader is as follows:

public class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

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

    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

    private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");

    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();

    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
}

Through the above analysis of the examples of SPI mechanism in Java and the application of SPI mechanism in Dubbo and JDBC driver framework, I believe you should have an overall understanding of the principle. If you need a deeper understanding of the implementation logic of some details, you need to take a good look at the source code of ServiceLoader. If other frameworks implement SPI mechanism separately, their corresponding implementation loading tool classes also need to see how their source code is implemented!

reference material:

https://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html

https://blog.csdn.net/qiangcai/article/details/77750541

https://www.cnblogs.com/lovesqcc/p/5229353.html

--—END--—

This article is from WeChat official account.   Invincible code farmer (Jiangqiao dege), author: Invincible code farmer

The source of the original text and reprint information are detailed in the text. If there is infringement, please contact us   yunjia_community@tencent.com   Delete.

Original publication time: May 9, 2019

Posted by mguili on Mon, 01 Nov 2021 19:58:21 -0700