The background,

SPI stands for Service Provider Interfaces. A Service provides Interfaces. Java is a set of technical architecture provided by Java for use by third party implementations or extensions. By decoupling the service implementation and service usage, the scalability of the program is greatly enhanced and even pluggable.

Based on the mechanism of service registration and discovery, service providers register services with the system, and service users discover services through searching, so as to achieve the separation of service provision and use, and even complete the management of services.

The JDK provides a default concrete implementation, ServiceLoader, based on the idea of SPI. With the built-in ServiceLoader of the JDK, it is easy to implement service-oriented registration and discovery, decoupling service provision from use.

After completing the separated service, the modification or replacement of the service provider will not bring code modification to the service user. Based on the interface oriented service contract, the provider and the user will directly program to the interface without paying attention to the specific implementation of the other party. At the same time, when service users use the service, they will discover the service in a real sense to complete the initialization of the service and form the dynamic loading of the service.

In Java or Android system implementation or project practice, there are also functions directly based on ServiceLoader, or on the basis of ServiceLoader implementation, further extension and optimization of its use.


Second,ServiceLoaderRealize the principle of

Let’s take a look at the implementation of ServiceLoader in the JDK. The ServiceLoader is in the java.util package, the main part of which is as follows:

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

    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;
    
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(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(); } // Private inner class implementing fully-lazy provider lookup 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 booleanhasNextService() {
            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() { returnhasNextService(); }};return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { returnnextService(); }};return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    }
    
    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(); }}; } 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(); }}; } public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {return new ServiceLoader<>(service, loader);
    }
    
    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() {
        return "java.util.ServiceLoader[" + service.getName() + "]";
    }
Copy the code

For external use, the LazyIterator is usually called by load(Class service, ClassLoader loader) or load(Class service). The LazyIterator is created on reload. LazyIterator is an internal class of ServiceLoader that implements the Iterator interface. It is a LazyIterator that performs parsing of configuration files in the meta-inf /services/ directory in the hasNextService method. In the nextService method, the concrete implementation class is instantiated.

Meta-inf /services/ is the configuration directory for the relationship between the interface and the implementation class specified in the ServiceLoader. The file name is the fully qualified class name of the interface, and the content is the implementation class corresponding to the interface. If there are multiple implementation classes, configure each implementation class as a row. Providers of LinkedHashMap

data structures cache discovered interface implementation classes and provide iterator() methods to facilitate external traversal.
,s>

Android uses OpenJDK, and its ServiceLoader implementation is slightly different from that in Java JDK, but the body logic and implementation process are the same.

In general, the general implementation and use process of ServiceLoader includes four steps: service interface agreement, service implementation, service registration, service discovery and use.

The following is an example of ServiceLoader in a simple Java project.

Project Structure:  -------------- |____src | |____main | | |____resources | | | |____META-INF | | | | |____services | | | | | |____com.corn.javalib.IMyServiceProvider | | |____java | | | |____com | | | | |____corn | | | | | |____javalib | | | | |  | |____IMyServiceProvider.java | | | | | | |____TestClass.java | | | | | | |____MyServiceProviderImpl2.java | | | | | |  |____MyServiceProviderImpl1.javaCopy the code

1. Service interface conventions: IMyServiceProvider defines the service interface conventions:

package com.corn.javalib;

public interface IMyServiceProvider {

    String getName();

}
Copy the code

2, service implementation: MyServiceProviderImpl1, MyServiceProviderImpl2 are the concrete interface implementation classes:

package com.corn.javalib;

public class MyServiceProviderImpl1 implements IMyServiceProvider {

    @Override
    public String getName() {
        return "name:ProviderImpl1"; }}Copy the code
package com.corn.javalib;

public class MyServiceProviderImpl2 implements IMyServiceProvider {

    @Override
    public String getName() {
        return "name:ProviderImpl2"; }}Copy the code

3, service registration (in fact to the system registration service providers and the mapping relationship between the service interface, in order to use the service discovery) : / meta-inf/services/directory to create file com. Corn. Javalib. IMyServiceProvider, content as follows:

