The spi mechanism of jdk is used to realize the expansion and decoupling of the interface

0. Background

As we all know, SPI has good practice in many places, such as JDBC driver loading, dubbo, etc. The autoConfiguration of SpringBoot project is also a similar principle. It feels good to use it for module decoupling. For example, we do a service of application management, which has the functions of application installation, upgrade and capacity expansion. Often, in the process of installation, upgrade and capacity expansion, the application requires some customized things, such as installing the database and initializing the database; To install kafka, you need to initialize topic; To expand the capacity of kafka consumers, we need to expand the topic partition synchronously. For the service of application management, it should not treat these applications differently. Otherwise, with the development of time, the service will become more and more complex and difficult to decouple from the upper application.

At this time, we can use the SPI mechanism to define an extension interface for each process that supports extension and hand it to the upper application for implementation. We can use SPI to load these implementation classes, so as to enable the application to add its own personalized functions in the installation, upgrade and capacity expansion process.
It is good for both the application management service and the application itself:

  • For the application management service, it only needs to maintain the extension interface without understanding the logic of the application and maintaining the personalized customization function of the application
  • As for the upper application, it is not necessary to understand the logic of the application management service. If you need to expand, you can implement the interface by yourself, maintain the customized process code by yourself, and directly delete the implementation when you don't need customization.

1. Let's play SPI first

1.1 define an interface

Create a new module named service SPI, and add the following interfaces:

package com.example.service.spi;

public interface MyServiceSpi {
    int order();
    void service();
}

There are two methods in this interface. service is an extension for applications to implement. Order is used to control the order of multiple implementation classes. In case there are order requirements among multiple applications, this order can be used to control.

1.2 define the first implementation class

Create a new module named service impl one, POM Reference service SPI in XML and add an implementation class:

package com.example.service.imp.one;

import com.example.service.spi.MyServiceSpi;

public class ServiceImpleOne implements MyServiceSpi {

    public int order() {
        return 0;
    }

    public void service() {
        System.out.println("my service one");
    }
}

Add a folder META-INF/services in resources and create a new one named com example. service. spi. The file of myservicespi is consistent with the full path of the interface
The content of the file is com example. service. imp.one. ServiceImpleOne

1.3 define the second implementation class

Create a new module named service impl two, POM Reference service SPI in XML and add an implementation class:

package com.example.service.imp.two;

import com.example.service.spi.MyServiceSpi;

public class ServiceImplTwo implements MyServiceSpi {
    public int order() {
        return 1;
    }

    public void service() {
        System.out.println("my service two");
    }
}

Add a folder META-INF/services in resources and create a new one named com example. service. spi. The file of myservicespi is consistent with the full path of the interface
The content of the file is com example. service. imp.two. ServiceImplTwo

1.4 calling MyServiceSpi

Called module reference

        <dependency>
            <groupId>org.example</groupId>
            <artifactId>service-impl-one</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>service-imp-two</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

Get all implementation classes through ServiceLoader and call them in sequence after sorting

        ServiceLoader<MyServiceSpi> loader = ServiceLoader.load(MyServiceSpi.class);
        Iterator<MyServiceSpi> iterator = loader.iterator();
        List<MyServiceSpi> myServiceSpiList = new ArrayList<>();
        while(iterator.hasNext()){
            myServiceSpiList.add(iterator.next());
        }
        // Sort according to order. Those with small order value shall be executed first
        Collections.sort(myServiceSpiList, new Comparator<MyServiceSpi>() {
            @Override
            public int compare(MyServiceSpi o1, MyServiceSpi o2) {
                return o1.order()- o2.order();
            }
        });
        // Call all implementation classes serially
        for (MyServiceSpi serviceSpi:myServiceSpiList) {
            serviceSpi.service();
        }

Post run results
my service one
my service two

2. Think

  • Where we call the extension interface, we don't need to care about whether the interface has implementation classes or how many implementation classes, so as to achieve the decoupling between modules we want.
  • However, it is not found that the jar providing the interface should run in the same jvm as the implemented jar, which means that we can either print all the extension jar packages into our application management service in advance, or support the dynamic loading of the extension jar during separate installation or upgrade, and then reload to reload the implementation class.

Keywords: Java

Added by Brand Hill on Fri, 25 Feb 2022 15:38:44 +0200