Understanding SPI Mechanism in Java

Click the above “Yudao Source Code” to select “Top public account”

Technical articles delivered to you first!

High-quality source code column

  • In-depth Dubbo Principles and Source Code 69 Articles

  • In-depth Netty Principles and Source Code 61 Articles

  • Open Source Projects with Detailed Chinese Annotations

  • Java Concurrency Source Code Collection

  • RocketMQ Source Code Collection

  • Sharding-JDBC Source Code Analysis Collection

  • Spring MVC and Security Source Code Collection

  • MyCAT Source Code Analysis Collection

Source: http://t.cn/E2ktNM5

Implementing a Custom SPI1. Project Structure2. Interface Module3. Good Printer Module4. Bad Printer ModuleApplications of SPI in Real ProjectsApplications of SPI in Extensions

The SPI (Service Provider Interface) mechanism provided by JDK may not be familiar to many people, as this mechanism is aimed at vendors or plugins and can also be seen in some framework extensions. The core class java.util.ServiceLoader is detailed in the JDK 1.8 documentation. Although it is not common, it does not mean it is not often used; on the contrary, you are using it all the time. It’s mysterious, but don’t worry; think about whether you have used third-party logging packages or database drivers in your project? In fact, these are all related to SPI. Furthermore, consider how modern frameworks load logging dependencies and database drivers. You might be familiar with the code class.forName(“com.mysql.jdbc.Driver”), which every Java beginner must encounter, but is this how database drivers are still loaded today? Can you still find this code? All these questions will be answered by the end of this article.

First, let’s introduce what the SPI mechanism is.

Implementing a Custom SPI

1. Project Structure

Understanding SPI Mechanism in Java
SPI Project Structure
  1. Invoker is our main project for testing.

  2. Interface is the interface project defined for vendors and plugin providers, providing only the interface without implementation.

  3. Good-printer and bad-printer are two different implementations of the interface by different vendors, so they will depend on the interface project.

This simple demo is designed to allow everyone to experience switching the implementation vendor of the interface without changing the invoker code, only by changing the dependencies.

2. Interface Module

2.1 moe.cnkirito.spi.api.Printer

public interface Printer {
    void print();
}

The interface only defines a single interface without providing an implementation. The entities that define the specifications are usually quite powerful, and these interfaces are typically located in packages with the prefixes java or javax. Here, Printer simulates a specification interface.

3. Good Printer Module

3.1 good-printer\pom.xml

<dependencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

The specific implementation class of the specification must depend on the specification interface.

3.2 moe.cnkirito.spi.api.GoodPrinter

public class GoodPrinter implements Printer {
    public void print() {
        System.out.println("You are a good person~");
    }
}

This is one implementation of the Printer specification interface.

3.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer

moe.cnkirito.spi.api.GoodPrinter

It is important to note that each SPI interface must declare a services file in its project’s static resource directory, with the filename being the full path of the implementing class of the specification interface. In this case, it is moe.cnkirito.spi.api.Printer, and in the file, the full path of the specific implementation class should be written, which in this case is moe.cnkirito.spi.api.GoodPrinter.

This completes the implementation of one vendor.

4. Bad Printer Module

We will complete another vendor’s implementation of the Printer specification in the same way as defined in the good-printer module.

4.1 bad-printer\pom.xml

<dependencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

4.2 moe.cnkirito.spi.api.BadPrinter

public class BadPrinter implements Printer {
    public void print() {
        System.out.println("I smoke, drink, party, but I know I am a good girl~");
    }
}

4.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer

moe.cnkirito.spi.api.BadPrinter

This completes the implementation of another vendor.

5 Invoker Module

The invoker here is our own project. If we want to use the Printer implementation from the good-printer vendor, we need to introduce its dependency.

<dependencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>good-printer</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

5.1 Writing the Main Class

public class MainApp {
    public static void main(String[] args) {
        ServiceLoader<Printer> printerLoader = ServiceLoader.load(Printer.class);
        for (Printer printer : printerLoader) {
            printer.print();
        }
    }
}

The ServiceLoader is a loader provided by java.util for loading files in a fixed class path, and it loads the implementation classes declared for the corresponding interface.

5.2 Print Result 1

You are a good person~

If in subsequent scenarios, we want to replace the vendor’s Printer implementation, we only need to change the dependency.

<dependencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>bad-printer</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

The main class does not need to change the code, which complies with the Open/Closed Principle.

5.3 Print Result 2

I smoke, drink, party, but I know I am a good girl~

Isn’t it amazing? Everything is transparent to the caller; you just need to switch dependencies!

Applications of SPI in Real Projects

First, let’s summarize what new knowledge we have gained. The files under resources/META-INF/services seem to be something we have not encountered much before, and we also haven’t dealt much with ServiceLoader. Now let’s open our project’s dependencies and see what we find.

  1. In mysql-connector-java-xxx.jar, we found the META-INF\services\java.sql.Driver file, which contains only two lines:

    com.mysql.jdbc.Driver
    com.mysql.fabric.jdbc.FabricMySQLDriver
    

    We can analyze that java.sql.Driver is a specification interface, and com.mysql.jdbc.Driver and com.mysql.fabric.jdbc.FabricMySQLDriver are the implementations of this specification by mysql-connector-java-xxx.jar.

  2. In jcl-over-slf4j-xxxx.jar, we found the META-INF\services\org.apache.commons.logging.LogFactory file, which contains only one line:

    org.apache.commons.logging.impl.SLF4JLogFactory
    

    I believe no further explanation is needed for everyone to understand the significance of this.

  3. There are many more; if you are interested, you can explore the jar packages in your project path.