com.corn.javalib.MyServiceProviderImpl1
com.corn.javalib.MyServiceProviderImpl2
Copy the code

4. Service discovery and use: Testclass. Java is the service user.

package com.corn.javalib;

import java.util.Iterator;
import java.util.ServiceLoader;

public class TestClass {

    public static void main(String[] argus){
        ServiceLoader<IMyServiceProvider> serviceLoader = ServiceLoader.load(IMyServiceProvider.class);

        Iterator iterator = serviceLoader.iterator();
        while (iterator.hasNext()){
            IMyServiceProvider item = (IMyServiceProvider)iterator.next();
            System.out.println(item.getName() +  ":"+ item.hashCode()); }}}Copy the code

The output is:

name:ProviderImpl1: 491044090
name:ProviderImpl2: 644117698
Copy the code


Three,ServiceLoaderUsing the instance

3.1 Discovery process of annotation processor

When using compile-time annotations, you also need to define a specific annotation handler after defining annotations and annotating them on target elements. Annotation processor is used for annotation discovery and processing, such as implementing custom annotation processing logic, generating new Java files and so on. How the annotation handler is discovered and invoked by the Javac compiler at compile time actually uses the ServiceLoader mechanism.

The annotation processor discovery and invocation process is performed during compilation of Javac, which itself is written in Java, as well as compilation tools to compile Java source files. To illustrate the logic related to annotation handlers in Javac, you can deliberately throw exceptions in your custom annotation handlers. To see the approximate execution path.

Caused by: java.lang.NullPointerException
	at com.corn.apt.AnnotationProcessor.process(AnnotationProcessor.java:42)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.accessThe $1800(JavacProcessingEnvironment.java:91)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
	at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
	at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:856)
	at com.sun.tools.javac.main.Main.compile(Main.java:523)
Copy the code

Above, is the general execution flow. The Open JDK comes with Javac source code, which can be downloaded. The main Javac source code is in the package com.sun.tools.javac. According to the error information path thrown above, the specific execution process is sorted out. 1, javac from com.sun.tools.javac.main.Main.com started running, which invokes the JavaCompiler compile method, the compile method concrete are defined as follows:

public void compile(List<JavaFileObject> sourceFileObject) throws Throwable {
        compile(sourceFileObject, List.<String>nil(), null);
    }
Copy the code

Its internal call compile method, the main part of the logic is as follows:

public void compile(List<JavaFileObject> sourceFileObjects,
                        List<String> classnames,
                        Iterable<? extends Processor> processors)
        throws IOException // TODO: temp, from JavacProcessingEnvironment
    {
        if(processors ! = null && processors.iterator().hasNext()) explicitAnnotationProcessingRequested =true;
        // as a JavaCompiler can only be used once, throw an exception if
        // it has been used before.
        if (hasBeenUsed)
            throw new AssertionError("attempt to reuse JavaCompiler");
        hasBeenUsed = true;

        start_msec = now();
        try {
            initProcessAnnotations(processors);

            // These method calls must be chained to avoid memory leaks
            delegateCompiler =
                processAnnotations(
                    enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                    classnames);

            delegateCompiler.compile2();
            delegateCompiler.close();
            elapsed_msec = delegateCompiler.elapsed_msec;
        } catch (Abort ex) {
            if (devVerbose)
                ex.printStackTrace();
        } finally {
            if (procEnvImpl != null)
                procEnvImpl.close();
        }
    }
Copy the code

InitProcessAnnotations (Processors) and processAnnotations(…) are called. Method, and by default, initProcessAnnotations pass in a processors to NULL, which initializes processing annotations. Its internal through new JavacProcessingEnvironment (context, processors) built JavacProcessingEnvironment object, JavacProcessingEnvironment the constructor, Start discovering annotation processors by calling initProcessorIterator(context, processors).

Let’s focus on the initProcessorIterator process.

