Understanding SPI Mechanisms in Java, Spring, and Dubbo

Click on the “Yudao Source Code“, and select “Set as Favorite”

What about the previous wave or the latter wave?

The waves that can surf are the good waves!

Every day 10:33 updates articles, losing a little bit of hair every day…

Quality Source Code Column

  • Original | Java 2021 Super God Road, very intense~

  • Open-source projects with detailed Chinese comments

  • RPC framework Dubbo source code analysis

  • Network application framework Netty source code analysis

  • Message middleware RocketMQ source code analysis

  • Database middleware Sharding-JDBC and MyCAT source code analysis

  • Job scheduling middleware Elastic-Job source code analysis

  • Distributed transaction middleware TCC-Transaction source code analysis

  • Eureka and Hystrix source code analysis

  • Java concurrency source code

Source: Sanyou’s Java Diary
  • What is SPI
  • Java SPI Mechanism — ServiceLoader
  • Spring SPI Mechanism — SpringFactoriesLoader
  • Dubbo SPI Mechanism — ExtensionLoader
  • Summary

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Today, let’s talk about the principles and differences of the SPI mechanisms among Java, Spring, and Dubbo.

I have previously written a similar article, but this article mainly analyzes the source code of Dubbo’s SPI mechanism, briefly introducing the SPI mechanisms of Java and Spring without going in-depth. So this article will delve into the principles and differences among the three.

What is SPI

SPI stands for Service Provider Interface, which is a mechanism for dynamic replacement discovery and an excellent decoupling idea. SPI allows for flexible separation between interfaces and implementations, enabling API providers to only provide interfaces while third parties can implement them. It can also use configuration files to achieve replacement or extension, which is quite common in frameworks, enhancing the framework’s extensibility.

In simple terms, SPI is an excellent design concept focused on decoupling and ease of extension.

A backend management system + user mini-program implemented based on Spring Boot + MyBatis Plus + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflows, third-party login, payment, SMS, e-commerce, and other functions.

  • Project address: https://github.com/YunaiV/ruoyi-vue-pro
  • Video tutorial: https://doc.iocoder.cn/video/

Java SPI Mechanism — ServiceLoader

ServiceLoader is a simple implementation of SPI provided by Java. The Java SPI implementation stipulates two things:

  • Files must be placed under the META-INF/services/ directory
  • The file name must be the fully qualified name of the interface, and the content should be the fully qualified name of the interface implementation

This way, the ServiceLoader can load the implementations of the interfaces from the files.

Here’s a demo

First, we need an interface and its implementation class

public interface LoadBalance {
}

public class RandomLoadBalance implements LoadBalance{
}

Second, create a file with the fully qualified name of LoadBalance in the META-INF/services/ directory, and the file content should be the fully qualified name of RandomLoadBalance.

Understanding SPI Mechanisms in Java, Spring, and Dubbo
Understanding SPI Mechanisms in Java, Spring, and Dubbo

Testing class:

public class ServiceLoaderDemo {

  public static void main(String[] args) {
    ServiceLoader<LoadBalance> loadBalanceServiceLoader = ServiceLoader.load(LoadBalance.class);
    Iterator<LoadBalance> iterator = loadBalanceServiceLoader.iterator();
    while (iterator.hasNext()) {
      LoadBalance loadBalance = iterator.next();
      System.out.println("Obtained load balancing strategy:" + loadBalance);
    }
  }
}

Test result:

Understanding SPI Mechanisms in Java, Spring, and Dubbo

At this point, we have successfully obtained the implementation.

In actual framework design, the above test code is typically written by the framework author internally, while for framework users, to customize the LoadBalance implementation and embed it into the framework, they only need to write the implementation of the interface and the spi file.

Implementation principle

Below is a core piece of code in ServiceLoader

Understanding SPI Mechanisms in Java, Spring, and Dubbo

First, obtain a fullName, which is essentially META-INF/services/fully qualified name of the interface

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Then, use ClassLoader to obtain the resource, which is the resource corresponding to the fully qualified name file of the interface, and pass it to the parse method for parsing the resource

Understanding SPI Mechanisms in Java, Spring, and Dubbo

The parse method reads the file content through IO streams, allowing us to obtain the fully qualified names of the interface implementations.

After that, it essentially uses reflection to instantiate objects, which will not be displayed here.

So it is not difficult to see that the implementation principle of ServiceLoader is relatively simple, summarized as reading the contents of the META-INF/services/fully qualified name of the interface file through IO streams and then reflecting to instantiate objects.

Advantages and Disadvantages

Due to the simplicity of Java’s SPI mechanism implementation, it also has some disadvantages.

