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.