Dubbo’s extension mechanism is based on Dubbo SPI. It can be said that SPI is the core of Dubbo’s extension mechanism, so if you want to read Dubbo’s source code, first read Dubbo SPI source code is a very good choice.

What is the SPI

SPI, short for Service Provider Interface, is a set of apis provided by Java that can be implemented or extended by third parties. It is a Service discovery mechanism. The essence of SPI is to configure the fully qualified name of the interface implementation class in a file that is read by the service loader to load the implementation class. This lets you dynamically replace the implementation class for the interface at run time.

The sample

package org.apache.dubbo.demo.provider.javaspi;
​
    public interface JavaSpi {
​
    void say(String str);
​
}
Copy the code
package org.apache.dubbo.demo.provider.javaspi;
​
public class DemoOneJavaSpi implements JavaSpi{
    @Override
    public void say(String str) {
        System.out.println("one:"+ str); }}Copy the code
package org.apache.dubbo.demo.provider.javaspi;
​
public class DemoTwoJavaSpi implements JavaSpi{
  @Override
  public void say(String str) {
    System.out.println("two:"+ str); }}Copy the code

Configuration file path: meta-inf/services (this path is fixed), the file name: org. Apache. The dubbo. Demo. The provider. Javaspi. Javaspi

org.apache.dubbo.demo.provider.javaspi.DemoOneJavaSpi
org.apache.dubbo.demo.provider.javaspi.DemoTwoJavaSpi
Copy the code
package org.apache.dubbo.demo.provider.javaspi;
​
import org.junit.jupiter.api.Test;
import java.util.ServiceLoader;
​
public class JavaSpiTest {
​
  @Test
  public void test1(a) {
    ServiceLoader<JavaSpi> load = ServiceLoader.load(JavaSpi.class);
    load.forEach(item -> item.say("demo")); }}Copy the code

Dubbo SPI

Instead of using Java’s native SPI mechanism, Dubbo has enhanced it to provide dependency injection (DI) functionality as long as it satisfies API extensions. Unlike the Java SPI, the Dubbo SPI configuration file is stored in the meta-INF/Dubbo directory, configured in key-value mode, and the interface to be extended must be annotated with @SPI annotation.

The sample

package org.apache.dubbo.demo.provider.dubbospi;
​
import org.apache.dubbo.common.extension.SPI;
​
@SPI
public interface DubboSpi {
​
  void say(String str);
​
}
Copy the code

Configuration file path: meta-inf/dubbo (this path is fixed), the file name: org. Apache. The dubbo. Demo. The provider. Javaspi. Javaspi

one=org.apache.dubbo.demo.provider.dubbospi.DemoOneDubboSpi
two=org.apache.dubbo.demo.provider.dubbospi.DemoTwoDubboSpi
Copy the code
package org.apache.dubbo.demo.provider.dubbospi;
​
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.junit.jupiter.api.Test;
​
import java.util.ServiceLoader;
​
public class DubboSpiTest {
​
    @Test
    public void test2(a) {
        ExtensionLoader<JavaSpi> extensionLoader =
                ExtensionLoader.getExtensionLoader(DubboSpi.class);
        DubboSpi one = extensionLoader.getExtension("one");
        one.say("test one");
        JavaSpi two = extensionLoader.getExtension("two");
        two.say("test two"); }}Copy the code

From the above examples can probably know, Dubbo SPI is encapsulated in ExtensionLoader, is through ExtensionLoader getExtensionLoader (Class tClass) method to get its extension.

Source code analysis

Gets the extension loader

public class ExtensionLoader<T> {
  /** * Load dubbo SPI configuration, type must be interface, and must have {@linkSPI} tag * /
  @SuppressWarnings("unchecked")
  public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null) {
      throw new IllegalArgumentException("Extension type == null");
    }
    // Non-interface throws invalid arguments
    if(! type.isInterface()) {throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
    }
    // None the @spi annotation throws an invalid argument
    if(! withExtensionAnnotation(type)) {throw new IllegalArgumentException("Extension type (" + type +
              ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    }
    // Validate the ExtensionLoader cache, create it if it does not exist, and add it to the cache
    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;
  }

  /** * constructor */
  private ExtensionLoader(Class
        type) {
    // Interface class that needs to be extended
    this.type = type;
    // Extend the class factory
    // The Dubbo SPI adaptive extension mechanism will be used later
    
       
        
        
       
      
    // Get the specific ExtensionFactory from the getAdaptiveExtension method of ExtensionLoader
      
    objectFactory = (type == ExtensionFactory.class ? null: ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); }}Copy the code

The above is to obtain the source code of the extended class loader, there is no too complicated place, according to the process down, is nothing more than to obtain the cache of ExtensionLoader, does not exist to create and add to the cache. It is worth noting that the assignment to objectFactory in the constructor is to obtain the adaptive extension, which is not analyzed here and will be explained later when the adaptive extension mechanism of Dubbo SPI is analyzed. The Dubbo SPI entry method is getExtension(String name).

Gets the extended class instance

