Understanding SPI Mechanism in Java

Having memorized a lot of stock responses, I believe everyone has heard the term SPI Extension.

Some interviewers really like to ask this question: How is Spring Boot’s auto-configuration implemented?

Basically, if you mention that it is based on Spring’s SPI extension mechanism and bring up the spring.factories file and EnableAutoConfiguration, then you are likely to answer this question quite well.

Just like four or five years ago, when I was asked this question in an interview, mentioning SPI Dynamic Extension Mechanism made the interviewer stunned. They probably had never seen someone so pretentious, simply explaining it in a way that sounded impressive.

That being said, it wasn’t just the interviewer who was impressed; I was also confused. As for what SPI extension really is and how it works, I had no idea at that time.

However, this is how interviews are today; if you want to impress the interviewer, you must first impress yourself.

So today, let’s not talk about Spring’s SPI extension; let’s first look at how Java’s built-in SPI extension mechanism works.

1. Introduction

SPI stands for Service Provider Interface, which translates to Service Provider Interface, and it actually implements a service discovery mechanism.

This might still be a bit hard to understand, so let me give an analogy.

In a Spring project, before writing service layer code, it is customary to add an interface layer. Then, through dependency injection in Spring, we can inject an instance of the implementation class of this interface using methods like @Autowired, and subsequent calls to the service are generally based on the interface.

Simply put, it looks like this:

Understanding SPI Mechanism in Java

As shown in the figure, both the interface and the implementation class are provided by the service provider. We can think of the controller as the service caller, who only needs to call the interface.

Although there are voices suggesting that in most cases, there is only one implementation class for the service, making the interface layer seem a bit redundant. However, in the book Head First Design Patterns, the authors still suggest:

Program to an interface, not an implementation.

Indeed, this is commonly said as programming to interfaces. As for the benefits, they include reducing coupling, facilitating future expansion, increasing code flexibility and maintainability, and so on.

In the above example, we can refer to this interface layer and its methods as API, while the SPI we are discussing has similarities and differences compared to it. Let’s first look at the diagram:

Understanding SPI Mechanism in Java

In simple terms, the service caller defines an interface specification that can be implemented by different service providers. Moreover, the caller can discover the service provider through some mechanism and call its capabilities via the interface.

By comparison, we can see that while they both involve interfaces, there are significant differences:

The interface in an API is a functionality list provided by the service provider to the service caller, while SPI emphasizes a constraint on the service implementation by the service caller, allowing the service provider to implement a service that can be discovered by the service caller.

In other words, SPI in Java means that if you implement the service according to my interface specification, I can find this service for you through some mechanism.

This might still sound abstract; let’s use a concrete example to describe this process.

2. Defining the Interface

Speaking of smart home systems, everyone is quite familiar with them now. As long as the products are from the same brand, you can control them through a mobile app after connecting to Wi-Fi, which is very convenient.

Although products are constantly being updated and new models are emerging, the functionalities of similar appliances in the app are generally the same. Take air conditioners, for instance; we usually have three main functions in the app: On/Off, Select Mode, and Adjust Temperature.

Suppose I have installed three different models of air conditioners in the living room, bedroom, and study, and connected them all to my app. The operations afterward are just the same few buttons, simple and straightforward.

Understanding SPI Mechanism in Java

Think about it: whether it’s turning on/off or adjusting the temperature, it’s merely an interface call to the device through the app. If each model of air conditioner had its own interface, the backend app development would be a nightmare just to connect to those interfaces.

The solution is simple: I first define a set of interface specifications. No matter what model of air conditioner you have in the future, you must implement the interface according to my specification. As long as I can discover your device, I can call the interface in the same way.

Now, let’s define such a set of interface specifications. If you want to connect to the smart home system in the future, you must follow this specification to develop the interface.

Let’s create a new project as a standard, called aircondition-standard, and then create an interface. In addition to the three operations, we will also add a method to get the air conditioner model.

