Make it a habit to like it first

SPI, also known as the Service Provider Interface, is a Service discovery mechanism. The essence of SPI is to configure the fully qualified name of the interface implementation class in a file that is read by the service loader to load the implementation class. This lets you dynamically replace the implementation class for the interface at run time. Because of this feature, we can easily extend our programs through SPI mechanisms.

This article is mainly features & usage introduction, does not involve the source code analysis (source code is very simple, I believe you must understand a read)

What’s SPI good for?

For example, we’ve designed a new logging framework: super-Logger. By default, we use XML file as the configuration file of this log, and design a configuration file parsing interface:

package com.github.kongwu.spisamples;

public interface SuperLoggerConfiguration {
	void configure(String configFile);
}
Copy the code

Then come up with a default XML implementation:

package com.github.kongwu.spisamples;

public class XMLConfiguration implements SuperLoggerConfiguration{
	public void configure(String configFile){... }}Copy the code

So when we initialize and parse the configuration, all we need to do is call this XMLConfiguration to parse the XML configuration file.

package com.github.kongwu.spisamples;

public class LoggerFactory {
	static {
        SuperLoggerConfiguration configuration = new XMLConfiguration();
		configuration.configure(configFile);
    }
    
    public static getLogger(Class clazz){... }}Copy the code

This completes a basic model and looks fine. It doesn’t scale very well though, because if I wanted to customize/extend/rewrite the parsing, I would have to redefine the entry code, and the LoggerFactory would have to be rewritten. It’s not flexible enough, it’s too intrusive.

For example, if the user/user wants to add a YML file as a log configuration file, it just needs to create a New YAMLConfiguration and implement SuperLoggerConfiguration. But… How do you do that? How do you get LoggerFactory to use the new YAMLConfiguration? What if LoggerFactory was rewritten?

If you use the SPI mechanism, this is very simple, can easily complete the extension function of the entry.

Let’s take a look at how the JDK’s SPI mechanism addresses the above extensibility issues.

JDK SPI

An SPI functionality is provided in the JDK, and the core class is java.util.Serviceloader. This allows you to obtain multiple configuration implementation files under “meta-INF /services/” by class name.

Spread in order to solve the above problem, now we under the meta-inf/services/create a com. Making. Kongwu. Spisamples. SuperLoggerConfiguration file (no suffix). File is only one line of code, and that is our default com. Making. Kongwu. Spisamples. XMLConfiguration (note that a file can also write multiple implementations, enter space)

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:

com.github.kongwu.spisamples.XMLConfiguration
Copy the code

Then get the implementation class for our SPI mechanism configuration via ServiceLoader:

ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;

while(iterator.hasNext()) {
    // Load and initialize the implementation class
	configuration = iterator.next();
}

// Call configure on the last Configuration class
configuration.configure(configFile);
Copy the code

Finally, adjust the LoggerFactory initialization configuration to the current SPI mode:

package com.github.kongwu.spisamples;

public class LoggerFactory {
	static {
        ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
        Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
        SuperLoggerConfiguration configuration;

        while(iterator.hasNext()) {
            configuration = iterator.next();// Load and initialize the implementation class
        }
        configuration.configure(configFile);
    }
    
    public static getLogger(Class clazz){... }}Copy the code

Wait, why iterator? As opposed to just getting one instance like get?

What does SPI mean if it’s a fixed get method and it gets a fixed instance?

The purpose of SPI is to enhance scalability. The fixed configuration is extracted and configured through the SPI mechanism. In that case, there is usually a default configuration, and then different implementations are configured through SPI files, so there is the problem of multiple implementations of one interface. If multiple implementations are found, which implementation should be used as the last instance?

So we use iterator here to get all the implementation class configuration. We just added the default SuperLoggerConfiguration implementation to our super-Logger package.

To support YAML configuration, now add a SPI configuration of YAMLConfiguration to the user/user code:

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:

com.github.kongwu.spisamples.ext.YAMLConfiguration
Copy the code

Using the iterator method, you get the default XMLConfiguration and the YAMLConfiguration implementation class that we extended.

In the loaded code above, we iterate over iterator, and at the end of the iteration, we ** use the last implementation configuration as the final instance.

Again, and so on? The last one? How about the last one?

Must this user/user – defined YAMLConfiguration be the last one?

Depending on our ClassPath configuration at runtime, the jar loaded first comes first, and the jar loaded last comes later. So if the user’s package is further down in the ClassPath than the super-Logger’s package, it will be last; If the user’s package location is first, the so-called last one is still the default XMLConfiguration.

For example, if our program’s startup script is:

java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main
Copy the code

The default XMLConfiguration SPI configuration is in super-logger.jar, and the extended YAMLConfiguration SPI configuration file is in main.jar, The last element iterator gets must be YAMLConfiguration.

But what if the classpath order is reversed? Main. jar comes first and super-logger.jar comes last

java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main
Copy the code

So the last element iterator gets is the default XMLConfiguration again, so there’s no point in using JDK SPI, and we get the first one again, the default XMLConfiguration.

Since the load order (classpath) is user-specified, whether we load the first one or the last one, it is possible that the user-defined configuration will not be loaded.

So this is also a disadvantage of the JDK SPI mechanism. There is no way to determine which implementation to load, or to load a specific implementation, and relying solely on the order of the ClassPath is a very loose approach

Dubbo SPI

Dubbo loads all the components through the SPI mechanism. However, instead of using Java’s native SPI mechanism, Dubbo has enhanced it to better meet its needs. In Dubbo, SPI is a very important module. Based on SPI, we can easily extend Dubbo. If you want to learn Dubbo source, SPI mechanism must understand. Next, let’s take a look at the Java SPI and Dubbo SPI usage, and then analyze Dubbo SPI source code.

