This article explains the highly scalable tool SPI of Java core technology

The concept of SPI

JAVA SPI = interface-based programming + strategy mode + dynamic loading mechanism of configuration files

SPI usage scenarios

Java is an object-oriented language. Although Java8 began to support functional programming and Stream, it is still an object-oriented language in general. When using Java for object-oriented development, it is generally recommended to use interface-based programming, and the modules and modules of the program will not be directly hard-coded to implement classes. In the actual development process, there are often multiple implementation classes for an interface, and each implementation class either implements different logic, uses different methods, or implements different technologies. In order to make the caller clearly know which implementation class of the interface it is calling when calling the interface, or in order to realize that it does not need to be dynamically specified in the program during module assembly, a service discovery mechanism is required. The SPI loading mechanism in Java can meet such requirements, it can automatically find the implementation class of an interface.

A large number of frameworks use Java’s SPI technology, as follows:

(1) JDBC loads different types of database drivers (2) The log facade interface implements class loading, and SLF4J loads the log implementation classes of different providers (3) Spring uses a lot of SPI

  • Specification for servlet 3.0
  • Implementation of ServletContainerInitializer
  • Automatic type conversion Type Conversion SPI (Converter SPI, Formatter SPI), etc.

(4) There are many components in Dubbo, and each component is abstracted in the form of an interface in the framework! There are many specific implementations. When the program is executed, the implementation of the interface can be obtained as needed according to the user’s configuration.

Use of SPI

When the service provider provides an implementation of the interface, it needs to create a file named after the interface name (package name.interface name) in the META-INF/services/ directory of the Jar package. In the file The implementation class of the configuration interface in (full package name + class name).

When the external program loads this interface through the java.util.ServiceLoader class, it can find the specific implementation class name through the configuration file in the META/Services/ directory of the Jar package, load and instantiate it, and complete the injection. At the same time, the SPI specification stipulates that the implementation class of the interface must have a no-argument constructor.

The implementation class for finding the interface in SPI is through java.util.ServiceLoader , and there is a line of code in the java.util.ServiceLoader class as follows:

 // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";

That is to say, we must write the configuration file of the interface to the META/Services/ directory of the Jar package.

SPI instance

Here, a simple SPI usage example is given to demonstrate how to use SPI to dynamically load the implementation class of the interface in a Java program.

Note: The example is developed based on Java8.

1. Create a Maven project

Create a Maven project spi-demo in IDEA as follows:

2. Edit pom.xml

 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <artifactId>spi-demo</artifactId>
    <groupId>io.binghe.spi</groupId>
    <packaging>jar</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <modelVersion>4.0.0</modelVersion>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3. Create a class loader class

Create MyServiceLoader under the io.binghe.spi.loader package. The MyServiceLoader class directly calls the JDK’s ServiceLoader class to load the Class. The code is shown below.

 package io.binghe.spi.loader;

import java.util.ServiceLoader;

/**
 * @author binghe
 * @version 1.0.0
 * @description 类加载工具
 */
public class MyServiceLoader {

    /**
     * 使用SPI机制加载所有的Class
     */
    public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) {
        return ServiceLoader.load(clazz);
    }
}

4. Create an interface

Create the interface MyService under the io.binghe.spi.service package. As a test interface, there is only one method in the interface, which prints the incoming string information. The code looks like this:

 package io.binghe.spi.service;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定义接口
 */
public interface MyService {

    /**
     *  打印信息
     */
    void print(String info);
}

5. Create the implementation class of the interface

(1) Create the first implementation class MyServiceA

Create the MyServiceA class under the io.binghe.spi.service.impl package to implement the MyService interface. The code looks like this:

 package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;

/**
 * @author binghe
 * @version 1.0.0
 * @description 接口的第一个实现
 */
public class MyServiceA implements MyService {
    @Override
    public void print(String info) {
        System.out.println(MyServiceA.class.getName() + " print " + info);
    }
}

(2) Create a second implementation class MyServiceB

Create the MyServiceB class under the io.binghe.spi.service.impl package to implement the MyService interface. The code looks like this:

 package io.binghe.spi.service.impl;

import io.binghe.spi.service.MyService;

/**
 * @author binghe
 * @version 1.0.0
 * @description 接口第二个实现
 */
public class MyServiceB implements MyService {
    @Override
    public void print(String info) {
        System.out.println(MyServiceB.class.getName() + " print " + info);
    }
}

6. Create an interface file

