SPI refers to the Service Provider Interface. It is a mechanism to dynamically find services for an Interface, similar to the idea of IOC.

The use of SPI

Let’s start with a simple example to get an idea of the SPI mechanism

Defines the interface

Create a new Module in Android Studio and add a new interface Machine. The interface definition is as follows:

public interface Machine {
    void powerOn(a);
}
Copy the code

The implementation class

Two new implementation classes, TV and Computer, are added as follows:

public class TV implements Machine {
    @Override
    public void powerOn(a) {
        System.out.println("TV power on"); }}Copy the code
public class Computer implements Machine {
    @Override
    public void powerOn(a) {
        System.out.println("Computer power on"); }}Copy the code

Defining class relationships

  • inmainDefine one in the directoryresources.META-INF.servicesDirectory to which you add a name namedcom.fred.spi.MachineNote that the file name must be and aboveMachineThe corresponding.
  • incom.fred.spi.MachineAdd two lines to the file
    com.fred.spi.impl.Computer
    com.fred.spi.impl.TV
    Copy the code

test

  • Let’s define aMachineFactoryClass, managed by a factory.
public class MachineFactory {
    private static MachineFactory mInstance;
    private Iterator<Machine> mIterator;

    private MachineFactory(a) {
        ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
        mIterator = loader.iterator();
    }

    static MachineFactory getInstance(a) {
        if (null == mInstance) {
            synchronized (MachineFactory.class) {
                if (null == mInstance) {
                    mInstance = newMachineFactory(); }}}return mInstance;
    }
    Machine getMachine(a) {
        return mIterator.next();
    }
    boolean hasNextMachine(a) {
        returnmIterator.hasNext(); }}Copy the code
  • Test entry file
public static void main(String[] args) {
    MachineFactory factory = MachineFactory.getInstance();
    while(factory.hasNextMachine()) { factory.getMachine().powerOn(); }}Copy the code

Executing the code above will print

Computer power on
TV power on
Copy the code

From the Java code level, we don’t have anywhere new Computer, TV; Instead of executing its powerOn() method, it simply added the names of Computer and TV classes to a configuration file.

Principle of SPI mechanism

In MachineFactory we can see that the implementation class that loads the Machine interface relies on just one line of code:

ServiceLoader<Machine> loader = ServiceLoader.load(Machine.class);
Copy the code

You can then simply iterate through the ServiceLoader to find all the classes that have implementations. In the example above, because we matched two in the com.fred.spi.machine file, we found two implementation classes.

ServiceLoaderThe source of

Let’s take a look at how class loading is done from a ServiceLoader source code perspective. Since the ServiceLoader is in the rt.jar package, you can download a src.zip file when installing the JDK. You can import the file into the IDE to associate the source code, but rt.jar is not in the src.zip package, so you can get the relevant code here. Its core code is as follows:

public final class ServiceLoader<S>
    implements 可迭代<S>
{
    private static final String PREFIX = "META-INF/services/";
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    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;
        acc = (System.getSecurityManager() != null)? AccessController.getContext() :null;
        reload();
    }

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

    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(); }};return AccessController.doPrivileged(action, acc);
                }
            }
        }
    } 
}
Copy the code

As you can see, the idea is:

  1. Get a classloader first
  2. And then load itMETA-INF/services/The following file gets the relevant configuration, such as the code:
String fullName = PREFIX + service.getName();
if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
else
    configs = loader.getResources(fullName);
Copy the code
  1. Gets the corresponding implementation class name, that isparsemethods
  2. Use reflection to create the corresponding instance according to the class name, i.enextServicemethods

Apps on Android

For example, in a scenario where we need to obtain data from multiple data sources and the operations related to each data source are integrated into the APP as a sub-module, we can define a meta-INF /services/ XXX file to configure the data source.

If is similar to the developer himself wrote ARouter this routing framework, will surely understand com. Google, auto. Service: auto – service, the component is to simplify the use of SPI, let developers don’t need to manually maintain the meta-inf/services/XXX.

When writing a routing framework of our own, we’ll need to implement an AbstractProcessor of our own to generate routing-specific configurations. A similar file is defined:

@AutoService(Processor.class)
// Allow/support annotation types to be handled by the annotation processor
@SupportedAnnotationTypes({Constants.ROUTER_ANNOTATION_TYPES})
// Specify the JDK build version
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// annotate the parameters received by the processor
@SupportedOptions({Constants.MODULE_NAME, Constants.APT_PACKAGE})
public class RouterProcessor extends AbstractProcessor {
    privateElements elementsUtils; . }Copy the code

Handwritten injection

If we do not use automatic injection, we need to maintain a meta-INF /services/ XXX file (the first line of the code above is not necessary) as follows;

We need to configure the corresponding Processor to this file, and Google com. Google, auto. Service: auto – service component is to simplify the use of SPI, let developers don’t need to manually maintain the meta-inf/services/XXX. Just add an annotation and the framework will automatically generate this configuration file at compile time

Automatic injection

Going back to the first line of code above, @autoService (processor.class), this annotation is provided by the auto-service library. At compile time, the Process method of AutoServiceProcessor is executed, where generateConfigFiles is first called to generate a configuration file as follows:

 private void generateConfigFiles(a) {
    Filer filer = processingEnv.getFiler();

    for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;
      log("Working on resource file: " + resourceFile);
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
       
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
              resourceFile);
          log("Looking for existing resource file at " + existingFile.toUri());
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          log("Existing service entries: " + oldServices);
          allServices.addAll(oldServices);
        } catch (IOException e) {
          log("Resource file did not already exist.");
        }

        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
        if (allServices.containsAll(newServices)) {
          log("No new service entries being added.");
          return;
        }

        allServices.addAll(newServices);
        log("New service file contents: " + allServices);
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        OutputStream out = fileObject.openOutputStream();
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();
        log("Wrote to: " + fileObject.toUri());
      } catch (IOException e) {
        fatalError("Unable to create " + resourceFile + "," + e);
        return; }}}Copy the code

The resulting classes in the configuration file point to our custom Processor.

When the module is in use, the specific implementation class can be found through this configuration and instantiated.