Writing in the front

The SPI mechanism makes it very convenient to dynamically specify an implementation class for an interface, and to some extent this is the basis for the high degree of extensibility of some frameworks. Today, we’ll take a closer look at the SPI mechanism in Java at the source level.

Note: The article has been included: github.com/sunshinelyz…

The concept of SPI

SPI in Java, the full name of the Service Provider Interface, is a built-in Service discovery mechanism in the JDK. It is a set of apis provided by Java to be implemented or extended by third parties. It can be used to enable framework extensions and replace components.

JAVA SPI = interface-based programming + policy pattern + dynamic loading mechanism for configuration filesCopy the code

Usage scenarios for SPI

Java is an object-oriented language, and while Java8 is starting to support functional programming and Streams, 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 implementation classes are not directly hard-coded before the modules of the program. But in the actual development process, often an interface will have multiple implementation classes, each implementation class or the implementation of different logic, or the use of different ways, and is the implementation of different technology. In order to make the caller clearly know which implementation class of the interface he is calling when calling the interface, or to realize that the module assembly does not have to be dynamically specified, this needs a service discovery mechanism. The SPI loading mechanism in Java satisfies this requirement by automatically finding the implementation class for an interface.

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

(1) JDBC loads different types of database drivers (2) Logging facade interface implements class loading, SLF4J loads logging implementation classes from different vendors (3) SPI is heavily used in Spring

  • The servlet3.0 specification
  • Implementation of ServletContainerInitializer
  • Automatic Type Conversion SPI(Converter SPI, Formatter SPI)

(4) There are many components in Dubbo, and each component is abstracted from the interface formation in the framework! The specific implementation is divided into many kinds, in the program execution according to the user’s configuration according to the implementation of the interface

The use of SPI

When the service provider provides an implementation of the interface, it needs to create a Jar in the meta-INF /services/ directory with the interface name (package name). In the form of the interface name), configure the implementation class of the interface in the file (complete package name + class name).

When an external application loads the interface through the java.util.ServiceLoader class, it can find the implementation class name from the configuration file in the META/Services/ directory of the Jar package, load the instantiation, and complete the injection. Also, the SPI specification specifies that the implementation class of the interface must have a no-argument constructor.

The implementation class for looking up interfaces in SPI is java.util.ServiceLoader, and in the java.util.ServiceLoader class there is a line of code like this:

// The file named after the interface must be placed in the meta-INF /services/ directory of the Jar package
private static final String PREFIX = "META-INF/services/";
Copy the code

This means that we must write the interface configuration file to the META/Services/ directory of the Jar package.

SPI instance

Here, I present a simple SPI usage example to demonstrate how SPI can be used to dynamically load the implementation classes of the interface in a Java program.

Note: the example was developed based on Java8.

1. Create a Maven project

Create Maven project spI-demo in IDEA as follows:

2. Edit the pom. XML


      
<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 - the 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>
Copy the code

3. Create a class loading utility class

Create MyServiceLoader in io.binghe.spi.loader, MyServiceLoader Class is directly called in the JDK ServiceLoader Class load. The code is shown below.

package io.binghe.spi.loader;
 
import java.util.ServiceLoader;
 
/ * * *@author binghe
 * @version 1.0.0
 * @descriptionClass loading tools */
public class MyServiceLoader {
 
    /** * load all classes */ using the SPI mechanism
    public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) {
        returnServiceLoader.load(clazz); }}Copy the code

4. Create an interface

Create an interface MyService under the io.binghe.spi.service package as the test interface. There is only one method in the interface that prints the string information passed in. The code looks like this:

package io.binghe.spi.service;
 
/ * * *@author binghe
 * @version 1.0.0
 * @descriptionDefine the interface */
public interface MyService {
 
    /** * Prints information */
    void print(String info);
}
Copy the code

5. Create an implementation class for the interface

Create the first implementation class MyServiceA

In IO. Binghe. Spi. Service. Impl package created under MyServiceA class, realize 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
 * @descriptionThe first implementation of the interface */
public class MyServiceA implements MyService {
    @Override
    public void print(String info) {
        System.out.println(MyServiceA.class.getName() + " print "+ info); }}Copy the code

(2) Create the second implementation class MyServiceB

In IO. Binghe. Spi. Service. Impl package created under MyServiceB class, realize 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
 * @descriptionThe second implementation of the interface */
public class MyServiceB implements MyService {
    @Override
    public void print(String info) {
        System.out.println(MyServiceB.class.getName() + " print "+ info); }}Copy the code

6. Create an interface file

In the project of SRC/main/resources directory to create META/Services/directory, the directory is created in the IO. Binghe. Spi. Service. MyService file, note: The file must be the full name of the interface MyService, then configure the class that implements the MyService interface into the file as follows:

io.binghe.spi.service.impl.MyServiceA
io.binghe.spi.service.impl.MyServiceB
Copy the code

7. Create a test class