The first point is resource wastage. Although the example has only one implementation class, in reality, there may be many implementation classes, and Java’s SPI will instantiate all of them at once, but not all of these implementations may be needed, leading to unnecessary resource wastage.

The second point is the inability to distinguish specific implementations. With so many implementation classes, which one should be used? If you need to determine which specific implementation to use, you can only rely on the design of the interface itself, such as designing the interface as a strategy interface or designing the interface with priorities. However, regardless of how it is designed, the framework author must write code to make the judgment.

So overall, ServiceLoader cannot achieve on-demand loading or obtaining a specific implementation as needed.

Usage Scenarios

Although ServiceLoader may have some drawbacks, it still has usage scenarios, such as:

  • There is no need to choose a specific implementation; each loaded implementation needs to be utilized.
  • Although a specific implementation needs to be selected, it can be resolved through the design of the interface.

A backend management system + user mini-program implemented based on Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflows, third-party login, payment, SMS, e-commerce, and other functions.

  • Project address: https://github.com/YunaiV/yudao-cloud
  • Video tutorial: https://doc.iocoder.cn/video/

Spring SPI Mechanism — SpringFactoriesLoader

Spring is familiar to us; it also provides an SPI implementation called SpringFactoriesLoader.

The stipulations of Spring’s SPI mechanism are as follows:

  • Configuration files must be in the META-INF/ directory, and the file name must be spring.factories
  • The file content consists of key-value pairs, where one key can have multiple values separated by commas, and both keys and values need to be fully qualified class names. Keys and values can have no relationship between classes, but can also have an implementation relationship.

It can be seen that the stipulations for file names and content in Spring’s SPI mechanism differ from those in Java.

Here’s a demo

Create a spring.factories file in the META-INF/ directory, with LoadBalance as the key and RandomLoadBalance as the value.

Understanding SPI Mechanisms in Java, Spring, and Dubbo
Understanding SPI Mechanisms in Java, Spring, and Dubbo

Testing:

public class SpringFactoriesLoaderDemo {

  public static void main(String[] args) {
    List<LoadBalance> loadBalances = SpringFactoriesLoader.loadFactories(LoadBalance.class, MyEnableAutoConfiguration.class.getClassLoader());
    for (LoadBalance loadBalance : loadBalances) {
      System.out.println("Obtained LoadBalance object:" + loadBalance);
    }
  }
}

Run result:

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Successfully obtained the implementation object.

Core Principle

Below is a core piece of code in SpringFactoriesLoader

Understanding SPI Mechanisms in Java, Spring, and Dubbo

From this, it can be seen that it is similar to the Java implementation, but it reads the content of the spring.factories file in the META-INF/ directory, and parses it into key-value pairs.

Usage Scenarios

Spring’s SPI mechanism is widely used internally, especially in Spring Boot, where many extension points during Spring Boot startup are implemented through the SPI mechanism. Here are two examples:

1. Auto-configuration

In versions of Spring Boot prior to 3.0, auto-configuration was loaded using SpringFactoriesLoader.

Understanding SPI Mechanisms in Java, Spring, and Dubbo

However, after Spring Boot 3.0, it no longer uses SpringFactoriesLoader; instead, Spring reads from the META-INF/spring/ directory’s org.springframework.boot.autoconfigure.AutoConfiguration.imports file.

Understanding SPI Mechanisms in Java, Spring, and Dubbo

As for how it reads, it can be guessed that it is similar to the way the above SPI mechanism reads, with only the file path and name differing.

2. Loading PropertySourceLoader

PropertySourceLoader is used to parse application configuration files; it is an interface.

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Spring Boot provides two implementations, PropertiesPropertySourceLoader and YamlPropertySourceLoader, corresponding to parsing properties and yaml file formats.

When loading PropertySourceLoader, Spring Boot uses the SPI mechanism.

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Comparison with Java SPI Mechanism

Firstly, Spring’s SPI mechanism simplifies Java’s SPI mechanism; each interface in Java’s SPI requires a corresponding file, while Spring’s SPI mechanism only requires one spring.factories file.

Secondly, regarding content, Java’s SPI mechanism requires the file content to be the implementation classes of the interfaces, while Spring’s SPI does not require any relationship between the key-value pairs, allowing for greater flexibility.

The third point is that Spring’s SPI mechanism provides a method loadFactoryNames to obtain class names, while Java’s SPI mechanism does not have this feature. By obtaining the class names through this method, these classes can be injected into the Spring container, loading these beans rather than just using reflection.

However, Spring’s SPI also does not implement the ability to obtain a specific implementation class, so to find a specific implementation class, it still relies on the design of the specific interface.

