[TOC]

Extension point loading mechanism

Dubbo’s extension point loading comes from and enhances the STANDARD EXTENSION point discovery mechanism of the JDK, Service Provider Interface (SPI).

Dubbo improves the JDK standard SPI and addresses the following issues:

  • The STANDARD SPI of the JDK instantiates all the implementation of an extension point at once, which is time-consuming to initialize if an extension implementation is used, but wasteful to load if not used.
  • If the extension point fails to load, you cannot even get the name of the extension point.
  • Added support for IoC and AOP for extension points, where one extension point can inject other extension points directly in setters.

The Dubbo convention is to place the extension point configuration file Meta-INF/Dubbo/fully qualified name of the interface in the JAR package of the extension class. The content is as follows: Configuration name = fully qualified name of the extension implementation class. If multiple implementation classes exist, separate them with newlines.

Briefly introduce the Java SPI mechanism

The Service Provider Interface (SPI) is a Service discovery mechanism. It looks for and loads the implementation class automatically by meta-INF/Service in the ClassPath path.

In short, it is a dynamic replacement discovery mechanism. The implementation class is discovered only when the interface is running. You only need to add an implementation before the interface is running and describe the implementation to the JDK. The description can also be modified at any time to complete the implementation replacement.

Java provides a number of SPIs, allowing third parties to provide implementations for these interfaces; Common SPI include JDBC, JNDI, etc.

These SPI interfaces are provided by Java, and the SPI implementation is loaded into the CLASSPATHzhong as a dependent third-party JAR.

For example, Java provides Java. SQL. Driver and MySql implementation of com. MySql. Cj). The JDBC Driver, and exist in the meta-inf/service of the configuration: com. MySql. Cj). The JDBC Driver

Write a Demo implementation

Let’s write a simple Demo implementation to see:

  1. Let’s start with an interface

    package cc.ccoder.loader;
    
    public interface DownloadStrategy {
    
        void download(a);
    }
    Copy the code
  2. Each provides two implementation classes that can be understood as implementations of standard interfaces in third-party jars.

    package cc.ccoder.loader;
    
    public class HttpDownloadStrategy implements DownloadStrategy{
        @Override
        public void download(a) {
            System.out.println("Download via Http"); }}Copy the code
    package cc.ccoder.loader;
    
    public class SftpDownloadStrategy implements DownloadStrategy {
        @Override
        public void download(a) {
            System.out.println("Download using SFTP"); }}Copy the code
  3. Create an extension point configuration file in the meta-INF /services directory with the path to the fully qualified name of the meta-INF /services/ interface. Then we this example file name is cc ccoder. Loader. DownloadStrategy and name of the file content for a specific implementation class fully qualified name.

    cc.ccoder.loader.HttpDownloadStrategy
    cc.ccoder.loader.SftpDownloadStrategy
    Copy the code
  4. Write a test class to load the concrete implementation with the ServiceLoader

    package cc.ccoder.loader;
    
    import java.util.ServiceLoader;
    
    public class JavaSpiLoaderTest {
        public static void main(String[] args) {
            ServiceLoader<DownloadStrategy> serviceLoader = ServiceLoader.load(DownloadStrategy.class);
            // All interface implementations are instantiated at once
            for(DownloadStrategy downloadStrategy : serviceLoader) { downloadStrategy.download(); }}}Copy the code
  5. Look at the output

    As expected, all the interface implementations are instantiated at once.

    Connected to the target VM, address: '127.0.0.1:64219', transport: 'socket'Download Disconnected from the target VM, address:'127.0.0.1:64219', transport: 'socket'
    
    Process finished with exit code 0
    Copy the code

Normal extension point loading

With Java’s SPI mechanism mentioned above, Dubbo’s ExtensionLoader is an enhancement of SPI. Let’s modify the above example and write a test to see.

package cc.ccoder.loader;

import org.apache.dubbo.common.extension.SPI;

@SPI("http")
public interface DownloadStrategy {

    void download(a);
}
Copy the code

Add the @SPI annotation to the DownloadStrategy interface.

Modify the meta-INF /services configuration file

http=cc.ccoder.loader.HttpDownloadStrategy
sftp=cc.ccoder.loader.SftpDownloadStrategy
Copy the code

Run the test method

package cc.ccoder.loader;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class DubboSpiLoaderTest {

    public static void main(String[] args) {
        ExtensionLoader<DownloadStrategy> extensionLoader = ExtensionLoader.getExtensionLoader(DownloadStrategy.class);
        // Where the getexVector method loads the specific extension point
        DownloadStrategy downloadStrategy = extensionLoader.getExtension("http"); downloadStrategy.download(); }}Copy the code
