I. Service Provider Interface (SPI)

Before introducing ServiceLoader, we need to introduce the concept of Service Provider Interface (SPI).

SPI is a technique for dynamically loading interface implementation classes. It is a built-in service discovery mechanism in the JDK. It uses the ServiceLoader to load the corresponding implementation of the interface, so that we don’t have to worry about the implementation class. The official documentation describes it as a mechanism for finding services for an interface, similar to IOC thinking, giving control of assembly to the ServiceLoader.

Usage scenarios

Only provides a service interface, specific service by other components, interfaces and concrete implementation (similar to the bridge), at the same time can through a collection of ServiceLoader get these implementation class system, unified handling, in such componentization tend to bring a lot of convenience, SPI mechanism between different modules can achieve convenient program to an interface, Hard coding is rejected, and decoupling works well.

For example, a project with the following engineering structure:

graph TB; A(main engineering) B(Component 1) C(Component 2) A-->B A-->C

Scenario: If you want to use the main project in component 1 or the methods or variables used in component 2 in component 1, how do you do that

  1. The common way is to declare some interfaces in component 1, implemented by the main project. It is then passed to component 1 by injection when it is initialized.
  2. In ServiceLoader mode, if the dependency injection mode is adopted, the coupling between components is heavy. The ServiceLoader approach is also component 1 declaration interface, the main project implementation. However, no injection is required and this is done automatically by the ServiceLoader.

Two, use mode

The process for using ServiceLoader is as follows: Service contract > Service Implementation > Service registration > Service discovery/Use.

First of all, a few conceptual nouns should be agreed, and the following text should be written with these nouns.

concept instructions note
service Interface or (usually) abstract class For loading purposes,Services are represented by a single type, that is, a single interface or abstract class. (Concrete classes can be used, but not recommended.
Service provider Concrete implementations of services (interfaces and abstract classes). Service providers can be introduced as extensions, such as JAR packages; They can also be provided by adding them to your application’s classpath or by some other platform-specific means. A given service provider contains one or more concrete classes that extend the service type with provider-specific data and code. The only requirement that this tool enforces isThe provider class must have a zero-argument constructorSo that they can be instantiated during load.
  1. The service agreement

    Define interfaces or abstract classes as services

  2. The service implementation

    Implement defined services because ServiceLoader makes it easier for different components to communicate and is highly decoupled. So a more common scenario is that a service may be defined in an underlying component or introduced in a JAR package, which is concretely implemented in the upper-level business code.

  3. The service registry

    After a service is agreed and implemented, you must register the service so that the system can locate the service. To register, create a resources/META-INF/services directory in the Java sibling directory and create an SPI description file in that directory with the fully qualified name of the service. The directory hierarchy diagram is as follows:

With this file, the fully qualified name of the service provider (interface implementation class) can be written to the file, completing the service registration.

PS. The registry directory path is fixed. Why is explained in the code section below.

Example:


package com.example;
// Declare service
public interface IHello {
    String sayHello(a); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --// Implement the service
public class Hello implements IHello{
  @Override
  public String sayHello(a){
    System.out.println("hello, world"); }} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --// Use the service
ServiceLoader<Hello> loader = ServiceLoader.load(IHello.class);
mIterator =loader.iterator(); 
while(mIterator.hasNext()){
    mIterator.next().sayHello();
}

Copy the code

Three, code logic

Description of ServiceLoader member variables

field type instructions
service Class<S> The ServiceLoader loads an interface or abstract class
loader ClassLoader Class loader
providers LinkedHashMap<String,S> Cache-loaded interfaces or abstract classes (that is, service objects)
lookupIterator LazyIterator The iterator

3.1. Creation of ServiceLoader

    //ServiceLoader.class
    public static <S> ServiceLoader<S> load(Class service, ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Android-changed: Do not use legacy security code.
        // On Android, System.getSecurityManager() is always null.
        // acc = (System.getSecurityManager() ! = null) ? AccessController.getContext() : null;
        reload();
    }

    /**
     * Clear this loader's provider cache so that all providers will be
     * reloaded.
     *
     * <p> After invoking this method, subsequent invocations of the {@link
     * #iterator() iterator} method will lazily look up and instantiate
     * providers from scratch, just as is done by a newly-created loader.
     *
     * <p> This method is intended for use in situations in which new providers
     * can be installed into a running Java virtual machine.
     */
    public void reload(a) {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
Copy the code

You can see that the ServiceLoader#load method creates the ServiceLoader object and initializes the Service and loader object. The Reload method was called to clear the cache, and a LazyIterator object was created to iterate over the loaded service.

This phase finds that only the initialization is done, but the registered service is not loaded, so it is a lazy loading process (as the name of LazyIterator reveals).

3.2 Registration of services

Section 3.1 shows that the load method only does some initialization and does not register the service. So where exactly is the service registered? When used, we get the ServiceLoader iterator to iterate over the service. Look at the ServiceLoader#iterator method:

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(); }}; }Copy the code