Dubbo implements a new SPI mechanism that is both more powerful and a bit more complex. The logic is encapsulated in the ExtensionLoader class, through which we can load the specified implementation class. The configuration file required by the Dubbo SPI must be saved in the meta-INF/Dubbo directory. The configuration content is as follows (the following demo is from the Dubbo official document).

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
Copy the code

Unlike the Java SPI implementation class configuration, Dubbo SPI is configured through key-value pairs so that we can load the specified implementation class on demand. You also need to annotate the @spi annotation on the interface when you use it. Here’s how to use the Dubbo SPI:

@SPI
public interface Robot {
    void sayHello(a);
}

public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello(a) {
        System.out.println("Hello, I am Optimus Prime."); }}public class Bumblebee implements Robot {

    @Override
    public void sayHello(a) {
        System.out.println("Hello, I am Bumblebee."); }}public class DubboSPITest {

    @Test
    public void sayHello(a) throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); }}Copy the code

The biggest difference between Dubbo SPI and JDK SPI is the support for “aliases,” which allow you to obtain fixed extension points by aliasing an extension point. As in the example above, I can get an implementation alias “optimusPrime” from Robot’s multiple SPI implementations, as well as an implementation alias “bumblebee”, which is very useful!

You can also default to an “alias” implementation through the value attribute of the @spi annotation. For example, in Dubbo, the default is Dubbo private protocol: Dubbo protocol – Dubbo :// **

@SPI("dubbo")
public interface Protocol {... }Copy the code

On the Protocol interface, we add an @spi annotation with a value of Dubbo. When we get the implementation through SPI, we get the implementation alias Dubbo from the Protocol SPI configuration. Com. Alibaba. Dubbo, RPC Protocol file is as follows:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol


dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol


injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper
Copy the code

Then just get getDefaultExtension to get the extension implementation of the value on the @spi annotation

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol
Copy the code

There’s also Adaptive, which is very flexible, but… Usage is not very “elegant”, so I won’t cover it here

Dubbo’s SPI also has a “load priority”, which is loaded first internal, then external, in order of priority. If there is a repetition, the load will be skipped.

It would be unwise to override the built-in extensions with the classpath load order, for the same reason – the load order is not strict

Spring SPI

The SPI configuration file for Spring is a set of meta-inf/spring.Factories () files, which are very simple to use.

// Get the LoggingSystemFactory configured in all factories files
List<LoggingSystemFactory>> factories = 
    SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);
Copy the code

The following is a configuration of spring.factories in Spring Boot

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver

......
Copy the code

In Spring SPI, you put all your configuration into one fixed file, eliminating the need to configure a lot of files. As for the extension configuration of multiple interfaces, it is a matter of opinion whether it is better to use a single file or a single file for each one (I prefer Spring for its cleanliness).

Spring’s SPI belongs to the Spring-Framework (Core), but is currently mainly used in Spring Boot.

Like the previous two SPI mechanisms, Spring supports multiple Spring. factories in your ClassPath and loads them in the same order as the ClassPath. Add to an ArrayList. Since there are no aliases, there is no concept of de-duplication, adding as many as you can.

However, since Spring’s SPI is mainly used in Spring Boot, the ClassLoader in Spring Boot preferentially loads files in the project, rather than files in dependent packages. So if you define a Spring. factories file in your project, the files in your project will be loaded first, and so will the implementation class configured in the spring.factories file

If you want to extend an interface, just create a new meta-INF/Spring. factories file in your spring Boot and add only the configuration you want. Instead of copying the entire Spring. factories file from Spring Boot and changing it to ** I just want to add a new LoggingSystemFactory implementation, Then I just need to create a new meta-INF /spring.factories file instead of copying and modifying it:

org.springframework.boot.logging.LoggingSystemFactory=\
com.example.log4j2demo.Log4J2LoggingSystem.Factory
Copy the code

contrast

JDK SPI DUBBO SPI Spring SPI
The file A separate file for each extension point A separate file for each extension point All extension points in one file
Get some fixed implementation Not supported, all implementations can only be obtained sequentially There is the concept of an “alias” that lets you get a fixed implementation of an extension point by name, handy with annotations from Dubbo SPI Not supported, all implementations can only be obtained sequentially. However, since Spring Boot ClassLoader loads files in user code first, it is guaranteed that the user’s custom Spring. factoires file is the first one, and the custom extension is always available by retrieving the first factory
other There is no Supports dependency injection inside Dubbo. Internal SPIs and external SPIs are distinguished by directories. Internal spIs are loaded first to ensure that the internal one has the highest priority There is no
Document integrity Articles & materials from three parties are abundant Documentation & tripartite information is abundant The documentation is not rich enough, but with few features, it is very simple to use
IDE support There is no There is no IDEA perfect support, with grammar tips

Compared with the three SPI mechanisms, the built-in JDK mechanism is the weakest, but because it is built-in JDK, so there are certain application scenarios, after all, no additional dependencies; Dubbo has the most features, but the mechanics are a bit complex and can only be used with Dubbo, not completely a standalone module; Spring has almost the same functionality as the JDK, except that all extension points are written in a spring.Factories file, which is an improvement, and IDEA supports syntax hints perfectly.

Which of the three SPI mechanisms is better, JDK/Dubbo/Spring? Leave a comment in the comments section

reference

  • Introduction to the Service Provider Interfaces – Oracle
  • Dubbo SPI – Apache Dubbo
  • Creating Your Own Auto-configuration – Spring

Original is not easy, unauthorized reprint prohibited. If my article helped you, please like/bookmark/follow it ❤❤❤❤❤❤