public interface IAircondition {
    // Get model
    String getType();
    
    // On/Off
    void turnOnOff();

    // Adjust temperature
    void adjustTemperature(int temperature);

    // Change mode
    void changeModel(int modelId);
}

This interface will later be used by the service implementers. We will package it into a jar using Maven:

mvn clean install

After that, service providers can introduce this jar package into their projects. With this specification in place, we ensure that no matter how the products are updated in the future, they can still connect to the system.

3. Service Implementation

After establishing and publishing the rules, Hanging Air Conditioner arrives as the first service provider. We create a new project aircondition-hanging-type and import the jar package we just created:

<dependency>
    <groupId>com.cn.hydra</groupId>
    <artifactId>aircondition-standard</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

We create a service class and implement the previously defined interface:

public class HangingTypeAircondition
        implements IAircondition{
    public String getType() {
        return "HangingType";
    }
    
    public void turnOnOff() {
        System.out.println("Hanging air conditioner on/off");
    }

    public void adjustTemperature(int i) {
        System.out.println("Hanging air conditioner adjusting temperature");
    }

    public void changeModel(int i) {
        System.out.println("Hanging air conditioner changing mode");
    }
}

In the resources directory of the project, create the META-INF/services directory. Then create a file named after the previously defined interface name com.cn.hydra.IAircondition, and write the fully qualified name of the implementation class in the file:

com.cn.hydra.HangingTypeAircondition

The entire project structure is very simple:

Understanding SPI Mechanism in Java

Thus, a simple implementation from a service provider is complete. We package it into a jar using Maven, and it can then be provided for the caller to use.

Similarly, we can create a project for Vertical Air Conditioner called aircondition-vertical-type, and create a service class:

public class VerticalTypeAircondition
        implements IAircondition{
    public String getType() {
        return "VerticalType";
    }
    
    public void turnOnOff() {
        System.out.println("Vertical air conditioner on/off");
    }

    public void adjustTemperature(int i) {
        System.out.println("Vertical air conditioner adjusting temperature");
    }

    public void changeModel(int i) {
        System.out.println("Vertical air conditioner changing mode");
    }
}

Again, following the naming rules, create a configuration file:

com.cn.hydra.VerticalTypeAircondition

Once again, package it into a jar, and that’s it. As for how the service caller discovers and calls these two services, we will discuss that in detail next.

4. Service Discovery

Now that both service providers have implemented the interface, the next crucial step is service discovery, which is already handled by the SPI discovery mechanism in Java.

Create a new project aircondition-app and introduce the two previously packaged jar files:

<dependencies>
    <dependency>
        <groupId>com.cn.hydra</groupId>
        <artifactId>aircondition-hanging-type</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>com.cn.hydra</groupId>
        <artifactId>aircondition-vertical-type</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

As mentioned earlier, although each service provider has a different implementation for the interface, the caller does not need to worry about the specific implementation classes. What we need to do is call the methods implemented by the service providers through the interface.

Now comes the key service discovery step. We will write a method to call the corresponding air conditioner’s switch method based on the model.

public class AirconditionApp {
    public static void main(String[] args) {
        new AirconditionApp().turnOn("VerticalType");
    }

    public void turnOn(String type){
        ServiceLoader<IAircondition> load = ServiceLoader
                .load(IAircondition.class);

        for (IAircondition iAircondition : load) {
            System.out.println("Detected:" + iAircondition.getClass().getSimpleName());
            if (type.equals(iAircondition.getType())){
                iAircondition.turnOnOff();
            }
        }
    }
}

Test results:

Understanding SPI Mechanism in Java

As you can see from the test, the defined interface IAircondition discovered two implementation classes and called a specific method of a particular implementation class based on the parameters. Throughout the code, there was no mention of specific service implementation classes; all operations were performed through interface calls.

5. Principles

Having understood the workflow of SPI, let’s take a look at its implementation. The key part lies in the ServiceLoader class that appeared in the code above.

