Dubbo source code article 07—SPI mysterious veil—principle article—next

Dubbo Source Code Chapter 07—SPI Mysterious Veil—Principle Chapter—Part 2

  • introduction
  • Get the extended instance object according to the name
  • Get the default extension instance object
  • Get extended instance objects in batches by condition
    • Example demonstration
  • summary

Introduction

Previous article: Dubbo Source Code 06-SPI Mystery Veil-Principle-Part 1 We traced the entire process of getAdaptiveExtension to obtain adaptive extension points. The core of the entire process is as follows:

private T createAdaptiveExtension() {<!-- -->
        T instance = (T) getAdaptiveExtensionClass().newInstance();
        instance = postProcessBeforeInitialization(instance, null);
        injectExtension(instance);
        instance = postProcessAfterInitialization(instance, null);
        initExtension(instance);
        return instance;
}

private Class<?> getAdaptiveExtensionClass() {<!-- -->
    getExtensionClasses();
    if (cachedAdaptiveClass != null) {<!-- -->
        return cachedAdaptiveClass;
    }
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

Because the purpose of the adaptive extension point in dubbo is to dynamically select the implementation class at runtime, the AOP capability will not be given to the adaptive extension point. From the above process, we have not found any Wrapper mechanism processing.

So in this article, we follow the ordinary extension class loading process to go through dubbo’s AOP processing process:

 ApplicationModel applicationModel = ApplicationModel. defaultModel();
        ExtensionLoader<FrameWork> extensionLoader = applicationModel.getExtensionLoader(FrameWork.class);
        FrameWork frameWork = extensionLoader. getExtension("guice");

Get the extended instance object according to the name

According to the name passed in as the serviceKey to load the corresponding extension implementation:

 public T getExtension(String name) {<!-- -->
        //The second parameter indicates whether to start Wrapper decoration for the current extension class
        T extension = getExtension(name, true);
        if (extension == null) {<!-- -->
            throw new IllegalArgumentException("Not find extension: " + name);
        }
        return extension;
    }
public T getExtension(String name, boolean wrap) {<!-- -->
        ...
        //If name is true, then get the default extension implementation
        if ("true".equals(name)) {<!-- -->
            return getDefaultExtension();
        }
        String cacheKey = name;
        if (!wrap) {<!-- -->
            cacheKey + = "_origin";
        }
        //query cache - return without creating a new Holder
        final Holder<Object> holder = getOrCreateHolder(cacheKey);
        Object instance = holder. get();
        //There is a cache, return directly, otherwise enter the creation logic
        if (instance == null) {<!-- -->
            synchronized (holder) {<!-- -->
                instance = holder. get();
                if (instance == null) {<!-- -->
                    //Create the core method of the extended class
                    instance = createExtension(name, wrap);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }
private T createExtension(String name, boolean wrap) {<!-- -->
        //getExtensionClasses method has been analyzed in the previous article, skip here
        Class<?> clazz = getExtensionClasses().get(name);
        //If there is no extension implementation of key=name, an exception will be thrown
        if (clazz == null || unacceptableExceptions. contains(name)) {<!-- -->
            throw findException(name);
        }
        try {<!-- -->
           // Determine whether the corresponding extension implementation type has already created an instance object--ensure singleton
            T instance = (T) extensionInstances. get(clazz);
            //If only the SPI file is parsed to form a <name, class> cache, the next step is to build a <class, signleInstance> cache for the current extension type
            if (instance == null) {<!-- -->
                //Use instantiationStrategy to instantiate the extended instance object --- the specific logic is in InstantiationStrategy
                //The instantiation logic is relatively simple: either it is the default construction, or the constructor can have parameters, but the parameter type must be a ScopeModel subclass
                extensionInstances. putIfAbsent(clazz, createExtensionInstance(clazz));
                instance = (T) extensionInstances. get(clazz);
                //Pre-post processing--dependency injection
                instance = postProcessBeforeInitialization(instance, name);
                injectExtension(instance);
                instance = postProcessAfterInitialization(instance, name);
            }
            //Different logic from adaptive extension point creation: determine whether the current extension instance needs to be decorated
            if (wrap) {<!-- -->
                List<Class<?>> wrapperClassesList = new ArrayList<>();
                //The collection of wrapper types related to the current extension class is completed in getExtensionClasses
                if (cachedWrapperClasses != null) {<!-- -->
                    wrapperClassesList.addAll(cachedWrapperClasses);
                    wrapperClassesList.sort(WrapperComparator. COMPARATOR);
                    Collections. reverse(wrapperClassesList);
                }
                
                //wrapper class collection is to satisfy the existence of a single-parameter copy constructor, and the parameter type is the current extended class type
                if (CollectionUtils. isNotEmpty(wrapperClassesList)) {<!-- -->
                    //Continuous loop, matryoshka creates layers of decorator objects
                    for (Class<?> wrapperClass : wrapperClassesList) {<!-- -->
                        //Wrapper annotation is used to achieve conditional decoration
                        Wrapper wrapper = wrapperClass. getAnnotation(Wrapper. class);
                        //If there is no Wrapper annotation on the wrapper class, it means that the decoration does not need to meet any conditions
                        //Otherwise, it is necessary to judge whether the condition is satisfied, and then it will be decorated if it is satisfied
                        boolean match = (wrapper == null) ||
                            ((ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) & amp; & amp;
                                !ArrayUtils.contains(wrapper.mismatches(), name));
                        if (match) {<!-- -->
                            //Satisfied, enter the decoration process
                            //1. Instantiate the current decoration class, using a single-parameter copy constructor
                            //2. Execute dependency injection process
                            instance = injectExtension((T) wrapperClass. getConstructor(type). newInstance(instance));
                            //3. Execute the post-processing process
                            instance = postProcessAfterInitialization(instance, name);
                        }
                    }
                }
            }

            // Warning: After an instance of Lifecycle is wrapped by cachedWrapperClasses, it may not still be Lifecycle instance, this application may not invoke the lifecycle.initialize hook.
            //Call the initialization interface --- pay attention to the above warning information, that is to say, after packaging, our packaging object may not inherit the lifecycle interface, so the initialization call will not happen
            initExtension(instance);
            return instance;
        } catch (Throwable t) {<!-- -->
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t.getMessage(), t);
        }
    }

Get the default extension instance object

Use the val value in the @SPI annotation as the serviceKey to load the corresponding extension implementation:

 public T getDefaultExtension() {<!-- -->
        //Load the SPI file and build the relevant cache, such as: <name,class>
        getExtensionClasses();
        //cachedDefaultName comes from the val value in the @SPI annotation
        if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {<!-- -->
            return null;
        }
        //This process has been mentioned above
        return getExtension(cachedDefaultName);
    }

Get extended instance objects in batches by condition

So far, we still haven’t explained the extensionLoader.getActivateExtensions() process. Let’s take a look at how to achieve batch acquisition of extension instance objects according to conditions:

 public List<T> getActivateExtension(URL url, String key, String group) {<!-- -->
        //Extract the value from the url according to the incoming key
        String value = url. getParameter(key);
        //If value is not empty, split according to "," as serviceKey
        return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
    }
 public List<T> getActivateExtension(URL url, String[] values, String group) {<!-- -->
        checkDestroyed();
        // solve the bug of using @SPI's wrapper method to report a null pointer exception.
        Map<Class<?>, T> activateExtensionsMap = new TreeMap<>(activateComparator);
        List<String> names = values == null ? new ArrayList<>(0) : asList(values);
        Set<String> namesSet = new HashSet<>(names);
        if (!namesSet.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {<!-- -->
            // Set conditional cache construction: <serviceKey,groups> and <serviceKey,keyParis>
            if (cachedActivateGroups. size() == 0) {<!-- -->
                synchronized (cachedActivateGroups) {<!-- -->
                    // cache all extensions
                    if (cachedActivateGroups. size() == 0) {<!-- -->
                        //Load the SPI resource file corresponding to the current extension class, and establish the relevant cache mapping, here is mainly cachedActivates mapping
                        //cachedActivates cached <serviceKey, @Activate annotation> (serviceKey is our SPI file: serviceKey=serivceImpl full class name)
                        //If the serviceKey is not specified in the configuration file, then it is the val value specified in the @Extension annotation, if there is no annotation, then it is the simple class name of the implementation class
                        getExtensionClasses();
                        //Process in turn all implementation classes annotated with @Activate annotation under the current extension class
                        for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {<!-- -->
                            //key is serviceKey
                            String name = entry. getKey();
                            //activate annotation
                            Object activate = entry. getValue();

                            String[] activateGroup, activateValue;
                            //Extract the value in the annotation
                            if (activate instanceof Activate) {<!-- -->
                                activateGroup = ((Activate) activate).group();
                                activateValue = ((Activate) activate). value();
                            } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {<!-- -->
                                activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                                activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                            } else {<!-- -->
                                continue;
                            }
                            //Cache <serviceKey, groups> mapping, that is, to activate the current implementation class, which grouping requirements need to be met (satisfy one of them)
                            cachedActivateGroups. put(name, new HashSet<>(Arrays. asList(activateGroup)));
                            //Cache <serviceKey, keyParis> mapping
                            //The value attribute in the activate annotation has two ways of writing: key1:val1 or key2
                            //The former means that there is a key-value pair of key1=val1 in the URL to meet the condition
                            //The post indicates that the existence of key2 in the URL means that the condition is met
                            String[][] keyPairs = new String[activateValue. length][];
                            for (int i = 0; i < activateValue. length; i ++ ) {<!-- -->
                                if (activateValue[i]. contains(":")) {<!-- -->
                                    keyPairs[i] = new String[2];
                                    String[] arr = activateValue[i].split(":");
                                    keyPairs[i][0] = arr[0];
                                    keyPairs[i][1] = arr[1];
                                } else {<!-- -->
                                    keyPairs[i] = new String[1];
                                    keyPairs[i][0] = activateValue[i];
                                }
                            }
                            cachedActivateValues. put(name, keyPairs);
                        }
                    }
                }
            }

            // traverse all cached extensions
            //Traverse <serviceKey, groups> mapping
            cachedActivateGroups.forEach((name, activateGroup) -> {<!-- -->
                // If the group matching condition passed in the function call is empty, or the group exists in the groups collection, the group matching condition is satisfied
                if (isMatchGroup(group, activateGroup)
                 //nameSet is obtained from the key in the incoming function, after obtaining the value from the url, and splitting it according to ", " to obtain a set
                 //The processing of serviceKey=name is removed here, because the logic will be processed below
                     & amp; & amp; !namesSet. contains(name)
                     & amp; & amp; !namesSet.contains(REMOVE_VALUE_PREFIX + name)
                    //Obtain and activate the current extension implementation class from the <serviceKey, keyParis> collection, which user-defined conditions need to be met
                    //The judging logic here is: if the user specifies something like @Active(value="key1:value1, key2:value2")
                    //Then the value of key1 will be taken out from the url first, compared with value1, and if it is equal, true will be returned directly, otherwise, the value of key2 will be taken out and continue to judge, that is to say, if any condition is met here, it will return true
                    //If only @Active(value="key1") is specified in the user annotation, then as long as key1 exists in the url, the condition is met
                     & amp; & amp; isActive(cachedActivateValues.get(name), url)) {<!-- -->
                    
                    //If both grouping conditions and user-defined conditions are met, then join the activateExtensionsMap collection <class,Instance>
                    activateExtensionsMap.put(getExtensionClass(name), getExtension(name));
                }
            });
        }

        if (namesSet. contains(DEFAULT_KEY)) {<!-- -->
           ...
        } else {<!-- -->
            // add extensions, will be sorted by its order
            for (int i = 0; i < names. size(); i ++ ) {<!-- -->
                String name = names. get(i);
                if (!name.startsWith(REMOVE_VALUE_PREFIX)
                     & amp; & amp; !namesSet.contains(REMOVE_VALUE_PREFIX + name)) {<!-- -->
                    if (!DEFAULT_KEY.equals(name)) {<!-- -->
                        //<serviceKey, class> collection contains serviceKey=name
                        if (containsExtension(name)) {<!-- -->
                            //Then add the extended implementation class of serviceKey=name to the result set
                            activateExtensionsMap.put(getExtensionClass(name), getExtension(name));
                        }
                    }
                }
            }
            //Return the final collection extension implementation class collection
            return new ArrayList<>(activateExtensionsMap. values());
        }
    }

The above paragraph may look confusing, but it doesn’t matter. The following figure explains in detail the entire process of filtering according to activation conditions:

  • If the keyParis corresponding to a serviceKey is empty, that is to say, the user does not customize the matching condition, then the condition branch returns true by default.
  • If the group passed in by the function is empty, the group matching condition will not be considered, and the condition branch will return true by default

Note: Before putting in the result set, the getExtension method is called to obtain the extension class, which means that in the scenario of obtaining extension instance objects in batches according to conditions, the implementation class enjoys AOP (Wrapper mechanism) support:

 activateExtensionsMap.put(getExtensionClass(name), getExtension(name));

Example demo

  • Extension interface, and its implementation class
@SPI("spring")
public interface FrameWork {<!-- -->
    @Adaptive
    String getName(URL url);
    String getInfo();
}

@Activate(value = {<!-- -->"name:dhy","age:18"}, group = "test")
public class Guice implements FrameWork{<!-- -->
    @Override
    public String getName(URL url) {<!-- -->
        return "guice";
    }

    @Override
    public String getInfo() {<!-- -->
        return "google open source lightweight IOC framework";
    }
}

@Activate(value = {<!-- -->"name","sex"}, group = "test")
public class Spring implements FrameWork{<!-- -->
    @Override
    public String getName(URL url) {<!-- -->
        return "spring";
    }

    @Override
    public String getInfo() {<!-- -->
        return "Popular Spring framework";
    }
}


@Activate(group = "prod")
public class SpringBoot implements FrameWork{<!-- -->
    @Override
    public String getName(URL url) {<!-- -->
        return "springBoot";
    }

    @Override
    public String getInfo() {<!-- -->
        return "Automated SpringBoot framework";
    }
}
  • SPI file content
spring=com.adaptive.Spring
springBoot=com.adaptive.SpringBoot
guice=com.adaptive.Guice
  • test class
class ActivateTest {<!-- -->
    @Test
    void activateTest() {<!-- -->
        ApplicationModel applicationModel = ApplicationModel. defaultModel();
        ExtensionLoader<FrameWork> extensionLoader = applicationModel.getExtensionLoader(FrameWork.class);
        List<FrameWork> frameWorkList = extensionLoader.getActivateExtension(URL.valueOf("dubbo://127.0.0.1:80/?age=18"), "", "test");
        frameWorkList.forEach(frameWork -> {<!-- -->
            System.out.println(frameWork.getInfo());
        });
    }
}


You can change the parameter conditions by yourself to test other branches.

Summary

This article mainly leads you through the loading process of ordinary extension classes, and sees the difference between ordinary extension class loading and adaptive extension point loading. The difference is that adaptive extension points do not have Wrapper mechanism support, because adaptive extension points The original intention of the design is to dynamically select the extension class according to the conditions at runtime.

The @Acativate annotation determines whether to activate the current extension implementation class according to the runtime conditions, and can activate all extension implementation classes that meet the conditions, so it is activated in batches by condition.

The extension implementation class activated by condition is just a layer of activation by condition on the basis of ordinary extension class. Therefore, when the condition is met, the getExtension method is also called to finally obtain the extension implementation class activated by condition. It can be seen that it also enjoys the Wrapper provided by dubbo. supported by the mechanism.

The self-adaptive extension point is passed to the URL through method parameters, and the internal self-adaptive extension point judges which extension to implement based on the conditions. The conditional activation of the extension is a conditional judgment within the getActivateExtension method, which you need to pay attention to.

Common extension class, adaptive extension point and conditionally activated extension class loading, all three methods enjoy dependency injection and pre- and post-processing support.