Since we are talking about database drivers, let’s elaborate a bit more. Do you remember the classic interview question: What exactly does class.forName(“com.mysql.jdbc.Driver”) do?

First, think about how you would answer that.

We all know that class.forName is related to the class loading mechanism and will trigger the execution of the static method in the com.mysql.jdbc.Driver class, thus loading the database driver for the main class. If further asked why its static block does not trigger automatically, you could answer: Because of the special nature of database driver classes, the JDBC specification explicitly requires that the Driver class must register itself with the DriverManager, which necessitates that it be triggered manually by class.forName, which can be explained in java.sql.Driver. Is it perfect? Not yet. In the latest DriverManager source code, we can see the following comment, translated as:

The methods getConnection and getDrivers of the DriverManager class have been enhanced to support the Java Standard Edition Service Provider mechanism. JDBC 4.0 Drivers must include a META-INF/services/java.sql.Driver file. This file contains the names of JDBC driver implementations for java.sql.Driver. For example, to load the my.sql.Driver class, the META-INF/services/java.sql.Driver file should contain the following entry:

my.sql.Driver

Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs that use Class.forName() to load JDBC drivers will continue to work without modification.

We can see that Class.forName has been deprecated, so the best answer to this question should involve discussing the SPI mechanism in JAVA with the interviewer, and then talk about the evolution of driver loading history.

java.sql.DriverManager

public Void run() {
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();

    try{
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
    } catch(Throwable t) {
        // Do nothing
    }
    return null;
}

Of course, the main content of this section is still to introduce SPI; the driver part is an extension. If you don’t quite understand, you can go back and review the source code of Driver and DriverManager in jdk1.8, and I believe you will gain a lot.

Applications of SPI in Extensions

SPI is not only a standard for vendors but also provides an idea for framework extensions. Frameworks can reserve SPI interfaces, allowing for extension through adding or deleting dependencies without intruding on the code. The premise is that the framework must reserve core interfaces, similar to the interfaces in the interface module of this example, and the rest of the adaptation work is left to the developers.

For example, in my previous article https://www.cnkirito.moe/2017/11/07/spring-cloud-sleuth/, I introduced the extension of the Filter in motan, which also adopted the SPI mechanism. Once you are familiar with this setup, going back to understand the SPI extensions of some frameworks will not feel too unfamiliar.

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

Understanding SPI Mechanism in Java

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

  • “In-depth Dubbo Source Code Analysis Series” 69 articles.

  • “In-depth Netty Source Code Analysis Series” 61 articles.

  • “In-depth Spring Source Code Analysis Series” 35 articles.

  • “In-depth MyBatis Source Code Analysis Series” 34 articles.

  • “Database Entity Design” 17 articles.

  • Preparing to update “In-depth Spring MVC Source Code Analysis Series”

Currently, the “Dubbo Source Code Analysis” directory has been updated in the knowledge circle as follows: 01. Debugging Environment Setup 02. Project Structure Overview 03. Configuration 04. Core Process Overview

05. Extension Mechanism SPI

06. Thread Pool

07. Service Exposure Export

08. Service Reference Refer

09. Registry Center Registry

10. Dynamic Compilation Compile

11. Dynamic Proxy Proxy

12. Service Invocation Invoke

13. Invocation Features

14. Filter

15. NIO Server

16. P2P Server

17. HTTP Server

18. Serialization

19. Cluster Fault Tolerance

20. Graceful Shutdown

21. Log Adaptation

22. Status Check

23. Monitoring Center

24. Management Center

25. Operation and Maintenance Command QOS

26. Link Tracking Tracing

… A total of 69+ articles

Currently, the “Netty Source Code Analysis” directory has been updated in the knowledge circle as follows: 01. Debugging Environment Setup 02. NIO Basics 03. Introduction to Netty 04. Startup Bootstrap

05. Event Polling EventLoop

06. Channel Pipeline ChannelPipeline

07. Channel Channel

08. Byte Buffer ByteBuf

09. Channel Handler ChannelHandler

10. Codec Codec

11. Utility Class Util

A total of 61+ articles

Currently, the “Database Entity Design” directory has been updated in the knowledge circle as follows:

01. Product Module 02. Transaction Module 03. Marketing Module 04. Common Module

… A total of 17+ articles

Currently, the “Spring Source Code Analysis” directory has been updated in the knowledge circle as follows:

01. Debugging Environment Setup 02. IoC Resource Location 03. IoC BeanDefinition Loading

04. IoC BeanDefinition Registration

05. IoC Bean Retrieval

06. IoC Bean Lifecycle

… A total of 35+ articles

Currently, the “MyBatis Source Code Analysis” directory has been updated in the knowledge circle as follows:

01. Debugging Environment Setup 02. Project Structure Overview 03. MyBatis Interview Questions Collection

04. MyBatis Learning Materials Collection

05. MyBatis Initialization

06. SQL Initialization

07. SQL Execution

08. Plugin System

09. Spring Integration

… A total of 34+ articles

Leave a Comment