java SPI 03-ServiceLoader jdk Source Parsing

Posted by frizzo on Thu, 18 Jun 2020 20:26:14 +0200

Series Catalog

What is spi 01-spi?Getting Started

spi 02-spi battle resolution slf4j package conflict problem

spi 03-spi jdk for source code parsing

spi 04-spi dubbo for source code resolution

spi 05-dubbo adaptive extension adaptive expansion

spi 06 - Implement SPI framework from scratch

spi 07-Automatically generate SPI profile implementation

java SPI loading process

1 Application Call ServiceLoader.load Method

ServiceLoader.load The method first creates a new ServiceLoader and instantiates the member variables in the class, including:

loader(ClassLoader type, class loader)
acc(AccessControlContext type, Access Controller)
Providers (LinkedHashMap <String, S>type, used to cache successfully loaded classes)
LookupIterator (implementing iterator functionality)

2 Application obtains object instances through iterator interface

ServiceLoader first determines if there is a cached instance object in the member variable providers object (LinkedHashMap< String, S> type) and returns it directly if there is a cache.

If there is no cache, load the class as follows:

(1) Read the configuration file under META-INF/services/to get the names of all the classes that can be instantiated. It is worth noting that ServiceLoader can get the configuration file under META-INF across the jar package, and load the implementation code of the configuration as follows:

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);
}

(2) by reflection method Class.forName() Load the class object and instantiate the class using the instance() method.

(3) Cache the instantiated class into the providers object, (LinkedHashMap< String, S> type, and then return the instance object.

Seeing this is actually helpful in understanding SPI standards, such as why parameterless constructors are needed.

java SPI Source

Let's go through the source code as a whole.

class

stayJava.utilUnder the bag.

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

The class is supported starting with jdk1.6 and is inherited from Iterable as seen in the comments.

private variable

PREFIX corresponds to where we specify the SPI file configuration.

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

// The class or interface representing the service being loaded
private final Class<S> service;

// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
private LazyIterator lookupIterator;

Method Entry

Let's recall how we used it:

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ServiceLoader<Say> loader = ServiceLoader.load(Say.class, classLoader);

for (Say say : loader) {
    say.say();
}

This also shows the entry to the method, getting the current ClassLoader, so let's take a look at the load method.

load

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

An instance is created directly, and this constructor method is private ly:

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();
}
  • load() overload

To provide traversal, a default lassLoader implementation is provided

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
  • reload()

This has been reloaded, providers have been emptied, and a new LazyIterator has been created.

Private LinkedHashMap< String, S> providers = new LinkedHashMap< > (); is actually a cache, which is emptied for each initialization.

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

LazyIterator

Let's look at the implementation of this iterator

Source code

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() {
        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() {
        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() {
        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();
    }
}

Two of the core methods, hasNextService(), process configuration information in the specified directory, and hasNextService(), has the following core implementations.

c = Class.forName(cn, false, loader);

// Create and cache
S p = service.cast(c.newInstance());
providers.put(cn, p);

parse

The parse in the hasNextService() method is also worth looking at.

Instead, it parses the configuration file under the folder and reads it on a line.

You can see that utf-8 file encoding is used by default.

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

fail()

This method has appeared several times and is actually just an error message, just know it:

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

Get value

This is a grammatical sugar that walks through the loader, which is actually java.

The corresponding methods are Iterator, hasNext(), and next().

for (Say say : loader) {
    say.say();
}

This is where the implementation in the cache is traversed. As to how the information in the cache comes from, it is the creation of an instance + cache in the LazyIterator traversal process.

public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

Summary

However, as far as the source code is concerned, the implementation mechanism is not complex.

But it has a good idea and great convenience.

When we look at the source code, we will have a clearer understanding of the advantages and disadvantages.

Advantages and disadvantages

Advantage

The advantage of using Java SPI mechanisms is decoupling, which allows the logic of assembly control for third-party service modules to be separated from, rather than coupled with, the caller's business code.

Applications can enable framework extensions or replace framework components based on actual business conditions.

Rather than using the jar package that provides the interface for third-party service modules to implement the interface, the SPI approach allows the source framework not to care about the path of the interface's implementation class, but to obtain the interface implementation class without the following:

  1. Code Hardcoded import Import Implementation Class

  2. Specify class full-path reflection acquisition: for example, before JDBC4.0, acquiring database-driven classes in JDBC required aClass.forName("Com.mysql.jdbc.Driver "), similar to a statement that dynamically loads database-related drivers before performing operations such as getting connections

  3. A third-party service module registers an instance of an interface implementation class to a specified location from which the source framework accesses the instance

Through SPI, when a third-party service module implements the interface, the full path name of the implementation class is specified in the configuration file in the META-INF/services directory of the third-party project code, and the implementation class is found in the source code framework.

shortcoming

  1. Although ServiceLoader is also considered a delayed load, it can only be obtained by traversing all, that is, the implementation classes of the interface are all loaded and instantiated once.If you don't want to use some implementation class, it is also loaded and instantiated, which is wasteful.

  2. Getting an implementation class is not flexible enough, it can only be obtained as an Iterator, and it cannot be obtained from a parameter.

  3. It is not safe for multiple concurrent multithreads to use instances of the ServiceLoader class.

Follow-up

In fact, SPI is useful in common frameworks such as hibernate-validator/dubbo.

Later, we'll look at the implementation in dubbo and see how dubbo addresses these shortcomings.

Reference material

Deep understanding of SPI mechanisms

SPI mechanisms in Java that must be understood for advanced development

Topics: Java Dubbo Database JDBC