[Java] Implementation and application of SPI in Java

1. The concept of SPI

1.1. What is API?

API is relatively intuitive to see in our daily development work. For example, in the Spring project, we are usually used to adding an interface layer before writing the service layer code. The calls to the service are generally based on interface operations, through dependencies Injection can be done using an instance of an interface implementation class.

As shown in the figure above, the service caller does not need to care about the definition and implementation of the interface, and only needs to make calls. Interfaces and implementation classes are provided by the service provider. The interface and its implementation method provided by the service provider can be called API. The interface defined in the API is closer to the service provider (implementation side), both in concept and in concrete implementation. Usually the interface In the same package as the implementation class.

1.2. What is SPI?

If we place the definition of the interface on the caller, the caller of the service defines an interface specification that can be implemented by different service providers. Moreover, the caller can discover the service provider through a certain mechanism and use the functions provided by the service provider by calling the interface. This is the idea of SPI.

SPI, the full name of Service Provider Interface, is a set of API provided by Java for implementation or extension by third parties. It can be used to enable framework extensions and replacement components.

The service provider implements the service according to the interface specification, and the service caller finds the service for this interface through some mechanism. The characteristics of SPI are obvious: the definition of the interface (provided by the caller) is isolated from the specific implementation (provided by the service provider) , the implementation class using the interface needs to rely on some kind of service discovery mechanism.

Through comparison, we can see that the meanings of interfaces in API and SPI are still very different. Generally speaking, the interfaces in API are more It is like a list of functions provided by the service provider to the caller, while SPI emphasizes more on the constraints imposed by the service caller on the service implementation.

2. Why use SPI?

  • Interface-oriented programming: In object-oriented design and programming, we often emphasize “relying on abstraction rather than concreteness”. This is to achieve high cohesion, low coupling, and provide code flexibility and maintainability etc.

  • Business scenarios that provide standards but no specific implementation: The usage scenarios of the SPI mechanism are business scenarios that do not have a unified implementation standard. Generally speaking, the service caller has a defined standard interface, but there is no unified implementation, and the service provider needs to provide its specific implementation.

  • Decoupling: The advantage of the SPI mechanism is low coupling. By separating the interface definition and specific implementation, specific implementation classes can be enabled or replaced at runtime based on actual business scenarios.

3. How to use SPI in Java

We are all familiar with interface definition and service implementation. The caller directly relies on the interface and does not rely on the specific implementation. This is the dependency inversion principle. When we use the API in the Spring project, we will use Spring’s dependency injection (DI) to implement “service discovery” ,Similarly, the focus of SPI is also how to let the caller discover the specific implementation of the interface, which is some kind of service discovery mechanism mentioned above.

The service discovery mechanism of SPI is provided by ServiceLoader. ServiceLoader is a new feature introduced by Java in JDK 6. It is mainly used to discover and load a series of service providers. When the service provider provides an implementation of the service interface, it only needs to create a file named after the service interface in the META-INF/services/ directory of the jar package. The content of the file is the implementation of the service interface. Concrete implementation class. When an external program assembles this module, it can find the specific implementation class name through the configuration file in the jar package META-INF/services/, load the implementation class, and complete dependency injection. This is the service discovery of Java SPI. mechanism.

Let’s talk about it in detail with an example. If there is such a requirement, an interface needs to be used to complete the content search service. The specific implementation of the interface is left to other service providers. The implementation may be a file system-based search or a database-based search.

3.1. Define interface

Provide a search service standard interface, first define the caller’s content search method:

// Find service interface
public interface Search {
    //Query content method by keyword
     String searchDoc(String keyword);
}

This interface is implemented by the service provider. Package it and publish it using mvn clean install to ensure that the jar package is in the maven warehouse. Then the provider can introduce the jar package into the project.

3.2, Service Implementation

After the standard interface is developed and published, we assume that the first service provider provides an implementation of File Lookup. Create a new project search-file and introduce the standard interface jar package just released:

<dependency>
    <groupId>com.blblccc.search</groupId>
    <artifactId>search-standard</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Implement the defined interface:

public class FileSearch implements Search {

    @Override
    public String searchDoc(String keyword) {
        return "File search:" + keyword;
    }
}

And create the META-INF/services directory in the project’s resources directory, then create a file with the interface name com.blblccc.spi.learn.Search defined earlier, and write the fully qualified name of the implementation class in the file.

com.blblccc.file.search.FileSearch

The simple implementation of a server is completed. Use maven to create a jar package. After publishing to maven, it can be provided to the caller.

Then, according to the above implementation method, create a project search-database to use the implementation interface of the database:

public class DatabaseSearch implements Search {
    @Override
    public String searchDoc(String keyword) {
        return "Database search:" + keyword;
    }
}

Similarly, after packaging and publishing, it can be provided to the caller.

3.1, Service Discovery

The next key step is service discovery, which relies on the use of ServiceLoader. Create a new project search-sever and introduce the jar packages of the two providers created above.

<dependencies>
    <dependency>
        <groupId>com.blblccc.search</groupId>
        <artifactId>search-file</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.blblccc.search</groupId>
        <artifactId>search-database</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

Although each service provider has different implementations of the interface, as a caller, it does not need to care about the specific implementation class. What we have to do is to call the methods implemented by the service provider through the interface.