So have you noticed that PropertySourceLoader is actually a strategy interface, as noted, so when your configuration file is in properties format, it can find and parse the PropertiesPropertySourceLoader object to parse the configuration file.

Dubbo SPI Mechanism — ExtensionLoader

ExtensionLoader is the implementation class of the SPI mechanism in Dubbo. Each interface will have its own ExtensionLoader instance, which is similar to Java’s SPI mechanism.

Likewise, Dubbo’s SPI mechanism has the following stipulations:

  • The interface must have the @SPI annotation
  • Configuration files can be placed in META-INF/services/, META-INF/dubbo/internal/, META-INF/dubbo/, and META-INF/dubbo/external/. The file name is also the fully qualified name of the interface.
  • The content is key-value pairs, where the key is a short name (which can be understood as the name of the bean in Spring), and the value is the fully qualified name of the implementation class.

Here’s a demo

First, add the @SPI annotation to the LoadBalance interface

@SPI
public interface LoadBalance {
}

Then, modify the configuration file content during the Java SPI mechanism test to key-value pairs. Since Dubbo’s SPI mechanism can also read files from the META-INF/services/ directory, we won’t rewrite the file here.

random=com.sanyou.spi.demo.RandomLoadBalance

Testing class:

public class ExtensionLoaderDemo {

  public static void main(String[] args) {
    ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
    LoadBalance loadBalance = extensionLoader.getExtension("random");
    System.out.println("Obtained object corresponding to random key:" + loadBalance);
  }
}

By using the getExtension method of ExtensionLoader and passing in the short name, you can accurately find the implementation class corresponding to the short name.

Thus, it can be seen that Dubbo’s SPI mechanism solves the previously mentioned problem of not being able to obtain a specific implementation class.

Test result:

Understanding SPI Mechanisms in Java, Spring, and Dubbo

In addition to solving the problem of not being able to obtain a specific implementation class, Dubbo’s SPI mechanism also provides many additional features, which are widely used internally in Dubbo. Next, let’s discuss them in detail.

Core Mechanisms of Dubbo

1. Adaptive Mechanism

Adaptation means that based on parameters, the specific target class is dynamically selected at runtime and then executed.

Each interface can have only one adaptive class, which can be obtained through the getAdaptiveExtension method of ExtensionLoader. This object can find the target implementation class based on the specific parameters during runtime and then call the methods of the target object.

For example, if the above LoadBalance has an adaptive object, after obtaining this adaptive object, if the key random is passed during runtime, this adaptive object will find the implementation class corresponding to the random key and call the methods of that implementation class. If another key is dynamically passed, it will route to another implementation class.

Adaptive classes can be generated in two ways: the first is by specifying it yourself, by adding the @Adaptive annotation to the implementation class. Then this implementation class becomes the adaptive implementation class.

@Adaptive
public class RandomLoadBalance implements LoadBalance{
}

In addition to specifying it yourself, Dubbo can also dynamically generate an adaptive class based on certain conditions, but the generation process is quite complex, so it will not be elaborated here.

The adaptive mechanism is widely used in Dubbo, and many are automatically generated. If you are unaware of Dubbo’s adaptive mechanism, you might not understand why the code can reach that point when reading the source code.

2. IOC and AOP

When talking about IOC and AOP, people immediately think of Spring, but IOC and AOP are not concepts unique to Spring; Dubbo also implements IOC and AOP functions, but in a lightweight manner.

2.1 Dependency Injection

Dubbo’s dependency injection is done through setter injection, where the injected object is the adaptive object mentioned above. In a Spring environment, Spring Beans can be injected.

public class RoundRobinLoadBalance implements LoadBalance {

  private LoadBalance loadBalance;

  public void setLoadBalance(LoadBalance loadBalance) {
    this.loadBalance = loadBalance;
  }
}

In the above code, RoundRobinLoadBalance has a setLoadBalance method with a LoadBalance parameter. When creating RoundRobinLoadBalance, in a non-Spring environment, Dubbo will find the LoadBalance adaptive object and inject it via reflection.

This approach is also common in Dubbo. For example, in the RegistryProtocol, a Protocol is injected, and this injected Protocol is actually an adaptive object.

Understanding SPI Mechanisms in Java, Spring, and Dubbo

2.2 Interface Callback

Dubbo also provides some interface callback functionalities similar to those in Spring. For instance, if your class implements the Lifecycle interface, it will call several methods during creation or destruction.

Understanding SPI Mechanisms in Java, Spring, and Dubbo

In a certain version after Dubbo 3.x, Dubbo has provided more interface callbacks, such as ExtensionPostProcessor and ExtensionAccessorAware, which are very similar in naming and function to those in Spring.