Create the META/Services/ directory in the src/main/resources directory of the project, and create the io.binghe.spi.service.MyService file in the directory. Note: the file must be the full name of the interface MyService, and then the class that implements the MyService interface will be Configure into a file as follows:

 io.binghe.spi.service.impl.MyServiceA
io.binghe.spi.service.impl.MyServiceB

7. Create a test class

Create the Main class under the io.binghe.spi.main package of the project. This class is the entry class of the test program and provides a main() method. In the main() method, the ServiceLoader class is called to load the implementation class of the MyService interface. And print the result through Java8’s Stream, as follows:

 package io.binghe.spi.main;

import io.binghe.spi.loader.MyServiceLoader;
import io.binghe.spi.service.MyService;

import java.util.ServiceLoader;
import java.util.stream.StreamSupport;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试的main方法
 */
public class Main {

    public static void main(String[] args){
        ServiceLoader<MyService> loader = MyServiceLoader.loadAll(MyService.class);
        StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
    }
}

8. Test Example

Run the main() method in the Main class, and the printed information is as follows:

 io.binghe.spi.service.impl.MyServiceA print Hello World
io.binghe.spi.service.impl.MyServiceB print Hello World

Process finished with exit code 0

It can be seen from the printed information that the implementation class of the interface is correctly loaded through the Java SPI mechanism, and the implementation method of the interface is called.

Source code analysis

Here, it is mainly to analyze the source code of java.util.ServiceLoader involved in the SPI loading process.

Enter the source code of java.util.ServiceLoader , you can see that the ServiceLoader class implements the java.lang.Iterable interface, as shown below.

 public final class ServiceLoader<S>  implements Iterable<S>

It shows that the ServiceLoader class can be traversed and iterated.

The following member variables are defined in the java.util.ServiceLoader class:

 // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";

// 需要加载的接口
private final Class<S> service;

// 类加载器,用于加载以接口命名的文件中配置的接口的实现类
private final ClassLoader loader;

// 创建ServiceLoader时采用的访问控制上下文环境
private final AccessControlContext acc;

// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 用于延迟加载实现类的迭代器
private LazyIterator lookupIterator;

You can see that the ServiceLoader class defines the loading prefix as “META-INF/services/”, so the interface file must be created in the META-INF/services/ directory in the src/main/resources directory of the project.

Call the ServiceLoader.load(clazz) method from the MyServiceLoader class to enter the source code, as shown below:

 //根据类的Class对象加载指定的类,返回ServiceLoader对象
public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取当前线程的类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    //动态加载指定的类,将类加载到ServiceLoader中
    return ServiceLoader.load(service, cl);
}

The ServiceLoader.load(service, cl) method is called in the method, and the code continues to be traced, as shown below:

 //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

You can see that in the ServiceLoader.load(service, cl) method, the constructor of the ServiceLoader class is called, and the code continues to follow, as shown below:

 //构造ServiceLoader对象
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    //如果传入的Class对象为空,则判处空指针异常
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

Continue with the reload() method as shown below.

 //重新加载
public void reload() {
    //清空保存加载的实现类的LinkedHashMap
    providers.clear();
    //构造延迟加载的迭代器
    lookupIterator = new LazyIterator(service, loader);
}

Continue to follow up on the lazy-loading iterator’s constructor as shown below.

 private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

As you can see, the Class object and class loader of the interface to be loaded will be assigned to the member variables of LazyIterator .

When we iteratively obtain object instances in the program, we first look in member variables providers to see if there are cached instance objects. If it exists, return directly, otherwise call lookupIterator lazy load iterator to load.

The code for the logical judgment of the iterator is as follows:

 //迭代ServiceLoader的方法