Create the main class in 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, call the ServiceLoader class to load the MyService interface implementation class. And print the result via a Java8 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
 * @descriptionTest the main method */
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")); }}Copy the code

8. Test examples

Run the Main () method of the Main class and print something like this:

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

Process finished with exit code 0
Copy the code

The printed information shows that the implementation class of the outgoing interface is correctly loaded through the Java SPI mechanism and the implementation method of the interface is called.

The source code parsing

Here, the focus is on parsing the java.util.ServiceLoader source code involved in SPI’s loading process.

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

public final class ServiceLoader<S>  implements 可迭代<S> 
Copy the code

The ServiceLoader class can iterate.

The java.util.ServiceLoader class defines the following member variables:

// The file named after the interface must be placed in the meta-INF /services/ directory of the Jar package
private static final String PREFIX = "META-INF/services/";

// The interface to load
private final Class<S> service;

// Class loader, used to load the implementation class of the interface configured in the file named interface
private final ClassLoader loader;

// The access control context used to create the ServiceLoader
private final AccessControlContext acc;

// Cache the loaded interface implementation class, where Key is the full class name of the interface implementation class, Value is the implementation class object
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// An iterator for lazy loading implementation classes
private LazyIterator lookupIterator;
Copy the code

The ServiceLoader class defines the loading prefix as “meta-INF /services/”, so the interface file must be created in the meta-INF /services/ directory of the project SRC /main/resources.

Call the serviceloader.load (clazz) method from the MyServiceLoader class to get the source code as follows:

// Load the specified Class based on the Class object and return the ServiceLoader object
public static <S> ServiceLoader<S> load(Class<S> service) {
	// Get the class loader for the current thread
	ClassLoader cl = Thread.currentThread().getContextClassLoader();
	// Dynamically load the specified class into the ServiceLoader
	return ServiceLoader.load(service, cl);
}
Copy the code

The serviceloader.load (service, CL) method is called to continue tracing the code as follows:

// Load the specified Class by ClassLoader and encapsulate the return result into a ServiceLoader object
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
	return new ServiceLoader<>(service, loader);
}
Copy the code

The serviceloader. load(service, CL) method calls the constructor of the ServiceLoader class and continues with the code as follows:

Construct the ServiceLoader object
private ServiceLoader(Class<S> svc, ClassLoader cl) {
	// If the Class object passed is null, a null pointer exception is declared
	service = Objects.requireNonNull(svc, "Service interface cannot be null");
	/ / if the incoming this is empty, it is through this. GetSystemClassLoader (), or directly introduced to use this
	loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
	acc = (System.getSecurityManager() != null)? AccessController.getContext() :null;
	reload();
}
Copy the code

Continue with the reload() method, as shown below.

// Reload
public void reload(a) {
	// Clear the LinkedHashMap that saved the loaded implementation class
	providers.clear();
	// Construct a lazy-loaded iterator
	lookupIterator = new LazyIterator(service, loader);
}
Copy the code

Follow up with the lazy-loaded iterator constructor, as shown below.

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

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

When we iterate through the program to get an instance of an object, we first look for cached instance objects in the member variable providers. Return it if it exists, otherwise call the lookupIterator lazy-loading iterator to load it.

The code for the iterator to make a logical judgment looks like this:

// Iterate through the ServiceLoader method
public Iterator<S> iterator(a) {
	return new Iterator<S>() {
		// Get an iterator that holds the implementation class's LinkedHashMap
      ,s>
		Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
		// Determine if there is a next element
		public boolean hasNext(a) {
			// If knownProviders has elements, return true
			if (knownProviders.hasNext())
				return true;
			// Returns whether the lazy loader has elements
			return lookupIterator.hasNext();
		}
		// Get the next element
		public S next(a) {
			// If there are elements in knownProviders, get them
			if (knownProviders.hasNext())
				return knownProviders.next().getValue();
			// Get the elements in the deferred iterator lookupIterator
			return lookupIterator.next();
		}

		public void remove(a) {
			throw newUnsupportedOperationException(); }}; }Copy the code

The LazyIterator class loading process is shown in the following code