/** * find the extension with the given name. If the specified name is not found, throws {@link IllegalStateException}
   */
  @SuppressWarnings("unchecked")
  public T getExtension(String name) {
    return getExtension(name, true);
  }

  public T getExtension(String name, boolean wrap) {
    if (StringUtils.isEmpty(name)) {
      throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
			// Get the default extension implementation class, specified by the @spi annotation or null if not configured
      return getDefaultExtension();
    }
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
      synchronized (holder) {
        instance = holder.get();
        if (instance == null) {
					// Create an extension instance
          instance = createExtension(name, wrap);
					// Set to Holderholder.set(instance); }}}return (T) instance;
  }
Copy the code

Inside the getExtension method there are two ways to get an instance. The first way is to get the default instance, whose name is specified by the @spi annotation applied to the extension class, and the second way is to get an instance based on the extension class name. The first way returns to the second way, so let’s continue analyzing the source code for the second way to get an extension instance. As you can see from the above, there is a double check in the second way to get the extended instance, first get the cache, then create the cache if it does not exist. It is worth noting that there is a double check and lock operation, why locking the instance is thread safe? We can go to the getOrCreateHolder method, which is also unlocked, but uses the ConcurrentMap, which is thread-safe to keep the thread safe.

Creating an extension instance

/** * Create extension class * by name@paramName Extension class name *@paramWhether the wrap class needs to be used *@return T
     */
    @SuppressWarnings("unchecked")
    private T createExtension(String name, boolean wrap) {
        // Get the extension classClass<? > clazz = getExtensionClasses().get(name);if (clazz == null) {
            throw findException(name);
        }
        try {
            // Fetch from the cache
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                // Create and add to cache
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }

            // Attribute injection
            injectExtension(instance);

            // Use a wrapper class. If a wrapper class exists, return the wrapper class instead of an instance of the class
            if(wrap) { List<Class<? >> wrapperClassesList =new ArrayList<>();
                // Get all wrapper classes for this class
                if(cachedWrapperClasses ! =null) {
                    wrapperClassesList.addAll(cachedWrapperClasses);
                    // Sort the different wrapper classes
                    wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                    Collections.reverse(wrapperClassesList);
                }

                if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                    // If there are multiple wrapper classes, the wrapper class obtained in the previous loop will be injected into the new wrapper class through the constructor
                    // This ensures that multiple wrapper classes are possible in the case of multiple wrapper classes. It is worth mentioning that the execution order can be specified for different wrapper classes
                    // TODO implements the sort of wrapper class to be determined
                    for(Class<? > wrapperClass : wrapperClassesList) { Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);// If the extension class uses the @wrapper annotation, that class should not use the Wrapper class
                        if (wrapper == null|| (ArrayUtils.contains(wrapper.matches(), name) && ! ArrayUtils.contains(wrapper.mismatches(), name))) {//instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); }}}}Lifecycle is implemented and the initialize() method is executed if Lifecycle is implemented
            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

The createExtension method has a complex logic. The logic is to obtain the extension class, instantiate the extension class, inject IOC, use the proxy, and initialize the extension class.

Get extended classes
 /** * gets the extension class, double-checked */
privateMap<String, Class<? >> getExtensionClasses() { Map<String, Class<? >> classes = cachedClasses.get();if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // Load the extension classclasses = loadExtensionClasses(); cachedClasses.set(classes); }}}return classes;
}
Copy the code

The code for the getExtensionClasses method is easy to understand. Load the extension class when the cache does not exist and add it to the cache. Next, look at the loadExtensionClasses method.

/** * load the extension class and synchronize */ in getExtensionClasses
privateMap<String, Class<? >> loadExtensionClasses() {// Extract and cache the default extension from @spicacheDefaultExtensionName(); Map<String, Class<? >> extensionClasses =new HashMap<>();

    // Iterate through the loading policy to load the Dubbo SPI profile
    for (LoadingStrategy strategy : strategies) {
        // Here is the method to load and parse the Dubbo SPI configuration file
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
      	// Compatible with the old version, after ali team donated Dubbo to Apach, the package name was changed
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache"."com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}
Copy the code

The loadExtensionClasses method does two things. The first action is to parse the @spi annotation on the extension class and get its default value. If the default value is not empty, set it to the name of the implementation class of the default extension class and add it to the cache. The second is to iterate over the Dubbo SPI load policy and load and parse the Dubbo SPI configuration file using the loadDirectory method.

/** * Load and parse the Dubbo SPI configuration *@paramExtensionClasses extensionClasses *@paramDir Name of the package to load *@paramType Requires class * to be loaded@paramExtensionLoaderClassLoaderFirst extensions loader class loader first *@paramOverridden to cover *@paramExcludedPackages */
private void loadDirectory(Map<String, Class<? >> extensionClasses, String dir, String type,boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
    // The file name to load is in the following format
    // META-INF/services/org.apache.dubbo.common.context.FrameworkExt
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls = null;
        // Get the class loader
        ClassLoader classLoader = findClassLoader();

        // Try loading from the ClassLoader of ExtensionLoader first
        if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
            if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                urls = extensionLoaderClassLoader.getResources(fileName);
            }
        }

        if (urls == null| |! urls.hasMoreElements()) {if(classLoader ! =null) {
                urls = classLoader.getResources(fileName);
            } else{ urls = ClassLoader.getSystemResources(fileName); }}if(urls ! =null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                / / load the resourcesloadResource(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

The loadDirectory method first resolves the complete configuration file path, obtains the ClassLoader, then obtains all resource links through the ClassLoader, and finally loads resources through the loadResource.

/** * Load and parse the Dubbo SPI configuration *@paramExtensionClasses extensionClasses *@paramClassLoader classLoader *@paramResourceURL SPI resource file *@paramOverridden Whether ridden *@paramExcludedPackages */
private void loadResource(Map<String, Class<? >> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL,boolean overridden, String... excludedPackages) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            while((line = reader.readLine()) ! =null) {
                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;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // Get the key of the Dubbo SPI configuration file
                            name = line.substring(0, i).trim();
                            / / get Dubbo SPI value of configuration file, such as one = org. Apache. Dubbo. Demo. The provider. Javaspi. DemoOneDubboSpi
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
                            / / load the classes
                            loadClass(extensionClasses, resourceURL, Class.forName(line, 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

The loadResource method only does two things. First, it reads the resource information, parses the name of the extension class implementation and class name on each line as per the convention of the Dubbo SPI configuration file, and finally calls the loadClass method to load the corresponding extension class.

/ * * * *@paramExtensionClasses extensionClasses *@paramResourceURL Resource file *@paramClazz needs to load classes *@paramName Name of the class to load *@paramOverridden Whether ridden */
private void loadClass(Map<String, Class<? >> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,boolean overridden) throws NoSuchMethodException {
    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.");
    }
    // Check if there is an @adaptive annotation on the target class
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        // Set the cache
        cacheAdaptiveClass(clazz, overridden);
     // Check whether the class is of wrapper type
    } else if (isWrapperClass(clazz)) {
        // Set the cache
        cacheWrapperClass(clazz);
    // This is a normal extension class
    } else {
        // Checks if the class has a default constructor, or throws an exception
        clazz.getConstructor();
        // If name is null, try to get name from the Extension annotation, or use a lowercase class name as name
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config "+ resourceURL); }}/ / shard name
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            // Use the first element of the NAMES array if there is an Activate annotation on the class
            // Store the mapping between name and the Activate annotation object
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // Store the mapping between classes and names
                cacheName(clazz, n);
                // Store the mapping between names and classessaveInExtensionClass(extensionClasses, clazz, n, overridden); }}}}Copy the code

The loadClass method has three cases for loading extended classes: There’s no logic here as to whether the extension class has @Adaptive annotations, whether it’s a wrapper class, or whether it’s a normal extension class, except that it operates on various caches such as cacheAdaptiveClass and cacheWrapperClasses, cacheNames, and so on. Now, we don’t know what the purpose of this is, but the first case is an adaptive extension mechanism for the Dubbo SPI, which we will explain in detail in the Dubbo SPI adaptive extension. The functions, usage methods and principles of Wrapper and Adaptive are explained in detail.

After the getExtensionClasses method has been executed, we instantiate the extension class through reflection and then inject the instance through injectExtension

Dubbo IOC
/** * IOC, based on setter methods to implement *@paramInstance Extended class instance */
    private T injectExtension(T instance) {

        if (objectFactory == null) {
            return instance;
        }

        try {
          	// A method to traverse the instance
            for (Method method : instance.getClass().getMethods()) {
                if(! isSetter(method)) {continue;
                }
                /**
                 * Check {@link DisableInject} to see if we need auto injection for this property
                 */
                if(method.getAnnotation(DisableInject.class) ! =null) {
                    continue; } Class<? > pt = method.getParameterTypes()[0];
                // A method that filters out arguments as primitive types
                if (ReflectUtils.isPrimitives(pt)) {
                    continue;
                }

                try {
                    String property = getSetterProperty(method);
                    Object object = objectFactory.getExtension(pt, property);
                    if(object ! =null) {
                        / / 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

As shown above, Dubbo first retrieves all of the instance’s methods by reflection, and then iterates through the method list to check if the method name has setter method characteristics. If so, the dependency object is obtained through ObjectFactory, and finally setter methods are called through reflection to set the dependency into the target object. The @disableInject annotation is available on the setter method, which does not require injected dependencies.

Going back to creating the extension class instance, we can see that after IOC injection, the logic of whether or not to use the Wrapper class will be executed, which will be examined in detail in the Dubbo source code section -Wrapper explanation.

Finally, a summary of the Dubbo SPI loading process:

  1. Through ExtensionLoader. GetExtensionLoader (Class clazz) method to get to the extension Class loader
  2. Parses and caches the Dubbo SPI configuration file (including but not limited to names, extended classes) using the getExtensionClass() method in ExtentsionLoader
  3. Instantiate the extension class and add it to the cache through getExtension(String Name)
  4. IOC injection of instantiated extended classes, based on setters
  5. Return the extended class instance