public Iterator<S> iterator() {
    return new Iterator<S>() {
        //获取保存实现类的LinkedHashMap<String,S>的迭代器
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
        //判断是否有下一个元素
        public boolean hasNext() {
            //如果knownProviders存在元素,则直接返回true
            if (knownProviders.hasNext())
                return true;
            //返回延迟加载器是否存在元素
            return lookupIterator.hasNext();
        }
        //获取下一个元素
        public S next() {
            //如果knownProviders存在元素,则直接获取
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            //获取延迟迭代器lookupIterator中的元素
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

The process of loading classes by LazyIterator is shown in the following code

 //判断是否拥有下一个实例
private boolean hasNextService() {
    //如果拥有下一个实例,直接返回true
    if (nextName != null) {
        return true;
    }
    //如果实现类的全名为null
    if (configs == null) {
        try {
            //获取全文件名,文件相对路径+文件名称(包名+接口名)
            String fullName = PREFIX + service.getName();
            //类加载器为空,则通过ClassLoader.getSystemResources()方法获取
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                //类加载器不为空,则直接通过类加载器获取
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        //如果configs中没有更过的元素,则直接返回false
        if (!configs.hasMoreElements()) {
            return false;
        }
        //解析包结构
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //加载类对象
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        //通过c.newInstance()生成对象实例
        S p = service.cast(c.newInstance());
        //将生成的对象实例保存到缓存中(LinkedHashMap<String,S>)
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

Finally, give the entire java.util.ServiceLoader class as follows:

 package java.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;


public final class ServiceLoader<S>  implements Iterable<S> {
    // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
    private static final String PREFIX = "META-INF/services/";

    // 需要加载的接口
    private final Class<S> service;

    // 类加载器,用于加载以接口命名的文件中配置的接口的实现类
    private final ClassLoader loader;

    // 创建ServiceLoader时采用的访问控制上下文环境
    private final AccessControlContext acc;

    // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 用于延迟加载实现类的迭代器
    private LazyIterator lookupIterator;

    //重新加载
    public void reload() {
        //清空保存加载的实现类的LinkedHashMap
        providers.clear();
        //构造延迟加载的迭代器
        lookupIterator = new LazyIterator(service, loader);
    }

    //构造ServiceLoader对象
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //如果传入的Class对象为空,则判处空指针异常
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

    private static void fail(Class<?> service, String msg, Throwable cause)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg,
                                            cause);
    }

    private static void fail(Class<?> service, String msg)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg);
    }

    private static void fail(Class<?> service, URL u, int line, String msg)
        throws ServiceConfigurationError
    {
        fail(service, u + ":" + line + ": " + msg);
    }

    // Parse a single line from the given configuration file, adding the name
    // on the line to the names list.
    //
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        return names.iterator();
    }

    // Private inner class implementing fully-lazy provider lookupload
    private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        //判断是否拥有下一个实例
        private boolean hasNextService() {
            //如果拥有下一个实例,直接返回true
            if (nextName != null) {
                return true;
            }
            //如果实现类的全名为null
            if (configs == null) {
                try {
                    //获取全文件名,文件相对路径+文件名称(包名+接口名)
                    String fullName = PREFIX + service.getName();
                    //类加载器为空,则通过ClassLoader.getSystemResources()方法获取
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        //类加载器不为空,则直接通过类加载器获取
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                //如果configs中没有更过的元素,则直接返回false
                if (!configs.hasMoreElements()) {
                    return false;
                }
                //解析包结构
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //加载类对象
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                //通过c.newInstance()生成对象实例
                S p = service.cast(c.newInstance());
                //将生成的对象实例保存到缓存中(LinkedHashMap<String,S>)
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    }

    //迭代ServiceLoader的方法
    public Iterator<S> iterator() {
        return new Iterator<S>() {
            //获取保存实现类的LinkedHashMap<String,S>的迭代器
            Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
            //判断是否有下一个元素
            public boolean hasNext() {
                //如果knownProviders存在元素,则直接返回true
                if (knownProviders.hasNext())
                    return true;
                //返回延迟加载器是否存在元素
                return lookupIterator.hasNext();
            }
            //获取下一个元素
            public S next() {
                //如果knownProviders存在元素,则直接获取
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                //获取延迟迭代器lookupIterator中的元素
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

    //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

    //根据类的Class对象加载指定的类,返回ServiceLoader对象
    public static <S> ServiceLoader<S> load(Class<S> service) {
        //获取当前线程的类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        //动态加载指定的类,将类加载到ServiceLoader中
        return ServiceLoader.load(service, cl);
    }
    
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return ServiceLoader.load(service, prev);
    }

    /**
     * Returns a string describing this service.
     *
     * @return  A descriptive string
     */
    public String toString() {
        return "java.util.ServiceLoader[" + service.getName() + "]";
    }

}

SPI Summary

Finally, a brief summary of the SPI mechanism provided by Java.

advantage:

It can realize project decoupling, so that the assembly control logic of the third-party service module is separated from the caller’s business code, instead of being coupled together. Applications can enable framework extensions or replace framework components according to actual business situations.

shortcoming:

  • It is not safe to use instances of the ServiceLoader class with multiple concurrent threads
  • Although ServiceLoader can be regarded as lazy loading, it can only be obtained by traversing all of them, that is, all the implementation classes of the interface are loaded and instantiated once.

Reference: In-depth understanding of the spi mechanism in Java