2.3 Automatic Wrapping

Automatic wrapping is essentially the implementation of AOP functionality, which proxies the target object, and this AOP functionality is enabled by default.

In the implementation of SPI interfaces in Dubbo, there is a special class called Wrapper class, which is responsible for implementing AOP.

The only criterion for identifying a Wrapper class is that it must have a constructor with a single parameter, and the parameter type must be the type of the interface, as shown in the following code:

public class RoundRobinLoadBalance implements LoadBalance {

  private final LoadBalance loadBalance;

  public RoundRobinLoadBalance(LoadBalance loadBalance) {
    this.loadBalance = loadBalance;
  }
}

At this point, RoundRobinLoadBalance is a Wrapper class.

When obtaining the target object RandomLoadBalance through random, by default, it will wrap RandomLoadBalance, and the actual object obtained is actually the RoundRobinLoadBalance object, which internally references RandomLoadBalance.

*Let’s test it*

Add the following to the configuration file:

roundrobin=com.sanyou.spi.demo.RoundRobinLoadBalance

Test result:

Understanding SPI Mechanisms in Java, Spring, and Dubbo

As can be seen from the result, although random was specified, the actual object obtained is RoundRobinLoadBalance, which internally references RandomLoadBalance.

If there are many Wrapper classes, a chain of responsibility will form, one wrapping the other.

Thus, the AOP implementation in Dubbo differs from that in Spring; Spring’s AOP is based on dynamic proxies, while Dubbo’s AOP is essentially static proxies, as Dubbo automatically assembles this proxy, forming a chain of responsibility.

At this point, we already know that the implementation classes of SPI interfaces in Dubbo have two types:

  • Adaptive classes
  • Wrapper classes

In addition to these two types, there is also a default class, which is the implementation class corresponding to the value of the @SPI annotation. For example:

@SPI("random")
public interface LoadBalance {
}

In this case, the implementation class corresponding to the random key is the default implementation, which can be obtained through the getDefaultExtension method.

3. Automatic Activation

Automatic activation refers to dynamically selecting a batch of implementation classes based on your input parameters.

Implementation classes that can be automatically activated need to have the Activate annotation, thus learning about another type of implementation class.

@Activate
public interface RandomLoadBalance {
}

At this point, RandomLoadBalance is classified as an automatically activated class.

The method to obtain automatically activated classes is getActivateExtension, allowing for dynamic selection of a batch of implementation classes based on the parameters of this method.

The automatic activation mechanism is a core usage scenario in Dubbo, particularly in the filter chain of filters.

Filters in Dubbo are extension points that can intercept requests before they are initiated or after responses are obtained, similar to Spring MVC’s HandlerInterceptor.

Understanding SPI Mechanisms in Java, Spring, and DubboSome implementations of filters

As shown, there are many implementations of filters, so to distinguish whether a filter implementation is for the provider or consumer side, the automatic activation mechanism can be used to dynamically select a batch of filter implementations based on input parameters.

For example, the ConsumerContextFilter is a filter that works on the consumer side.

Understanding SPI Mechanisms in Java, Spring, and DubboConsumerContextFilter

Summary

Through the above analysis, we can see that the core principle of implementing the SPI mechanism is to read the specified file’s content through IO streams, parse it, and finally add some of its own features.

In summary, Java’s SPI implementation is relatively simple and does not have other functionalities; Spring benefits from its own IOC and AOP features, so it does not implement a very complex SPI mechanism, only simplifying and optimizing Java’s. However, Dubbo’s SPI mechanism, in order to meet the requirements of its framework, implements many features, integrating both IOC and AOP functionality into the SPI mechanism, as well as providing adaptive injection and automatic activation functionalities.

Welcome to join my knowledge circle to discuss architecture and exchange source code. To join, long press the QR code below:

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Source code analysis has been updated in the knowledge circle as follows:

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Understanding SPI Mechanisms in Java, Spring, and Dubbo

Recently updated the series “Yudao SpringBoot 2.X Introduction”, with over 101 articles covering MyBatis, Redis, MongoDB, ES, sharding, read-write separation, SpringMVC, Webflux, permissions, WebSocket, Dubbo, RabbitMQ, RocketMQ, Kafka, performance testing, and more.

Providing nearly 30,000 lines of code as SpringBoot examples and over 40,000 lines of code for e-commerce microservice projects.

How to obtain: Click “Looking“, follow the public account, and reply 666 to receive, with more content to be released gradually.

If this article is helpful, please look, share, and support it.
Thank you! (*^__^*)

Leave a Comment