// Determine whether the next instance is available
private boolean hasNextService(a) {
	// If you have the next instance, return true
	if(nextName ! =null) {
		return true;
	}
	// If the full name of the implementation class is null
	if (configs == null) {
		try {
			// Get the full file name, relative file path + file name (package name + interface name)
			String fullName = PREFIX + service.getName();
			/ / class loader is empty, by this. GetSystemResources () method
			if (loader == null)
				configs = ClassLoader.getSystemResources(fullName);
			else
				// If the class loader is not empty, get it directly from the class loader
				configs = loader.getResources(fullName);
		} catch (IOException x) {
			fail(service, "Error locating configuration files", x); }}while ((pending == null) | |! pending.hasNext()) {// If there are no more elements in configs, return false
		if(! configs.hasMoreElements()) {return false;
		}
		// Parse the package structure
		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 {
		// Load the class object
		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 {
		// Generate an object instance through c.newinstance ()
		S p = service.cast(c.newInstance());
		// Save the generated object instance to the cache (LinkedHashMap
      
       )
      ,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(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); }}Copy the code

Finally, the entire java.util.ServiceLoader class is given 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 可迭代<S> {
    // The file named after the interface must be placed in the meta-INF /services/ directory of the Jar package
    private static final String PREFIX = "META-INF/services/";

    // The interface to load
    private final Class<S> service;
    
    // Class loader, used to load the implementation class of the interface configured in the file named interface
    private final ClassLoader loader;
    
    // The access control context used to create the ServiceLoader
    private final AccessControlContext acc;
    
    // Cache the loaded interface implementation class, where Key is the full class name of the interface implementation class, Value is the implementation class object
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
    // An iterator for lazy loading implementation classes
    private LazyIterator lookupIterator;
    
    // Reload
    public void reload(a) {
        // Clear the LinkedHashMap that saved the loaded implementation class
        providers.clear();
        // Construct a lazy-loaded iterator
        lookupIterator = new LazyIterator(service, loader);
    }
    
    Construct the ServiceLoader object
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // If the Class object passed is null, a null pointer exception is declared
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        / / if the incoming this is empty, it is through this. GetSystemClassLoader (), or directly introduced to use this
        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(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 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;
        }
    
        // Determine whether the next instance is available
        private boolean hasNextService(a) {
            // If you have the next instance, return true
            if(nextName ! =null) {
                return true;
            }
            // If the full name of the implementation class is null
            if (configs == null) {
                try {
                    // Get the full file name, relative file path + file name (package name + interface name)
                    String fullName = PREFIX + service.getName();
                    / / class loader is empty, by this. GetSystemResources () method
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        // If the class loader is not empty, get it directly from the class loader
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x); }}while ((pending == null) | |! pending.hasNext()) {// If there are no more elements in configs, return false
                if(! configs.hasMoreElements()) {return false;
                }
                // Parse the package structure
                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 {
                // Load the class object
                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 {
                // Generate an object instance through c.newinstance ()
                S p = service.cast(c.newInstance());
                // Save the generated object instance to the cache (LinkedHashMap
      
       )
      ,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(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(); }}// Iterate through the ServiceLoader method
    public Iterator<S> iterator(a) {
        return new Iterator<S>() {
            // Get an iterator that holds the implementation class's LinkedHashMap
      ,s>
            Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
            // Determine if there is a next element
            public boolean hasNext(a) {
                // If knownProviders has elements, return true
                if (knownProviders.hasNext())
                    return true;
                // Returns whether the lazy loader has elements
                return lookupIterator.hasNext();
            }
            // Get the next element
            public S next(a) {
                // If there are elements in knownProviders, get them
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // Get the elements in the deferred iterator lookupIterator
                return lookupIterator.next();
            }
    
            public void remove(a) {
                throw newUnsupportedOperationException(); }}; }// Load the specified Class by ClassLoader and encapsulate the return result into a ServiceLoader object
    public static <S> ServiceLoader<S> load(Class service, ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    
    // Load the specified Class based on the Class object and return the ServiceLoader object
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // Get the class loader for the current thread
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // Dynamically load the specified class into the 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(a) {
        return "java.util.ServiceLoader[" + service.getName() + "]"; }}Copy the code

SPI summary

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

Advantages:

Project decoupling enables the assembly control logic of third-party service modules to be separated from, rather than coupled to, the caller’s business code. Applications can enable framework extensions or replace framework components depending on the business situation.

Disadvantages:

  • It is not safe for multiple concurrent threads to use instances of the ServiceLoader class
  • Although ServiceLoader is lazy-loaded, it is basically only available through traversal, which means that the implementation classes of the interface are loaded and instantiated.

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

Big welfare

WeChat search the ice technology WeChat 】 the public, focus on the depth of programmers, daily reading of hard dry nuclear technology, the public, reply within [PDF] have I prepared a line companies interview data and my original super hardcore PDF technology document, and I prepared for you more than your resume template (update), I hope everyone can find the right job, Learning is a way of unhappy, sometimes laugh, come on. If you’ve worked your way into the company of your choice, don’t slack off. Career growth is like learning new technology. If lucky, we meet again in the river’s lake!

In addition, I open source each PDF, I will continue to update and maintain, thank you for your long-term support to glacier!!

Write in the last

If you think glacier wrote good, please search and pay attention to “glacier Technology” wechat public number, learn with glacier high concurrency, distributed, micro services, big data, Internet and cloud native technology, “glacier technology” wechat public number updated a large number of technical topics, each technical article is full of dry goods! Many readers have read the articles on the wechat public account of “Glacier Technology” and succeeded in job-hopping to big factories. There are also many readers to achieve a technological leap, become the company’s technical backbone! If you also want to like them to improve their ability to achieve a leap in technical ability, into the big factory, promotion and salary, then pay attention to the “Glacier Technology” wechat public account, update the super core technology every day dry goods, so that you no longer confused about how to improve technical ability!