This creates an iterator object that implements three methods hasNext (to determine if there are still services for traversal), next (to get services), and remove. Internal knownProviders cache registered services, and every time a hasNext or next method is called, it is fetched from the service in the cache, without calling lookupIterator.

In the case of the first creation, there is no registered service in the cache, and if hasNext is called, lookupiterator.hasnext () is called, as follows

private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
        public boolean hasNext(a) {
            return hasNextService();
        }

        private boolean hasNextService(a) {
           if(nextName ! =null) {/ / 1.
               return true;
           }
           if (configs == null) {/ / 2.
               try {
                   String fullName = PREFIX + service.getName();/ / 3.
                   if (loader == null)
                       configs = ClassLoader.getSystemResources(fullName);/ / 4.
                   else
                       configs = loader.getResources(fullName);
               } catch (IOException x) {
                   fail(service, "Error locating configuration files", x); }}while ((pending == null) | |! pending.hasNext()) {/ / 5.
               if(! configs.hasMoreElements()) {return false;
               }
               pending = parse(service, configs.nextElement());/ / 6.
           }
           nextName = pending.next();
           return true; }}Copy the code

① Check whether nextName is null, indicating the full name of the next service to be registered (directory path + service name). If it is not null, indicating that there is a service, return true. Otherwise continue

**resources/ meta-INF /services/** configs saves all resources with the specified name. If null, the resource file has not been loaded.

The value of PREFIX is as follows:

private static final String PREFIX = "META-INF/services/";
Copy the code

That’s why we create the resources/ meta-INF /services directory and declare a registry file in that directory.

If registered successfully, the configs variable stores the full names of all declared services. This step is where all services are actually registered, and this is where lazy loading occurs.

⑤ : The pending iterator object is null, and the loop starts. If there is no value in the configs, return false to indicate that there is no service to register. Otherwise enter ⑥

(6) Call the parse method, which opens the registration file and starts reading the contents of the file every time it reads the full name of the service, if it is not cached. Just add it to a list. Returns an iterator object for the list until the contents of the file have been read.

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

3.3 Use of services

By following the steps described in section 3.2, the services are now fully registered and the individual service instances can be obtained and used. An instance object of the service is usually obtained by the *next()* method as follows:

        public S next(a) {
                return nextService();
        }

        private S nextService(a) {
            if(! hasNextService())/ / 1.
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null; Class<? > c =null;
            try {
                c = Class.forName(cn, false, loader);/ / 2.
            } catch (ClassNotFoundException x) {
                fail(service,"Provider " + cn + " not found", x);
            }
            if(! service.isAssignableFrom(c)) { ClassCastException cce =new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
                fail(service,"Provider " + cn  + " not a subtype", cce);
            }
            try {
                S p = service.cast(c.newInstance());/ / 3.
                providers.put(cn, p);/ / 4.
                return p;
            } catch (Throwable x) {
                fail(service,"Provider " + cn + " could not be instantiated",x);
            }
            throw new Error();          // This cannot happen
        }
Copy the code

① : There is no service, directly throw an exception

② : Load a class object based on the fully qualified name of the service.

③ Call newInstance to create an instance of the service and install the instance type into a declared service type (interface or abstract class).

④ : cache a copy, improve access efficiency, avoid reflection loading every time.

At this point, you have an instance of the service, and the consumer can call various instance methods from that instance object.

4, summarize

This article explains the concept of SPI and the usage of ServiceLoader, including service contract -> service implementation -> service registration -> service discovery/use, etc

2. Through code analysis, the basic implementation of ServiceLoader is explained, including lazy loading mechanism of service registration, fixed directory of service registration and *hasNext() and next()* methods of service use.