Next, is the key service discovery link. Use ServiceLoader to load specific implementation classes. The caller only needs to call the corresponding interface method.

public class SearchDoc {

    public static void main(String[] args) {
        new SearchDoc().searchDocByKeyWord("hello world");
    }

    public void searchDocByKeyWord(String keyWord) {

        ServiceLoader<Search> searchServiceLoader = ServiceLoader.load(Search.class);

        for (Search search : searchServiceLoader){
            String doc = search.searchDoc(keyWord);
            System.out.println(doc);
        }
    }
}

Test Results:

File search: hello world
Database search: hello world

As you can see, two implementation classes were found through the defined Search. There is no specific service implementation class in the entire code, and operations are all called through interfaces.

4. SPI Implementation Principle

4.1, Java class loader

First of all, Java class loaders can be divided into four categories:

Start the class loader Bootstrap ClassLoader

  • Used to load Java’s core classes, implemented by the underlying C++. The startup class loader does not belong to the Java class library and cannot be directly referenced by Java programs.

  • The parent property of Bootstrap ClassLoader is null

Standard extension class loader Extension ClassLoader

  • Implemented by sun.misc.Launcher$ExtClassLoader

  • Responsible for loading all class libraries in the libext directory under JAVA_HOME or in the path specified by the java.ext.dirs system variable

Application ClassLoader Application ClassLoader

  • Implemented by sun.misc.Launcher$AppClassLoader

  • Responsible for loading the specified class library on the user class path when the JVM starts

User-defined class loader User ClassLoader

  • When the above three types of loaders cannot meet development needs, users can customize the loader

  • When customizing the class loader, you need to inherit the java.lang.ClassLoader class. If you don’t want to break the parent delegation model, you only need to override the findClass method; if you want to break the parent delegation model, you need to override the loadClass method

4.2, Parental delegation mechanism

  • The relationship chain of the parent delegation mechanism is as follows:

BootstrapBootstrap class loader→ ExtensionExtension class loader→ ApplicationSystem class loader→ UserCustom class Loader

  • Benefits of the parent delegation model: This bottom-up hierarchical design can avoid repeated loading of classes, while preventing Java’s core API classes from being tampered with at runtime, reducing performance overhead and security risks. .

  • Disadvantages of the parent delegation model: It is impossible not to delegate (must go up to the top level first), nor can it be delegated downward (top level takes precedence).

4.3. Why does SPI want to break the parental delegation mechanism?

There is an important principle here: the class loader visibility principle

  • The child class loader can view all classes loaded by the parent class loader, but the parent class loader cannot view the classes loaded by the child class loader.

The SPI interface located in the rt.jar package is loaded by the Bootstrap class loader, while the SPI implementation class in the classpath is loaded by the App class loader. But often in the SPI interface, the implementer’s code is often called, so you generally need to load your own implementation class first, but the implementation class is not within the loading range of the Bootstrap class loader. After the previous analysis of the parent delegation mechanism , we have learned that the child class loader can delegate the class loading request to the parent class loader for loading, but this process is irreversible. That is to say, the parent class loader cannot delegate the class loading request to its own child class loader for loading, so this question arises at this time: How to load the implementation class of the SPI interface? The answer is to break out of the parental delegation model.

Take JDBC as an example:

The ServiceLoader and DriverManager classes and the SPI interface class are all system classes of the rt core package. They are loaded by the bootstrap classloader, and the third-party jar package is under classPath. The startup class loader cannot load it and cannot delegate it. Load the parent loader, so we need to destroy the parent delegation mechanism and designate a class loader to load.

4.4, SPI underlying implementation principle

To figure out the cause of this problem, we must first confirm the entry we use SPI:

ServiceLoader<Xxxx> serviceLoader = ServiceLoader.load(Xxxx.class);

Enter the method and look for its implementation:

Note that the class loader of the current thread is obtained here, and it is our user himself who calls this class method in the thread. Then it is understood here that the user’s class loader is obtained.

Then search in the method and find this code:

Note that in this code, cl is the class loader obtained in the previous step. If it is found that the class loader does not exist, the system default loader will be obtained again. This system default loader is used to load startup classes under normal circumstances. (explained in jdk comments), and the startup class is a class defined by our users. There is no doubt that it will also be the application class loader.

From the above code, we can conclude that ServiceLoader has obtained our application class loader. At this point, there is basically no other content to take a closer look at the load method entry.

To reduce the pressure of reading the article, jump directly to this method

java.util.ServiceLoader.LazyIterator#nextService

Note that the loader here is the application class loader we obtained earlier. In this method, the specific implementation class that needs to be instantiated is obtained, and it is about to be instantiated. Before that, the Class needs to be obtained first. Here, use Class.forName(class, false, ClassLoader)Method, the meaning of this method is to use the specified class loader to load the specified class. Since the class loader here is the application class loader, the class loading order naturally returns to the application class loader–>extension class loader–>BootStrap class loader–>extension class loader–>application It’s not surprising that the class loader can load the classes we want.

To sum up, the implementation of Java SPI relies on ServiceLoader. ServiceLoader loads SPI interface byusing thread contextclass loader >Implementation class, the full path name of the implementation class needs to be configured in the META-INF/services/ directory. In the content of the file named with the interface name, ServiceLoader will read the full path name in the file, < strong>Instantiate interface implementation classes throughreflection mechanism.

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 139,295 people are learning the system