The architecture of The Dubbo SPI tells us the principle and implementation details of Dubbo SPI.

Follow my public account Solomon for more highlights

The principles and implementation details of Dubbo SPI

1. Introduction

SPI, also known as the Service Provider Interface, 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. Because of this feature, we can easily extend our programs through SPI mechanisms. The SPI mechanism is also used in third-party frameworks, such as Dubbo, which loads all components through the SPI mechanism. However, instead of using Java’s native SPI mechanism, Dubbo has enhanced it to better meet its needs. In Dubbo, SPI is a very important module. Based on SPI, we can easily extend Dubbo. If you want to learn Dubbo source, SPI mechanism must understand. Next, let’s take a look at the Java SPI and Dubbo SPI usage, and then analyze Dubbo SPI source code.

2. The SPI example

2.1 Java SPI Example

After a brief introduction to the SPI mechanism, this section uses an example to demonstrate how to use Java SPI. First, we define an interface called Robot.

public interface Robot {
    void sayHello(a);
}
Copy the code

Next, define two implementation classes, OptimusPrime and Bumblebee.

public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello(a) {
        System.out.println("Hello, I am Optimus Prime."); }}public class Bumblebee implements Robot {

    @Override
    public void sayHello(a) {
        System.out.println("Hello, I am Bumblebee."); }}Copy the code

Next create a file in the meta-INF /services folder named org.apache.spi.robot with the fully qualified name of Robot. The content of the file is the fully qualified class name of the implementation class, as follows:

org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee
Copy the code

Do the necessary preparatory work, then write code to test.

public class JavaSPITest {

    @Test
    public void sayHello(a) throws Exception {
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI"); serviceLoader.forEach(Robot::sayHello); }}Copy the code

Finally, the test results are as follows:

! [img](Principles and implementation details of Dubbo SPI. Assets /java-spi-result.jpg)

As can be seen from the test results, our two implementation classes were successfully loaded, and the corresponding output content. The Java SPI demo ends here, followed by the Dubbo SPI demo.

2.2 Dubbo SPI Example

Instead of using Java SPI, Dubbo has re-implemented a more powerful SPI mechanism. The logic for Dubbo SPI is encapsulated in the ExtensionLoader class, through which we can load the specified implementation class. The configuration file required by the Dubbo SPI must be saved in the meta-INF/Dubbo directory.

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
Copy the code

Unlike the Java SPI implementation class configuration, Dubbo SPI is configured through key-value pairs so that we can load the specified implementation class on demand. In addition, when testing Dubbo SPI, you need to annotate @SPI on the Robot interface. Here’s how to use the Dubbo SPI:

public class DubboSPITest {

    @Test
    public void sayHello(a) throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); }}Copy the code

The test results are as follows:

! [img](Principles and implementation details of Dubbo SPI. Assets /dubbo-spi-result.jpg)

In addition to its support for loading interface implementation classes on demand, Dubbo SPI adds features such as IOC and AOP, which are described in the following source code analysis sections.

3. Dubbo SPI source analysis

The previous chapter briefly demonstrated how to use the Dubbo SPI. We first get an instance of ExtensionLoader through its getExtensionLoader method, and then get the extensionClass object through its getExtension method. In this case, the getExtensionLoader method is used to get the ExtensionLoader corresponding to the extended class from the cache, or create a new instance if the cache misses. The logic of this method is simple and will not be analyzed in this chapter. Let’s start with the getExtension method of ExtensionLoader as an entry point for a detailed analysis of the acquisition process of extended class objects.

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // Get the default extension implementation class
        return getDefaultExtension();
    }
    // Holder, as the name implies, is used to hold the target object
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // Double check
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // Create an extended instance
                instance = createExtension(name);
                // Set the instance to the holderholder.set(instance); }}}return (T) instance;
}
Copy the code

The logic of the above code is relatively simple: first check the cache and create the extended object if the cache is not hit. Let’s look at what the process of creating an extension object looks like.