private void initProcessorIterator(Context context, Iterable<? extends Processor> processors) {
    Log   log   = Log.instance(context);
    Iterator<? extends Processor> processorIterator;

    if (options.get("-Xprint") != null) {
        try {
            Processor processor = PrintingProcessor.class.newInstance();
            processorIterator = List.of(processor).iterator();
        } catch (Throwable t) {
            AssertionError assertError =
                new AssertionError("Problem instantiating PrintingProcessor."); assertError.initCause(t); throw assertError; }}else if(processors ! = null) { processorIterator = processors.iterator(); }else {
        String processorNames = options.get("-processor");
        JavaFileManager fileManager = context.get(JavaFileManager.class);
        try {
            // If processorpath is not explicitly set, use the classpath.
            processorClassLoader = fileManager.hasLocation(ANNOTATION_PROCESSOR_PATH)
                ? fileManager.getClassLoader(ANNOTATION_PROCESSOR_PATH)
                : fileManager.getClassLoader(CLASS_PATH);

            /*
             * If the "-processor" option is used, search the appropriate
             * path for the named class.  Otherwise, use a service
             * provider mechanism to create the processor iterator.
             */
            if(processorNames ! = null) { processorIterator = new NameProcessIterator(processorNames, processorClassLoader,log);
            } else {
                processorIterator = new ServiceIterator(processorClassLoader, log);
            }
        } catch (SecurityException e) {
            /*
             * A security exception will occur if we can't create a classloader. * Ignore the exception if, with hindsight, we didn't need it anyway
             * (i.e. no processor was specified either explicitly, or implicitly,
             * in service configuration file.) Otherwise, we cannot continue.
             */
            processorIterator = handleServiceLoaderUnavailability("proc.cant.create.loader", e);
        }
    }
    discoveredProcs = new DiscoveredProcessors(processorIterator);
}
Copy the code

Processors are null. By default, the application logic goes to the else procedure to processorIterator = New ServiceIterator(processorClassLoader, log). ServiceIterator is an inner class. Let’s take a look at its implementation:

/**
 * Use a service loader appropriate for the platform to provide an
 * iterator over annotations processors.  If
 * java.util.ServiceLoader is present use it, otherwise, use
 * sun.misc.Service, otherwise fail ifa loader is needed. */ private class ServiceIterator implements Iterator<Processor> { // The to-be-wrapped iterator. private Iterator<? > iterator; private Loglog;

    ServiceIterator(ClassLoader classLoader, Log log) { Class<? > loaderClass; String loadMethodName; boolean jusl; this.log =log;
        try {
            try {
                loaderClass = Class.forName("java.util.ServiceLoader");
                loadMethodName = "load";
                jusl = true;
            } catch (ClassNotFoundException cnfe) {
                try {
                    loaderClass = Class.forName("sun.misc.Service");
                    loadMethodName = "providers";
                    jusl = false;
                } catch (ClassNotFoundException cnfe2) {
                    // Fail softly if a loader is not actually needed.
                    this.iterator = handleServiceLoaderUnavailability("proc.no.service",
                                                                      null);
                    return;
                }
            }

            // java.util.ServiceLoader.load or sun.misc.Service.providers
            Method loadMethod = loaderClass.getMethod(loadMethodName,
                                                      Class.class,
                                                      ClassLoader.class);

            Object result = loadMethod.invoke(null,
                                              Processor.class,
                                              classLoader);

            // For java.util.ServiceLoader, we have to call another
            // method to get the iterator.
            if (jusl) {
                Method m = loaderClass.getMethod("iterator"); result = m.invoke(result); // serviceLoader.iterator(); } // The result should now be an iterator. this.iterator = (Iterator<? >) result; } catch (Throwable t) { log.error("proc.service.problem");
            throw new Abort(t);
        }
    }

    public boolean hasNext() {
        try {
            return iterator.hasNext();
        } catch (Throwable t) {
            if ("ServiceConfigurationError".
                equals(t.getClass().getSimpleName())) {
                log.error("proc.bad.config.file", t.getLocalizedMessage());
            }
            throw new Abort(t);
        }
    }

    public Processor next() {
        try {
            return (Processor)(iterator.next());
        } catch (Throwable t) {
            if ("ServiceConfigurationError".
                equals(t.getClass().getSimpleName())) {
                log.error("proc.bad.config.file", t.getLocalizedMessage());
            } else {
                log.error("proc.processor.constructor.error", t.getLocalizedMessage());
            }
            throw new Abort(t);
        }
    }

    public void remove() { throw new UnsupportedOperationException(); }}Copy the code

Finally, we found that ServiceIterator provided a platform-appropriate service discovery mechanism for discovering annotation handlers. The load method corresponding to java.util.ServiceLoader is called dynamically through reflection. Object result = loadMethod.invoke(NULL, processor.class, classLoader) The interface parameter Processor. Class is passed to complete the dynamic service discovery process based on ServiceLoader.

Once the annotation processor is found, it is mainly followed by calling the corresponding annotation processor methods, such as init and process methods, which are often overridden in custom annotation processors, to achieve the logical processing of the annotation part required by the annotation processor.

At this point, the compile-time annotation processor’s service discovery is actually implemented through the ServiceLoader, and the flow is relatively clear.

We also know that after defining the specific annotation handler, we need to register the relationship between the Processor interface and the specific annotation handler implementation class in the corresponding/meta-INF /services/. Of course, this step can also listen to Google AutoService to complete.


3.2 based onServiceLoaderRealize communication and decoupling between different components

In the componentization process of Android projects, the provision and use of services between components or modules that do not have dependencies are often involved. For example, module A needs to call methods of module B. The ServiceLoader mechanism actually provides a mechanism for component decoupling and communication between Android components. By sinking the interface convention into the common baseLib module, different modules can provide specific implementations of the interface as needed, while other modules can get specific services and call them directly through methods such as Serviceloader.load (imyServiceprovider.class).

Here is a simple example of a componentized Android project:

.libBase |____build.gradle |____src | |____main | | |____res | | | |____drawable | | | |____values | | | | |____strings.xml | | |____AndroidManifest.xml | | |____java | | | |____com | | | | |____corn | | | | | |____libbase | | | | | | |____IMyServiceProvider.java .LibA |____libs |____build.gradle |____src | |____main | | |____res | | | |____drawable | | | |____values | | | | |____strings.xml | | |____resources | | | |____META-INF | | | | |____services | | | | | |____com.corn.libbase.IMyServiceProvider | | |____AndroidManifest.xml | | |____java | | | |____com | | | | |____corn | | | | | |____liba | | | | | | |____MyServiceProviderImpl2.java | | | | | | |____MyServiceProviderImpl1.java .LibB |____build.gradle |____src | |____main | | |____res | | | |____drawable | | | |____values | | | | |____strings.xml  | | |____resources | | | |____META-INF | | | | |____services | | | | | |____com.corn.libbase.IMyServiceProvider | | |____AndroidManifest.xml | | |____java | | | |____com | | | | |____corn | | | | | |____libb | | | | | | |____MyServiceProviderImpl3.javaCopy the code

By defining the service interface contract in the libBase module, the specific service implementation can provide the relationship between the interface and the service implementation in the upper modules such as LibA or LibB, and then register the corresponding relationship between the interface and the service implementation in the META-INF/services/ respectively, so that the corresponding mapping relationship can be packaged and merged.

ServiceLoader<IMyServiceProvider> serviceLoader = ServiceLoader.load(IMyServiceProvider.class);
    Iterator iterator = serviceLoader.iterator();
    while (iterator.hasNext()){
        IMyServiceProvider item = (IMyServiceProvider)iterator.next();
        Log.d(TAG, item.getName() +  ":" +  item.hashCode());
    }
Copy the code

If you are in a Release environment, you also need to configure de-obfuscation for the interface and implementation classes. Otherwise, the corresponding target class will not be found in the configuration file based on the interface name or interface implementation.

By means of system ServiceLoader and interface oriented programming, service decoupling between components is realized.


3.3 Us Group WMRouter medium pairServiceLoaderThe improvement and use of

We found that each load of a ServiceLoder actually rewrites the entire ServiceLoder process, so if you use a ServiceLoder directly, you have to rework the search and reflection de-instantiation process for the implementation class each time, and for the same interface. There may be multiple different service implementations.

On the basis of the system ServiceLoader idea and implementation process, The United States of WMRouter, the ServiceLoader has been improved, the main improvement points are as follows: 1, / meta-INF /services/ static Map

SERVICES property and ServiceLoader instance HashMap

mMap. SERVICES is a static variable that stores the mapping between the interface and the corresponding ServiceLoader. MMap is an internal attribute of the ServiceLoader. It stores a relational mapping of the key of each interface implementation class in the corresponding ServiceLoader instance (each key represents each different interface implementation) and the corresponding implementation class. 2. The user can invoke a specific implementation service of the interface through the key of each interface. 3. Interface implementation class. Objects created by reflection can decide whether to store them in the SingletonPool SingletonPool to facilitate the next use of interface implementation class, which is equivalent to object caching.
,>

WMRouter, WMRouter, WMRouter, WMRouter, WMRouter

ServiceLoader implementation:

package com.sankuai.waimai.router.service; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.sankuai.waimai.router.annotation.RouterProvider; import com.sankuai.waimai.router.components.RouterComponents; import com.sankuai.waimai.router.core.Debugger; import com.sankuai.waimai.router.interfaces.Const; import com.sankuai.waimai.router.utils.LazyInitHelper; import com.sankuai.waimai.router.utils.SingletonPool; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; Public Class ServiceLoader<I> {private static final Map<Class, private static final Map<Class, ServiceLoader> SERVICES = new HashMap<>(); private static final LazyInitHelper sInitHelper = new LazyInitHelper("ServiceLoader") {
        @Override
        protected void doInit() {try {// reflection calls Init class to avoid referencing too many classes, Class.forname (conconst.SERVICE_LOADER_INIT).getMethod(conconst.INIT_METHOD).invoke(null); Debugger.i("[ServiceLoader] init class invoked"); } catch (Exception e) { Debugger.fatal(e); }}}; /** * @see LazyInitHelper#lazyInit()
     */
    public static void lazyInit() { sInitHelper.lazyInit(); } /** * Public static void put(Class) public static void put(Class) public static void put(Class interfaceClass, String key, Class implementClass, boolean singleton) { ServiceLoader loader = SERVICES.get(interfaceClass);if(loader == null) { loader = new ServiceLoader(interfaceClass); SERVICES.put(interfaceClass, loader); } loader.putImpl(key, implementClass, singleton); } /** * Get {@link ServiceLoader} */ @suppressWarnings ("unchecked")
    public static <T> ServiceLoader<T> load(Class<T> interfaceClass) {
        sInitHelper.ensureInit();
        if (interfaceClass == null) {
            Debugger.fatal(new NullPointerException("Serviceloader. load class parameter should not be null"));
            return EmptyServiceLoader.INSTANCE;
        }
        ServiceLoader service = SERVICES.get(interfaceClass);
        if (service == null) {
            synchronized (SERVICES) {
                service = SERVICES.get(interfaceClass);
                if(service == null) { service = new ServiceLoader(interfaceClass); SERVICES.put(interfaceClass, service); }}}return service;
    }

    /**
     * key --> class name
     */
    private HashMap<String, ServiceImpl> mMap = new HashMap<>();

    private final String mInterfaceName;

    private ServiceLoader(Class interfaceClass) {
        if (interfaceClass == null) {
            mInterfaceName = "";
        } else {
            mInterfaceName = interfaceClass.getName();
        }
    }

    private void putImpl(String key, Class implementClass, boolean singleton) {
        if(key ! = null && implementClass ! = null) { mMap.put(key, new ServiceImpl(key, implementClass, singleton)); }} /** * Create an implementation class instance of the specified key, using the {@link RouterProvider} method or a no-argument construct. For implementation classes that declare singletons, instances are not created repeatedly. * * @returnPublic <T extends I> T get(String key) {returncreateInstance(mMap.get(key), null); } /** * create an implementation instance of the specified key, constructed with the Context argument. For implementation classes that declare singletons, instances are not created repeatedly. * * @returnPublic <T extends I> T get(String key, Context Context) {returncreateInstance(mMap.get(key), new ContextFactory(context)); } /** * creates an implementation class instance of the specified key, using the specified Factory construct. For implementation classes that declare singletons, instances are not created repeatedly. * * @returnPublic <T extends I> T get(String key, IFactory factory) {returncreateInstance(mMap.get(key), factory); } /** * Create instances of all implementation classes using the {@link RouterProvider} method or a no-argument construct. For implementation classes that declare singletons, instances are not created repeatedly. * * @return*/ @nonnull public <T extends I> List<T>getAll() {
        returngetAll((IFactory) null); } /** * create instances of all implementation classes, constructed with the Context argument. For implementation classes that declare singletons, instances are not created repeatedly. * * @return*/ @nonnull public <T extends I> List<T> getAll(Context Context) {returngetAll(new ContextFactory(context)); } /** * creates instances of all implementation classes, using the specified Factory construct. For implementation classes that declare singletons, instances are not created repeatedly. * * @returnIt may return EmptyList, */ @nonnull public <T extends I> List<T> getAll(IFactory Factory) {Collection<ServiceImpl> services = mMap.values();if (services.isEmpty()) {
            return Collections.emptyList();
        }
        List<T> list = new ArrayList<>(services.size());
        for (ServiceImpl impl : services) {
            T instance = createInstance(impl, factory);
            if (instance != null) {
                list.add(instance);
            }
        }
        returnlist; } /** * gets the implementation class for the specified key. Note that for an implementation Class that declares a Singleton, you can still create a new instance after fetching the Class. * * @returnMay return NULL */ @suppressWarnings ("unchecked")
    public <T extends I> Class<T> getClass(String key) {
        return(Class<T>) mMap.get(key).getImplementationClazz(); } /** * get the classes of all implementation classes. Note that for an implementation Class that declares a Singleton, you can still create a new instance after fetching the Class. * * @returnIt is possible to return EmptyList with non-empty elements */ @suppressWarnings ("unchecked")
    @NonNull
    public <T extends I> List<Class<T>> getAllClasses() {
        List<Class<T>> list = new ArrayList<>(mMap.size());
        for (ServiceImpl impl : mMap.values()) {
            Class<T> clazz = (Class<T>) impl.getImplementationClazz();
            if (clazz != null) {
                list.add(clazz);
            }
        }
        return list;
    }

    @SuppressWarnings("unchecked")
    @Nullable
    private <T extends I> T createInstance(@Nullable ServiceImpl impl, @Nullable IFactory factory) {
        if (impl == null) {
            return null;
        }
        Class<T> clazz = (Class<T>) impl.getImplementationClazz();
        if (impl.isSingleton()) {
            try {
                returnSingletonPool.get(clazz, factory); } catch (Exception e) { Debugger.fatal(e); }}else {
            try {
                if (factory == null) {
                    factory = RouterComponents.getDefaultFactory();
                }
                T t = factory.create(clazz);
                Debugger.i("[ServiceLoader] create instance: %s, result = %s", clazz, t);
                returnt; } catch (Exception e) { Debugger.fatal(e); }}return null;
    }

    @Override
    public String toString() {
        return "ServiceLoader (" + mInterfaceName + ")";
    }

    public static class EmptyServiceLoader extends ServiceLoader {

        public static final ServiceLoader INSTANCE = new EmptyServiceLoader();

        public EmptyServiceLoader() {
            super(null);
        }

        @NonNull
        @Override
        public List<Class> getAllClasses() {
            return Collections.emptyList();
        }

        @NonNull
        @Override
        public List getAll() {
            return Collections.emptyList();
        }

        @NonNull
        @Override
        public List getAll(IFactory factory) {
            return Collections.emptyList();
        }

        @Override
        public String toString() {
            return "EmptyServiceLoader"; }}}Copy the code

First, the doInit method is provided externally, so that the system calls the init method of ServiceLoaderInit class by reflection. By calling serviceloader. put method, the interface, the key of the interface implementation class, and the interface implementation class are loaded into SERVICES and mMap in sequence. This completes the registration of the mapping. Router.getService(ilocationService. class, “keyValue”) is used to call ServiceLoader.

WMRouterServiceLoader itself, other service lookup, service implementation class initialization and so on are relatively simple, the following focus on the service implementation class instance caching logic.

@Nullable
private <T extends I> T createInstance(@Nullable ServiceImpl impl, @Nullable IFactory factory) {
    if (impl == null) {
        return null;
    }
    Class<T> clazz = (Class<T>) impl.getImplementationClazz();
    if (impl.isSingleton()) {
        try {
            returnSingletonPool.get(clazz, factory); } catch (Exception e) { Debugger.fatal(e); }}else {
        try {
            if (factory == null) {
                factory = RouterComponents.getDefaultFactory();
            }
            T t = factory.create(clazz);
            Debugger.i("[ServiceLoader] create instance: %s, result = %s", clazz, t);
            returnt; } catch (Exception e) { Debugger.fatal(e); }}return null;
}
Copy the code

The createInstance method determines whether to read the cached object instance from SingletonPool by judging imp.issingleton (). SingletonPool is a singleton CACHE class, which caches the mapping relationship between the corresponding class and the instantiated object through the static CACHE constant. The next time we need to read the SingletonPool, we can directly determine whether the singleton object exists in the CACHE. If there is a singleton object, we can directly fetch it.

Public class SingletonPool {private static final Map< class, Object> CACHE = new HashMap<>(); private static final Map< class, Object> CACHE = new HashMap<>(); @SuppressWarnings("unchecked")
    public static <I, T extends I> T get(Class<I> clazz, IFactory factory) throws Exception {
        if (clazz == null) {
            return null;
        }
        if (factory == null) {
            factory = RouterComponents.getDefaultFactory();
        }
        Object instance = getInstance(clazz, factory);
        Debugger.i("[SingletonPool] get instance of class = %s, result = %s", clazz, instance);
        return (T) instance;
    }

    @NonNull
    private static Object getInstance(@NonNull Class clazz, @NonNull IFactory factory) throws Exception {
        Object t = CACHE.get(clazz);
        if(t ! = null) {return t;
        } else {
            synchronized (CACHE) {
                t = CACHE.get(clazz);
                if (t == null) {
                    Debugger.i("[SingletonPool] >>> create instance: %s", clazz);
                    t = factory.create(clazz);
                    //noinspection ConstantConditions
                    if(t ! = null) { CACHE.put(clazz, t); }}}returnt; }}}Copy the code

The existence of SingletonPool eliminates the repeated instantiation process that ServiceLoader might require, but at the same time, the problem is that the life cycle of service objects is prolonged, which can lead to long-term memory usage. As a result, the framework provider specifically added a Singleton parameter to the annotation of the service implementation class for the user to determine whether the service implementation class is singleton to determine whether caching is required.

For common implementations of concrete service classes, annotations are written as follows:

@RouterService(interfaces = IAccountService.class, key = DemoConstant.SINGLETON, singleton = true)
public class FakeAccountService implements IAccountService {
    ...
    
}
Copy the code

At this point, the analysis of the ServiceLoader improvements in WMRouter is complete.


Four, conclusion

Based on the idea of service provision and discovery, the system’s own ServiceLoader and the evolution form based on this idea are widely used in practical projects. In essence, through service interface agreement, service registration and service discovery, the decoupling of service provider and service user is completed, which greatly expands the system scalability. The essence of service registration is to register the mapping between a service interface and a specific service implementation into a system or specific implementation. The process of service discovery, which is essentially matching concrete implementation classes to a system or specific implementation, is written in an interface-based programming style because service consumers and service providers are transparent and unaware of each other. The implementation and evolution of ServiceLoader based on THE SPI idea is often widely used for componentization, extensibility, and even plug-in modules with pluggable capabilities.