SoFunction
Updated on 2025-03-08

Detailed explanation of Java's example of using SPI to achieve decoupling

Overview

The full name of SPI is a service provision interface, which can be used to launch framework extensions and replace components.

Its essence is to use interface implementation + policy mode + configuration files to achieve dynamic loading of implementation classes.

In specific use, there are some conventions:

(1) Specify that the full name file of this interface is created under META-INF/services/ of classPath

(2) In this file, write the full name of the interface implementation class (path + file name). If multiple implementation classes are written, write them in branch lines.

(3) When using 2, use load() of , get the implementation class, and then you can use it.

It is worth noting that the interface implementation class must have a constructor without parameters.

Implementation case

In this application, there are two modules, namely A module and B module. Among these two modules, A module is the main module, B is the slave module, and B module depends on A module. However, there is currently a class that implements in module B. Module A needs to call functions of this class, and the module can no longer rely on module B. It needs to be decoupled at this time. In this implementation, decoupling is performed using SPI. The specific implementation plan is:

(1) Create a new interface in module A: MyLogAppender, the specific implementation is:

/**
  * @author Huang gen(kenfeng)
  * @description Custom appender interface
  * @Since 2021/02/21
  **/

public interface MyLogAppender {

    /**
      * Get the implemented appender
      * @return Return the newly created appender object
      * */
    Appender getAppender();
}

This interface is simple, it just returns an appender object. For the actual operation of the object, operate in the implementation of the interface.

(2) Add the implementation of this interface in module B. The specific operations are:

/**
  * @author Huang gen(kenfeng)
  * @description Custom appender
  * @Since 2021/02/21
  **/
@Component
public class MeshLogAppender extends UnsynchronizedAppenderBase<ILoggingEvent> implements MyLogAppender,ApplicationContextAware {

    private ApplicationContext applicationContext;

    public MeshLogAppender(){ }

    @Override
    public Appender getAppender() {
        MeshLogAppender meshLogAppender = new MeshLogAppender();
        return meshLogAppender;
    }

    @Override
    protected void append(ILoggingEvent iLoggingEvent) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String std = (new Date(((()))));
        String log = std + "\t" + () +"\t"+"--- ["+ ()+"]\t"+()[0]+":\t "+();
        FlowMessage input = new FlowMessage();
        MeshFlowService meshFlowService = ();
        Map<String, Object> body = new HashMap<>(2);
        ("log",log);
        (());
        ("epoch");
        ("log_broadcast");
        (body);
        FlowMessage output = (input);
        if(!(())){
            throw new RuntimeException("Broadcast failed when publishing logs");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
         = applicationContext;
    }
}

In the declaration of this interface and the implementation of the interface, there are some small tricks to implement. In the interface, only one class acquisition is declared, and no specific method is implemented. In the implementation class, instantiate this class, new a new class and return it. At this time, the user can get this implementation class based on this get method, and then perform some operations on the implementation class. This way of writing can bring two benefits: i. The code is more concise and the code of the interface is simple and easy to understand ii. You can inject some parameters into the implementation class construction method. When the user uses it, just inject it directly into the get method.

(3) Add a configuration file in the folder where the implementation class is located, that is, sandbox-app-epoch-starter. The path of the configuration file is by default: resources/META-INF/services/. Create a new problem in this folder. The file name is the path of the interface, and the content is the path of the implementation class. This allows you to implement interface-> implement class mapping.

As shown in the above picture, the file name is:

The contents in the file are:.

The principle is that when the user uses the interface, he will scan all files under the project, look for the file name, and then find the relevant implementation class based on its content.

(4) In A, you can directly use the interface to make calls, and the specific implementation is as follows:

				ServiceLoader<MyLogAppender> myLoaderInterfaceServiceLoader = ();
        Iterator<MyLogAppender> myLoaderInterfaceIterator = ();
        while (()){
            MyLogAppender myLoaderInterface = ();

            Appender newAppender = ();
            ("application");
            (loggerContext);
            ();
            (newAppender);
        }

As can be seen from the above, it can directly call the MyLogAppender interface, use the Appender obtained by this interface, and then directly assign the value.

Advantages and deficiencies

Advantages: Can decouple code

Disadvantages: If there are multiple implementation classes, the instance cannot be obtained based on a certain parameter or flag bit, and can only be obtained through traversal, and the so-called lazy loading is not implemented.

This is the end of this article about the detailed explanation of Java's example of using SPI to achieve decoupling. For more related Java SPI content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!