private T createExtension(String name) {
    // Load all extension classes from the config file to obtain the mapping table from "config item name" to "config class"Class<? > clazz = getExtensionClasses().get(name);if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // Create an instance through reflection
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // Inject dependencies into the instanceinjectExtension(instance); Set<Class<? >> wrapperClasses = cachedWrapperClasses;if(wrapperClasses ! =null && !wrapperClasses.isEmpty()) {
            // Loop to create the Wrapper instance
            for(Class<? > wrapperClass : wrapperClasses) {// Pass the current instance as an argument to the Wrapper constructor and create the Wrapper instance through reflection.
                // Then inject the dependency into the Wrapper instance, and finally assign the Wrapper instance to the instance variable againinstance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); }}return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("..."); }}Copy the code

The logic of the createExtension method is a little more complicated and includes the following steps:

  1. Get all extended classes through getExtensionClasses
  2. Create extension objects through reflection
  3. Inject dependencies into extended objects
  4. Wrap the extension object in the corresponding Wrapper object

Of the above steps, the first step is the key to loading the extension class, and the third and fourth steps are the concrete implementation of Dubbo IOC and AOP. In the following sections, we will focus on the logic of the getExtensionClasses method and briefly introduce the implementation of Dubbo IOC.

3.1 Get all extension classes

Before we obtain the extension class by name, we first need to resolve the mapping table between the extension name and the extension class according to the configuration file (Map< name, extension class >), and then take out the corresponding extension class from the mapping table according to the extension name. Code analysis of related processes is as follows:

privateMap<String, Class<? >> getExtensionClasses() {// Get the loaded extended class from the cacheMap<String, Class<? >> classes = cachedClasses.get();// Double check
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // Load the extension classclasses = loadExtensionClasses(); cachedClasses.set(classes); }}}return classes;
}
Copy the code

Here, the cache is checked first. If the cache is not hit, the lock is synchronized. After locking, the cache is checked again and nulled. If classes are still null, the extended class is loaded through loadExtensionClasses. Let’s examine the logic of the loadExtensionClasses method.

privateMap<String, Class<? >> loadExtensionClasses() {// Get the SPI annotation, where the type variable is passed in when the getExtensionLoader method is called
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if(defaultAnnotation ! =null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // Shards the SPI annotation content
            String[] names = NAME_SEPARATOR.split(value);
            // Check if the SPI annotation content is valid, throw an exception if it is not
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }

            // Set the default name, referring to the getDefaultExtension method
            if (names.length == 1) {
                cachedDefaultName = names[0]; } } } Map<String, Class<? >> extensionClasses =newHashMap<String, Class<? > > ();// Load the configuration file in the specified folder
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}
Copy the code

The loadExtensionClasses method does two things altogether: it parses SPI annotations and calls the loadDirectory method to load the specified folder configuration file. The SPI annotation parsing process is simple and needless to say. Let’s take a look at what loadDirectory does.

private void loadDirectory(Map
       
        > extensionClasses, String dir)
       ,> {
    // fileName = folder path + type Fully qualified name
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        // Load all files with the same name according to the filename
        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 resourcesloadResource(extensionClasses, classLoader, resourceURL); }}}catch (Throwable t) {
        logger.error("..."); }}Copy the code

The loadDirectory method first obtains all resource links through the classLoader and then loads the resources through the loadResource method. Let’s follow along and look at the implementation of the loadResource method.

private void loadResource(Map
       
        > extensionClasses, ClassLoader classLoader, java.net.URL resourceURL)
       ,> {
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            // Read the configuration line by line
            while((line = reader.readLine()) ! =null) {
                // Position the # character
                final int ci = line.indexOf(The '#');
                if (ci >= 0) {
                    // Cut the string before #. The content after # is a comment and should be ignored
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // Cut off the key and value with the equal sign = bound
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // Load the class and cache the class using the loadClass method
                            loadClass(extensionClasses, resourceURL, 
                                      Class.forName(line, true, classLoader), name); }}catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class..."); }}}}finally{ reader.close(); }}catch (Throwable t) {
        logger.error("Exception when load extension class..."); }}Copy the code

The loadResource method is used to read and parse configuration files, load classes through reflection, and finally call the loadClass method for other operations. The loadClass method is used primarily for operational caching. The logic of this method is as follows:

private void loadClass(Map
       
        > extensionClasses, java.net.URL resourceURL, Class
         clazz, String name)
       ,> throws NoSuchMethodException {
    
    if(! type.isAssignableFrom(clazz)) {throw new IllegalStateException("...");
    }

    // Check whether there are Adaptive annotations on the target class
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            // Set the cachedAdaptiveClass cache
            cachedAdaptiveClass = clazz;
        } else if(! cachedAdaptiveClass.equals(clazz)) {throw new IllegalStateException("...");
        }
        
    // Check whether clazz is a Wrapper type
    } else if(isWrapperClass(clazz)) { Set<Class<? >> wrappers = cachedWrapperClasses;if (wrappers == null) {
            cachedWrapperClasses = newConcurrentHashSet<Class<? > > (); wrappers = cachedWrapperClasses; }// Store clazz in the cachedWrapperClasses cache
        wrappers.add(clazz);
        
    Clazz is a common extension class
    } else {
        // Tests whether Clazz has a default constructor and throws an exception if it does not
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // If name is empty, try to get name from the Extension annotation, or use a lowercase class name as name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("..."); }}/ / shard name
        String[] names = NAME_SEPARATOR.split(name);
        if(names ! =null && names.length > 0) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if(activate ! =null) {
                // If there is an Activate annotation on the class, use the first element of the NAMES array as the key,
                // Store the mapping between name and the Activate annotation object
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if(! cachedNames.containsKey(clazz)) {// Store the mapping between classes and namescachedNames.put(clazz, n); } Class<? > c = extensionClasses.get(n);if (c == null) {
                    // Store the mapping between names and classes
                    extensionClasses.put(n, clazz);
                } else if(c ! = clazz) {throw new IllegalStateException("...");
                }
            }
        }
    }
}
Copy the code

As above, the loadClass method operates on different caches, such as cachedAdaptiveClass, cachedWrapperClasses, cachedNames, and so on. There is no other logic to this method.

This concludes the analysis of the caching class loading process. The whole process is not particularly complicated, we can step by step analysis, do not understand the place can be debugged. Next, let’s talk about Dubbo IOC.

3.2 Dubbo IOC

Dubbo IOC is about injecting dependencies through setter methods. Dubbo first retrieves all of the instance’s methods by reflection, then iterates through the list of methods 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 corresponding code for the whole process is as follows:

private T injectExtension(T instance) {
    try {
        if(objectFactory ! =null) {
            // Iterate over all methods of the target class
            for (Method method : instance.getClass().getMethods()) {
                // Checks whether the method starts with set, has only one argument, and has access level public
                if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                    // Get setter method parameter typesClass<? > pt = method.getParameterTypes()[0];
                    try {
                        // Get the property name, such as setName
                        String property = method.getName().length() > 3 ? 
                            method.getName().substring(3.4).toLowerCase() + 
                            	method.getName().substring(4) : "";
                        // Get the dependent object from ObjectFactory
                        Object object = objectFactory.getExtension(pt, property);
                        if(object ! =null) {
                            // Call setter methods via reflection to set dependenciesmethod.invoke(instance, object); }}catch (Exception e) {
                        logger.error("fail to inject via method...");
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}
Copy the code

In the above code, the objectFactory variable is of type AdaptiveExtensionFactory, which internally maintains a list of ExtensionFactories, Used to store extensionFactories of other types. Dubbo currently provides two extensionFactories, SpiExtensionFactory and SpringExtensionFactory. The former is used to create adaptive extensions, and the latter is used to get the required extensions from Spring’s IOC container. The code of the two classes is not very complicated.

Dubbo IOC currently only supports setter injection, and overall, the logic is fairly straightforward.

4. To summarize

This article briefly introduces the usage of Java SPI and Dubbo SPI respectively, and analyzes the loading process of Dubbo SPI extended classes. In addition, there is an important piece of logic in Dubbo SPI that has not been analyzed here, namely the extension point adaptive mechanism of Dubbo SPI.


yourLike and followIs the continuing power of the Solomon_ Shogo shell structure.

Hot historical Articles

  • 🔥Serverless Microservices elegant shutdown practices

  • 🔥 this algorithm can not understand! How are the 9 images presented

  • 🔥SpringBoot Mastery – Custom Condition annotations (Series 1)

  • 🔥Java is how to take off the beautiful woman’s clothes

  • 🔥 High-performance gateway was originally designed this way

  • 🔥REST FUL look still don’t understand, you play me!

  • How much 🔥Serverless affects programmers

  • 🔥 How does distributed transaction XID connect all microservices in tandem

  • 🔥 Microservices Distributed Transaction TCC core implementation

  • 🔥 hundreds of millions of traffic site performance optimization methodology steps

  • 🔥 microservice Nacos implements proximity access through CMDB to improve performance

  • 🔥 Micro service architecture DNS service registration and discovery mechanism

  • . More and more