Make writing a habit together! This is the third day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.

When we encapsulate a set of interfaces, other projects only need to introduce our written package to call our interface. However, if other projects want to extend our interface, because the interface is encapsulated in a dependency package, it is not easy to extend, so they need to rely on the SPI mechanism provided by Java.

1 Brief Introduction

SPI stands for Service Provider Interface, and the closest concept is API, Application Programming Interface. So what’s the main difference between the two?

Callers of an API can only rely on using the provider’s existing implementation, and SPI is a customizable API that can be customized to replace the default implementation provided by the API.

The SPI mechanism is very important, especially for frameworks, to enable framework extensions and replacement components, and we’ll see a lot of SPI applications when we read the framework source code.

The purpose of SPI is to find service implementations for these extended apis.

2 case of SPI

Create a project, one to do the SPI service provider, one to do the SPI service introduction extension test, this case to build the simplest Maven child project can be, in the SPI-test project to introduce the dependency of the SPI-provider.

Spi-test adds a dependency

    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>spi-provider</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
Copy the code

In spI-Provider, you provide an interface and a default implementation class

Add the following file to the resource file and specify the default implementation class for the interface in the modification file

Build an executable test method in SPI-test, execute it directly, and get the default implementation

! [image-20220406221727379](C:\Users\coder zhj\AppData\Roaming\Typora\typora-user-images\image-20220406221727379.png)

We can extend this interface in spI-test, as shown in the figure below

At this time, our extension does not take effect, we still need to specify the implementation class of our extension for the interface in the resource file, then we can run the above test method to get the result of our extension, which is the SPI mechanism.

3 SPI principle analysis

The ServiceLoader class contains the core principles of SPI, and from the path prefix specified at the beginning, we can guess why we must put files in this path for it to take effect.

Load a ServiceLoader object, go into its constructor, reload it, and create a new LazyIterator. LazyIterator is an inner class that scans the configuration files in meta-INF /services/. Parse the names of all interfaces, and then load the class by reflection with fully qualified class names.

public final class ServiceLoader<S>
    implements 可迭代<S>
{

    // Scan for path prefixes
    private static final String PREFIX = "META-INF/services/";
	// The loaded class or interface
    private final Class<S> service;
	// Class loaders used to locate, load, and instantiate classes that need to be loaded
    private final ClassLoader loader;
	// Context object
    private final AccessControlContext acc;
	// Cache instantiated classes in the order in which they are instantiated
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
	// lazy search iterator
    private LazyIterator lookupIterator;

    // Reload
    public void reload(a) {
        // Clear the cache
        providers.clear();
        // New lazy lookup iterator
        lookupIterator = new LazyIterator(service, loader);
    }
	// constructor
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        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);
    }

    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(The '#');
        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 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(a) {
            if(nextName ! =null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    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()) {if(! configs.hasMoreElements()) {return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService(a) {
            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 {
                S p = service.cast(c.newInstance());
                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(a) {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run(a) { returnhasNextService(); }};returnAccessController.doPrivileged(action, acc); }}public S next(a) {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run(a) { returnnextService(); }};returnAccessController.doPrivileged(action, acc); }}public void remove(a) {
            throw newUnsupportedOperationException(); }}public Iterator<S> iterator(a) {
        return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext(a) {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next(a) {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove(a) {
                throw newUnsupportedOperationException(); }}; }public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        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);
    }

    public String toString(a) {
        return "java.util.ServiceLoader[" + service.getName() + "]"; }}Copy the code

The ServiceLoader does not have an additional locking mechanism, so there are concurrency problems. Furthermore, it is not flexible enough to obtain the corresponding implementation classes. You need to use iterators to obtain all the implementation classes of a given interface, so you must load and instantiate all the implementation classes each time. If an extension relies on other extensions and cannot be auto-injected and assembled, it is difficult for the extension to integrate with other frameworks.

For these reasons, many frameworks do not directly use the native SPI mechanism of ServiceLoader, but rather extend it to make it more powerful. Typical examples are Dubbo’s SPI and SpringBoot’s SPI.

In SpringBoot, you can instantiate beans with Spring. factories across modules.

Thank you for reading, if you feel helpful, please click a thumbs-up, thanks a million!!