The Http download mode is Process Finished with exit code0
Copy the code

As we can see from the way we get the extension point, the Dubbo extension point is to get a concrete instance, and does not need to instantiate all the implementation classes.

getExtensionLoader

. We learn from examples from ExtensionLoader getExtensionLoader method can be seen in similar to a factory method, obtain the Extension of Extension points, again by calling getExtension method was introduced into the expansion of the specified name available to the specified point implementation class.

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        if(! type.isInterface()) {throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
        if(! withExtensionAnnotation(type)) {throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
        // Check whether there is a local Loader of the extension point type
        // Create an ExtensionLoader and place the implementation classes of ExtensionFactory in the local map cache of this EXTENSION_LOADERS
        // Implementations of ExtensionFactory include SpiExtensionFactory and SpringExtensionFactory
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }
Copy the code

getExtension

GetExtensionClasses ().get(name) getExtensionClasses().get(name -> instance = injectexVector (T) WrapperClass. GetConstructor (type). NewInstance (instance)) # to iterate over all the wrapper implementation, instantiate the wrapper and extension point IOC injectionCopy the code

Obtain the Dubbo extension points out this method ExtensionLoader. GetExtension (String name) set out.

    public T getExtension(String name, boolean wrap) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        // Return the default extension point if the name passed is the string true
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        // If the Holder does not exist, create the cache object and take it from the cache to prevent failure to put the Holder into the cache.
        // Dubbo actually wraps the extension class inside the Holder class
        final Holder<Object> holder = getOrCreateHolder(name);
        Object instance = holder.get();
        if (instance == null) {
            // Double-check ensures thread safety
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    // Create the extension point if it is not in the cache
                    instance = createExtension(name, wrap);
                    // Put it into the cacheholder.set(instance); }}}return (T) instance;
    }
Copy the code

In the getDefaultexVector () method above, the value in @SPI is the default extension name and is assigned to cachedDefaultName, so you can get a default extension class.

createExtension

Next, see how the createExtension(String Name, Boolean WRAP) method creates an extension point.

 private T createExtension(String name, boolean wrap) {
        //name: SPI gets SpiExtensionFactoryClass<? > clazz = getExtensionClasses().get(name);if (clazz == null || unacceptableExceptions.contains(name)) {
            throw findException(name);
        }
        try {
            // Load the SpiExtensionFactory in the local cache Map(SpringExtensionFactory.spiExtensionFactory)
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                / / name: spi clazz: SpiExtensionFactory here reflection will load it to the cache
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            / / the IOC injection
            injectExtension(instance);


            if (wrap) {
            // Wrap decorationList<Class<? >> wrapperClassesList =new ArrayList<>();
                if(cachedWrapperClasses ! =null) {
                    wrapperClassesList.addAll(cachedWrapperClasses);
                    wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                    Collections.reverse(wrapperClassesList);
                }

                if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                    for(Class<? > wrapperClass : wrapperClassesList) { Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);if (wrapper == null|| (ArrayUtils.contains(wrapper.matches(), name) && ! ArrayUtils.contains(wrapper.mismatches(), name))) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } } } initExtension(instance);return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                    type + ") couldn't be instantiated: "+ t.getMessage(), t); }}Copy the code
  • IoC injection: There is one mentioned in the comment aboveExtensionFactory.injectExtensionMethod to remove dependencies that need to be injected from an extension pointExtensionFactoryAnd assign a value to.
  • Wrapper class decorations: Here’s onecachedWrapperClassesVariable, which is not empty, is traversed and gets the constructor of the concrete Class object, wrapping in the extension point just created by reflection and making an Ioc dependency.

getExtensionClasses

Continue to look at the key method getExtensionClasses() above for extension points

    privateMap<String, Class<? >> getExtensionClasses() {// Cache loadMap<String, Class<? >> classes = cachedClasses.get();if (classes == null) {
            //double-check
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    // Key method: get all extension pointsclasses = loadExtensionClasses(); cachedClasses.set(classes); }}}return classes;
    }
Copy the code

After loadExtensionClasses() is loaded, the classes variable should have two values,key-name and value-class, which are:

"http" -> {Class@1390} "class cc.ccoder.loader.HttpDownloadStrategy"
"sftp" -> {Class@1391} "class cc.ccoder.loader.SftpDownloadStrategy"
Copy the code

loadExtensionClasses