In the example code, we traversed the result of the load() method of ServiceLoader using a for loop. This can be understood by looking at the source code, as ServiceLoader implements the Iterable interface, and the core of the service discovery lies in its iterator() method.

Understanding SPI Mechanism in Java

Note that there are two key elements here; let’s find the places where they are defined in the source code:

Understanding SPI Mechanism in Java

The comments are very clear: providers is a cache that looks for the implementation classes in the iterator first. If it finds one, it continues to look further; if not, it uses the lazy loading lookupIterator to search.

So, let’s move on to LazyIterator and see how its hasNext() and next() methods are implemented.

Understanding SPI Mechanism in Java

The acc is a security manager that is assigned by checking System.getSecurityManager(). In debug mode, this is all null, so we can just look at the hasNextService() and nextService() methods.

In the hasNextService() method, the class name of the interface’s implementation class is taken out and placed in nextName:

Understanding SPI Mechanism in Java

Next, in the nextService() method, the implementation class is loaded first, then the object is instantiated, and finally placed into the cache.

Understanding SPI Mechanism in Java

During the iteration process of the iterator, all implementation classes are instantiated, which ultimately relies on Java reflection.

6. Applications

Regarding the actual application of SPI, the most common example is probably the logging framework slf4j, which uses SPI to implement plug-and-play integration with other specific logging frameworks.

In simple terms, slf4j itself is just a logging facade and does not provide a specific implementation. It requires binding to other specific implementations to truly introduce logging functionality.

For example, we can use log4j2 as a specific binder. By simply introducing slf4j-log4j12 in the pom file, we can utilize the specific functionality.

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.3</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.3</version>
</dependency>

After introducing the project, let’s take a look at the specific structure of its jar package:

Understanding SPI Mechanism in Java

Have you noticed an easter egg? First, let’s discuss why we introduced slf4j-log4j12 in the pom, but actually imported slf4j-reload4j? According to the official documentation:

Understanding SPI Mechanism in Java

In essence, in 2015 and 2022, log4j1.x announced its end of life, which is not hard to guess, likely due to frequent vulnerabilities. After that, slf4j-log4j will automatically redirect to slf4j-reload4j during the build phase, and the official strongly recommends using slf4j-reload4j as a replacement.

Looking back at the jar package’s META-INF.services, Reload4jServiceProvider is injected through SPI, which implements the SLF4JServiceProvider interface. In its initialization method initialize(), it will complete initialization and other tasks, allowing access to LoggerFactory and Logger specific logging objects afterward.

7. Conclusion

The SPI in Java provides a unique service discovery and invocation mechanism, flexibly separating service calls from service providers. This is very convenient for providing extension and integration for third-party implementations. However, it also has drawbacks; for instance, once an interface is loaded, all implementation classes will be loaded, which may include unnecessary redundant services. Nevertheless, from an overall perspective, it still offers us a very good framework for extension and integration.

WeChat 8.0 has opened up to 10,000 friends, so you can add my main account now, first come first served, once it’s full, it’s really gone.

Scan the QR code below to add me on WeChat, 2023, let's band together and be awesome.

Understanding SPI Mechanism in Java

Recommended Reading

  • After seeing my commonly used code optimization techniques, my colleagues have started to quietly imitate…
  • Being poorly treated in the company doesn’t necessarily mean a lack of ability; it might be related to the organizational structure!
  • A new technical director has arrived: whoever uses Redis to implement automatic closure of orders due to timeout will no longer be needed!
  • Free your hands! One-click packaging and deployment of Spring Boot projects, the official Docker plugin from IDEA is really great!
  • Still handwriting CRUD code? Try this code generation tool, and free your hands completely!
  • Take a look at other people’s backend management system; it’s indeed refreshing and elegant!
  • Major update! The Mall practical tutorial has been fully upgraded, instantly looking impressive!
  • 40K+ Stars! The Mall e-commerce practical project open-source memoir!
Understanding SPI Mechanism in Java

Leave a Comment