Then I’ll go ahead and look at the key method loadExtensionClasses()

    privateMap<String, Class<? >> loadExtensionClasses() {// Get the @SPI annotation on the extension point and cache the default extension point name
        cacheDefaultExtensionName();

        // Put all extension points into the cacheMap<String, Class<? >> extensionClasses =new HashMap<>();

        // The loading strategy is a factory method; Loads the specified folder
        //DubboInternalLoadingStrategy => META-INF/dubbo/internal/
        //DubboLoadingStrategy => META-INF/dubbo/
        //ServicesLoadingStrategy => META-INF/services/
        for (LoadingStrategy strategy : strategies) {
            loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
                    strategy.overridden(), strategy.excludedPackages());
            loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache"."com.alibaba"),
                    strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        }

        return extensionClasses;
    }
Copy the code

The previous version of loadingDirectory directly loadingDirectory these folders, after retuning the factory method class, the UML is as follows:

loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache"."com.alibaba"));
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache"."com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache"."com.alibaba"));
Copy the code

cachedDefaultExtensionName

Continue to the main line to look down, you need to look at cachedDefaultExtensionName did what () method

   /** * extract and cache default extension name if exists */
    private void cacheDefaultExtensionName(a) {
        // Get the @spi annotation
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation == null) {
            return;
        }
        // Get the value of the @spi annotation
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            // The default extension point can only be this one
            if (names.length > 1) {
                throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                        + ":" + Arrays.toString(names));
            }
            if (names.length == 1) {
                // Copy this value to cachedDefaultName
                cachedDefaultName = names[0]; }}}Copy the code

The loading of the default extension point corresponds to the previous one. The loading of the default extension point takes the value of the @SPI annotation and assigns it to the cachedDefaultName variable. When fetching the default extension point, it checks the variable and retrieves the default extension point from the cache.

loadDirectory

Then go back to the above loadDirectory method to load the files under the three paths, our configuration file with the extension interface qualified name as the file name, can be loaded by the loadingDirectory method to read.

private void loadDirectory(Map<String, Class<? >> extensionClasses, String dir, String type,boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
        // Take the relative path and the fully qualified class name as the file path
        / / such as: meta-inf/services/cc ccoder. Loader. HttpDownloadStrategy
        String fileName = dir + type;
        try {
            Enumeration<java.net.URL> urls = null;
            ClassLoader classLoader = findClassLoader();

            // try to load from ExtensionLoader's ClassLoader first
            // The first attempt is to load the property from ExtensionLoader's classloader. This is the default method in the LoaderStrategy interface and is false
            if (extensionLoaderClassLoaderFirst) {
                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                    urls = extensionLoaderClassLoader.getResources(fileName);
                }
            }

            if (urls == null| |! urls.hasMoreElements()) {if(classLoader ! =null) {
                    // Obtain the file according to the file path
                    urls = classLoader.getResources(fileName);
                } else{ urls = ClassLoader.getSystemResources(fileName); }}if(urls ! =null) {
                while (urls.hasMoreElements()) {
                    // Load resources to the contents of the file one by onejava.net.URL resourceURL = urls.nextElement(); loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages); }}}catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t); }}Copy the code

loadResource

The above loadDirectory method reads the extension point configuration from the configuration file in turn, and executes the loadResource method to load the resource.

private void loadResource(Map<String, Class<? >> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL,boolean overridden, String... excludedPackages) {
        try {
            // Create a stream ready to read the contents of the file
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                String line;
                String clazz = null;
                while((line = reader.readLine()) ! =null) {
                    // The locator # filters out comments in the file
                    final int ci = line.indexOf(The '#');
                    if (ci >= 0) {
                        line = line.substring(0, ci);
                    }
                    line = line.trim();
                    if (line.length() > 0) {
                        try {
                            String name = null;
                            // The locator =, which is the official record and is divided by =
                            int i = line.indexOf('=');
                            if (i > 0) {
                                // = The left side is name
                                // = The right side is class, the fully qualified class name of the extension point
                                name = line.substring(0, i).trim();
                                clazz = line.substring(i + 1).trim();
                            } else {
                                // We can also record the full qualified class name as the extension point
                                clazz = line;
                            }
                            if(StringUtils.isNotEmpty(clazz) && ! isExcluded(clazz, excludedPackages)) {// Load the class, using reflection to preload the fully qualified class name read above
                                loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden); }}catch (Throwable t) {
                            IllegalStateException e = new IllegalStateException(
                                    "Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +
                                            ", cause: " + t.getMessage(), t);
                            exceptions.put(line, e);
                        }
                    }
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", class file: " + resourceURL + ") in "+ resourceURL, t); }}Copy the code

In this case, the file stream is used to read the configuration file, filter the comment (#), read and parse the name and class contents line by line, and then use the loadClass method to load the fully qualified class names and names and cache them into extensionClasses.

loadClass

The next important thing is the loadClass method

   private void loadClass(Map<String, Class<? >> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,boolean overridden) throws NoSuchMethodException {
        // An extension class object must be a subclass of the extension point interface
        if(! type.isAssignableFrom(clazz)) {throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                    type + ", class line: " + clazz.getName() + "), class "
                    + clazz.getName() + " is not subtype of interface.");
        }
        // Adaptive extension point
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            // Set cachedWrapperClasses variable to clazz
            cacheAdaptiveClass(clazz, overridden);
        } else if (isWrapperClass(clazz)) {
            // Determine whether any argument in the extension point constructor is the current extension interface type
            // If so, decorates the class for the wrapper, adding the cachedWrapperClasses variable to the current clazz
            cacheWrapperClass(clazz);
        } else {
            // This is a common extension point
            // Get the default constructor, or throw an exception if there is no default constructor
            clazz.getConstructor();
            if (StringUtils.isEmpty(name)) {
                // If name is empty, check whether the @extension annotation exists on the class, and the value of the annotation is name =>extension.value
                Name => clazz.getSimplename ()
                name = findAnnotationName(clazz);
                if (name.length() == 0) {
                    throw new IllegalStateException(
                            "No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                }
            }

            String[] names = NAME_SEPARATOR.split(name);
            if (ArrayUtils.isNotEmpty(names)) {
                // Cache the name and annotation objects in the cacheActivateClass variable if the class has @activate annotation.
                cacheActivateClass(clazz, names[0]);
                for (String n : names) {
                    // Cache name key:clazz, value:name cachedNames.put(clazz, name);
                    cacheName(clazz, n);
                    Extensionclasses.put (name, clazz);saveInExtensionClass(extensionClasses, clazz, n, overridden); }}}}Copy the code

If the extension point is a normal extension point in the saveInExtensionClass method, put it in extensionClasses for later retrieval.

    /** * put clazz in extensionClasses */
    private void saveInExtensionClass(Map<String, Class<? >> extensionClasses, Class<? > clazz, String name,boolean overridden) { Class<? > c = extensionClasses.get(name);if (c == null || overridden) {
            / / in the extensionClasses
            extensionClasses.put(name, clazz);
        } else if(c ! = clazz) {// duplicate implementation is unacceptable
            unacceptableExceptions.add(name);
            String duplicateMsg =
                    "Duplicate extension " + type.getName() + " name " + name + " on " + c.getName() + " and " + clazz.getName();
            logger.error(duplicateMsg);
            throw newIllegalStateException(duplicateMsg); }}Copy the code

At this point we can recall that the acquisition of the @SPI extension points is almost complete and that only the ordinary extension points will be put into extensionClasses from loadClass.

  • If there is one in the class@AdaptiveAnnotation, put clazz incachedAdaptiveClassVariable, indicating that this class is an adaptive extension class
  • If the class’s function argument is of the extension point interface type, put it incachedWrapperClassesVariable to indicate that this class is a wrapper decorator class
  • If neither of the above is satisfied, the class is a normal extension class and is placed inextensionClassesIs obtained from the Map.

In general, extension points are normal extension points that we can use from extensionClasses, so we can use the createExtension method to get the Clazz object from the map using name and use reflection instantiation to use it. Class
clazz = getExtensionClasses().get(name);

IOC dependency injection mechanism

InjectExtension (Instance) is mentioned in the createExtension method above. Instead of simply returning an extension object when an extension class is acquired, this method is called and returned, and it is also called to inject dependencies and return in a wrapper class. So let’s look at what injectexVector (instance) does.

   private T injectExtension(T instance) {
        //objectFactory is a variable of ExtensionFactory, initialized by the class constructor according to the adaptive extension point
        // Similar to the IOC container factory, SpiExtensionFactory SpringExtensionFactory is implemented
        if (objectFactory == null) {
            return instance;
        }

        try {
            // Reflection traverses all methods in the instance
            for (Method method : instance.getClass().getMethods()) {
                // Check whether this is a setter method:
                StartWith ('set')
                Getparametertypes.length ()==1
                // The method is of public type Modifier
                if(! isSetter(method)) {continue;
                }
                /**
                 * Check {@link DisableInject} to see if we need auto injection for this property
                 */
                // Check whether the attribute needs to be automatically injected
                // If @disableInject is typed, no injection operation is required
                if(method.getAnnotation(DisableInject.class) ! =null) {
                    continue;
                }
                // Get the clazz for the method parameterClass<? > pt = method.getParameterTypes()[0];
                if (ReflectUtils.isPrimitives(pt)) {
                    continue;
                }

                try {
                    // Get the property in the method name that needs to be injected
                    // For example, the setVersion method gets the attribute version and puts the fourth lowercase to be version
                    String property = getSetterProperty(method);
                    // Use the clazz and name of the parameter to get the extension point class from the IOC container of ExtensionFactory
                    Object object = objectFactory.getExtension(pt, property);
                    if(object ! =null) {
                        // Call the set method to complete the injectionmethod.invoke(instance, object); }}catch (Exception e) {
                    logger.error("Failed to inject via method " + method.getName()
                            + " of interface " + type.getName() + ":"+ e.getMessage(), e); }}}catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }
Copy the code

The above methods can be summarized as follows:

  • The method must start with set, and the argument must be the dependency type to be injected
  • If this method is@DisableInjectTag, no injection is required
  • The aboveExtensionFactoryThe IOC containerobjectFactoryHow to obtain the extension point is the adaptive extension mechanism below.

Packaging decoration

We can see from the loadClass above that when we get an extension class, if any class is a wrapper class, the class will not be fetched in the extensionClasses class, but in cachedWrapperClasses to wrap the other extension examples when we return the extension examples.


            if (wrap) {
            // Wrap decorationList<Class<? >> wrapperClassesList =new ArrayList<>();
                if(cachedWrapperClasses ! =null) {
                    wrapperClassesList.addAll(cachedWrapperClasses);
                    wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                    Collections.reverse(wrapperClassesList);
                }
                // As mentioned earlier in loadClass, if you wrap decorator classes while extending them, you will put them into cachedWrapperClasses so that the wrapperClassList will not be empty
                if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                    for(Class<? > wrapperClass : wrapperClassesList) { Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);if (wrapper == null|| (ArrayUtils.contains(wrapper.matches(), name) && ! ArrayUtils.contains(wrapper.mismatches(), name))) {// This is where the package is injected
                            // Create an instance of the wrapper extension point using reflection and instantiate the wrapper extension point by passing the normal extension point as a constructor argument
                            // Think of it as a decorator pattern that enhances the original common extension point that can be withdrawn when RPC services are published and invokedinstance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); }}}}Copy the code

Adaptive extension mechanism

When ExtensionLoader initializes the constructor, there will be an objectFactory object. This object will be used in IOC dependency injection. It is obtained through the adaptive extension mechanism.

    private ExtensionLoader(Class
        type) {
        this.type = type;
        objectFactory =
                (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
Copy the code

All extension-point instances of the ExtensionFactory interface are retrieved, but the getAdaptiveExtension method is used, which means that calling the method gives you an adaptive extension, not a concrete extension class.

While we can get specific extension point classes, we don’t want to get them at first, but dynamically at run time. . If the start call ExtensionLoader getExtension () to obtain the extension points, so can’t change it later on.

It is understood that Dubbo dynamically loads extension points through a URL, which is also obtained through the configuration file. In this way, we can use ZooKeeper as the registry in the configuration file and redis as the configuration center after modifying the configuration file. Dubbo is driven according to the URL of the configuration file and adaptively loads different registries. It appears many times in the source code, and works with SPI to obtain the specified extension point above to enable flexible extension and dynamic configuration.

  • @AdaptiveWhen applied to classes, Dubbo treats the class as an adaptive extension class.
  • @AdaptiveApplied to methods, Dubbo dynamically generates code logic. For example,ProxyFactoryIs an adaptive extension class that dynamically generates a class calledProxyFactory$Adaptive, where all method logic is generated by Dubbo.
  • The general adaptive extension refers to that Dubbo gets the parameter from the URL, and uses dubbo’s @SPI method to get the extension point using the parameter, so as to achieve the adaptive effect. The above exampleProxyFactoryAdaptive extension classes are also annotated by @SPI.

getAdaptiveExtension

public T getAdaptiveExtension(a)
    -> createAdaptiveExtension(a)Get from cache otherwise create ->getAdaptiveExtensionClass(a).newInstance(a)AdaptiveExtensionClass ->getExtensionClasses(a)# Load all implementations of the current extension to see if any are marked @adaptive ->createAdaptiveExtensionClass(a)# Dynamically create a Adaptive implementation class if no implementation is labeled @adaptive ->createAdaptiveExtensionClassCode(a)-> compiler;compile(code, classLoader)# Compile Java code dynamically, load and instantiate classes ->injectExtension(instance)
Copy the code

Next, look for the getAdaptiveExtension method from the ExtensionLoader constructor and take that method as an entry point to see the adaptive extension point.

    public T getAdaptiveExtension(a) {
        // Cache fetch
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if(createAdaptiveInstanceError ! =null) {
                throw new IllegalStateException("Failed to create adaptive instance: " +
                        createAdaptiveInstanceError.toString(),
                        createAdaptiveInstanceError);
            }

            //double-check
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        // Create an adaptive extension instance
                        // The createAdaptiveExtension method needs to be concerned
                        instance = createAdaptiveExtension();
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("Failed to create adaptive instance: "+ t.toString(), t); }}}}return (T) instance;
    }
Copy the code

This method has the same logic as before, cache fetching, double-check, and creating an extension point instance.

We need to focus on the createAdaptiveExtension method here

createAdaptiveExtension

    private T createAdaptiveExtension(a) {
        try {
            / / getAdaptiveExtensionClass access to adaptive after class to the IOC injection, IOC before injection and logic
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: "+ e.getMessage(), e); }}Copy the code

This logic is simpler, getAdaptiveExtensionClass way to obtain the adaptive class, then the IOC injection, injection logic as mentioned earlier.

So next to see how getAdaptiveExtensionClass method for adaptive class.

getAdaptiveExtensionClass

    privateClass<? > getAdaptiveExtensionClass() {// Load all extension points
        getExtensionClasses();
        // The cachedAdaptiveClass cache variable is not empty if the @adaptive annotation exists on the extension point
        if(cachedAdaptiveClass ! =null) {
            // The @adaptive annotation exists on the class to indicate that the Adaptive extension class is manually coded and returned directly
            return cachedAdaptiveClass;
        }
        // indicates that without the @adaptive annotation flag, Dubbo will generate Adaptive extension classes
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }
Copy the code

In getAdaptiveExtensionClass will start use getExtensionClass loading all Adaptive extended class, and then judge whether there is @ the Adaptive annotation

  • If it exists on the class@AdaptiveAnnotation, it will be assigned tocacheAdaptiveClassObject indicating that this adaptive extension class is manually coded and returned directly.
  • If the constructor argument is an extension point interface type, then it is a decorator class.
  • Ordinary class, for ordinary extension class.

Now look at the class diagram.

Here we take a look at how AdaptiveExtensionFactory AdaptiveExtensionFactory adaptively creates extension classes.

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory(a) {
        // Customize the loading of the extension point class
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        for (String name : loader.getSupportedExtensions()) {
            // Put the extension point class directly into the factories after it is loaded
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        // Since the factories already have all the extension point classes of the ExtensionFactory, you can read them sequentially
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            // Return the name of the extension point
            if(extension ! =null) {
                returnextension; }}return null; }}Copy the code

You can see that AdaptiveExtensionFactory loads all ExtensionFactory classes in a custom way. You can dynamically load the IOC container by adding an ExtensionFactory class to the configuration file. Add one more IOC dependency source.

If there is an @adaptive annotation on the AdaptiveExtensionFactory class, it will be returned directly. If there is no annotation, dubbo will automatically create an Adaptive extension class. Here to return to the above createAdaptiveExtension method is the last line of the createAdaptiveExtensionClass ();

createAdaptiveExtensionClass

    privateClass<? > createAdaptiveExtensionClass() {// Create a generated class type =DownloadStrategy Extension point type, the class object of the extension point interface, and the default extension point name
        // The generate method is generated
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        // Get the ClassLoader of ExtensionLoader
        ClassLoader classLoader = findClassLoader();
        // The adaptive method takes the compiled Class and compiles the code to the classLoader to load it and finally programs a Class object
        org.apache.dubbo.common.compiler.Compiler compiler =
                ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }
Copy the code
  • Load creates a generated class of type extension point interface type and default extension point name
  • getExtensionLoadertheClassLoader
  • The adaptive approach takes the compiled class, compiles it and passes itClassLoaderLoad, and you end up with a Class object.

Generate generates an adaptive extension point class

The important point mentioned above is that the generate method is used to generate an adaptive extension point class.

 public String generate(a) {
        // no need to generate adaptive class since there's no adaptive method found.g
        // The @adaptive annotation cannot be found on the method. The annotation does not need to generate the Adaptive class because the Adaptive method cannot be found
        if(! hasAdaptiveMethod()) {throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
        }

        StringBuilder code = new StringBuilder();
        // Generate the package name package cc.ccoder.
        code.append(generatePackageInfo());
        / / import dependence on import org.apache.dubbo.com mon. The extension. ExtensionLoader;
        code.append(generateImports());
        ProxyFactory$Adaptive = ProxyFactory$Adaptive = ProxyFactory$Adaptive
        //public class %s$Adaptive implements %s {
        code.append(generateClassDeclaration());

        Method[] methods = type.getMethods();
        for (Method method : methods) {
            // Generate the method in turn
            code.append(generateMethod(method));
        }
        code.append("}");

        if (logger.isDebugEnabled()) {
            logger.debug(code.toString());
        }
        // Return code. The code structure is already returned
        return code.toString();
    }
Copy the code

The main steps are:

  • To determine if there is@AdaptiveAnnotations, whether you need to generate adaptive classes
  • To generate the package namepackage xxxxxx
  • Import dependenceinport org.apache.dubbo.common.extension.ExtensionLoader
  • Generate the class name, and here the class name will have a suffix$Adaptive.public class %s$Adaptive implements %s {
  • Then the interface methods for adaptive extension points are generated one by one
    • This is where we need to take a lookgenerateMethodHow are methods generated in methods
  • Returns the adaptive extension point class code

generateMethod

    private String generateMethod(Method method) {
        // Get the return value type of the method
        String methodReturnType = method.getReturnType().getCanonicalName();
        // Get the name of the method
        String methodName = method.getName();
        // Get the method content
        String methodContent = generateMethodContent(method);
        // Get the parameters of the method
        String methodArgs = generateMethodArguments(method);
        // The fetch method throws an exception
        String methodThrows = generateMethodThrows(method);
        // Use this template generation method
        //"public %s %s(%s) %s {\n%s}\n";
        return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
    }
Copy the code

We’ve got some ideas from this generation image. Public %s %s(%s) %s {\n%s}\n;} public %s %s(%s) %s {\n%s}\n; So here we just need to focus on how to get the method content. Generate method content using the generateMethodContext method.

generateMethodContext
 private String generateMethodContent(Method method) {
        // Get the @adaptive annotation on the method first
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
        if (adaptiveAnnotation == null) {
            / / if there is no @ this method the Adaptive annotation will not for this method to generate dynamic method And throw an exception UnsupportedOperationException
            return generateUnsupported(method);
        } else {
            // Get the URL parameter index
            int urlTypeIndex = getUrlTypeIndex(method);

            // found parameter in URL type
            // Find the parameter in the URL type. If it is not -1, it is found
            if(urlTypeIndex ! = -1) {
                // Null Point check
                // Add an arg == NULL validation
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
                // did not find parameter in URL type
                // There is no other method to determine whether the URL getUrl can be obtained
                code.append(generateUrlAssignmentIndirectly(method));
            }
            // The value on the get annotation represents the name value of the get when the extension point is obtained, or the value of the class name is used as the key if it does not exist
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

            / / to determine whether a parameter have Invocation type parameters and org. Apache. Dubbo. RPC. Invocation
            boolean hasInvocation = hasInvocationArgument(method);
            
            // Generate the code for the parameter value and NULL verification
            code.append(generateInvocationArgumentNullCheck(method));
            
            // Generate the code getMethodParameter getParameter getProtocol to get parameters from the URL
            code.append(generateExtNameAssignment(value, hasInvocation));
            // check extName == null?
            // Verify the code for extName== NULL
            code.append(generateExtNameNullCheck(value));
            
            // Generate the code for getExtensionLoader(xxx.class).geTexVector (extName)
            code.append(generateExtensionAssignment());

            // return statement
            // Generate the return statement extension point class execution method
            code.append(generateReturnAndInvocation(method));
        }

        return code.toString();
    }
Copy the code

The above code represents the method that generates the content of the extension point class method

  • First, judge whether there is @adaptive annotation on the method. If there is no dynamic method generated for this method and the following exception information is thrown, it means that the method does not support dynamic.

    throw new UnsupportedOperationException("The method %s of interface %s is not adaptive method!");
    Copy the code
  • Generate the getUrl method and dynamically obtain the configured parameter. At this time, the URL has been verified in the parameter

    if (arg%d == null) throw new IllegalArgumentException("url == null");
    %s url = arg%d;
    Copy the code

    If not will be in the other way if I can get to the url. The judgment by generateUrlAssignmentIndirectly methods.

       private String generateUrlAssignmentIndirectly(Method method) {
            // Get the parameter typeClass<? >[] pts = method.getParameterTypes();// Iterate over all the parameters in the method
            Map<String, Integer> getterReturnUrl = new HashMap<>();
            // find URL getter method
            for (int i = 0; i < pts.length; ++i) {
                for (Method m : pts[i].getMethods()) {
                    String name = m.getName();
                    // Whether the parameter method name starts with get or is longer than 3
                    // the method name isPublic
                    // isStatic for methods that are not static
                    Getparametertypes. length == 0
                    / / method return value for the URL type org.apache.dubbo.com mon. The URL
                    if ((name.startsWith("get") || name.length() > 3) && Modifier.isPublic(m.getModifiers()) && ! Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length ==0&& m.getReturnType() == URL.class) { getterReturnUrl.put(name, i); }}}if (getterReturnUrl.size() <= 0) {
                // getter method not found, throw
                throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
                        + ": not found url parameter or url attribute in parameters of method " + method.getName());
            }
    
            Integer index = getterReturnUrl.get("getUrl");
            if(index ! =null) {
                // Generate the code to get the URL
                return generateGetUrlNullCheck(index, pts[index], "getUrl");
            } else {
                Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
                returngenerateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey()); }}Copy the code

    So the next important thing is the generateGetUrlNullCheck method

generateGetUrlNullCheck
    /** * 1, test if argi is null * 2, test if argi.getXX() returns null * 3, assign url with argi.getXX() */
    private String generateGetUrlNullCheck(intindex, Class<? > type, String method) {
        // Null point check
        StringBuilder code = new StringBuilder();
        code.append(String.format("if (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\"); \n",
                index, type.getName()));
        code.append(String.format("if (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\"); \n",
                index, method, type.getName(), method));

        code.append(String.format("%s url = arg%d.%s(); \n", URL.class.getName(), index, method));
        return code.toString();
    }
Copy the code

As you can see from the comments above

  • Determine if the parameter is null and throw an exception
  • Determine if the parameter getXx is null and throws an exception
  • Assign the parameter value toorg.apache.dubbo.common.URLThis type.

So we end up getting the parameter from the URL

  • See if there are involocation type, if any, from the url access involocation. GetMethonName parameters

  • If no, the value is obtained from the @apaptive annotation. If multiple values exist, the value is obtained from Value1 and value2 successively.

    / / @ the Adaptive annotation value
    String[] value() default {};
    Copy the code
  • If more than one value is not fetched, it is fetched from the @SPI annotation.

    dubbo:/ / 10.20.54.65:20886 / cc. Ccoder. Exchange. Service. The facade. API. ExchangeFacade?
    Copy the code

DownloadStrategy$Adaptive

An extension point class is generated adaptively by adding the @adaptive annotation to the DownloadStrategy interface. Here is the generated extension point class.

package cc.ccoder.dubbodemo.loader;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class DownloadStrategy$Adaptive implements cc.ccoder.dubbodemo.loader.DownloadStrategy {
    public void download(a) {
        throw new UnsupportedOperationException("The method public abstract void cc.ccoder.dubbodemo.loader.DownloadStrategy.download() of interface cc.ccoder.dubbodemo.loader.DownloadStrategy is not adaptive method!");
    }

    public void download(org.apache.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("type");
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (cc.ccoder.dubbodemo.loader.DownloadStrategy) name from url (" + url.toString() + ") use keys([type])"); cc.ccoder.dubbodemo.loader.DownloadStrategy extension = (cc.ccoder.dubbodemo.loader.DownloadStrategy) ExtensionLoader.getExtensionLoader(cc.ccoder.dubbodemo.loader.DownloadStrategy.class).getExtension(extName); extension.download(arg0); }}Copy the code

url.getParameter(“type”); This type is the annotation @adaptive (value=”type”) that we apply to the extension point method, which then resolves the URL, adaptively finds the extension class, and executes the method

To summarize

  • ExtensionLoaderLoads a concrete extension point implementation class.
  • Each extension point will correspond to only oneExtensionLoaderThe instance
  • There will be only one instance of each extension point implementation, but an extension point implementation can have multiple names.
  • If you need to wait until runtime to decide which extension point to use to implement the class, use an adaptive extension point implementation instead.AdaptiveExtensionFactory
  • judge@AdaptiveAnnotations are used in@SPIAnnotated classes
  • If multiple wrappers exist for the extension point, the final order of execution is fluid and used internallyConcurrentHashSetstorage