04 Dubbo Spi Analysis Interface Implementation Polar Turnaround Below

04 Dubbo SPI Analysis Interface Implementation Polar Turnaround Below #

In the previous lesson, we learned about the basic usage and core principles of JDK SPI. However, Dubbo does not directly use the JDK SPI mechanism. Instead, it borrows its ideas and implements its own SPI mechanism, which is the focus of this lesson.

Dubbo SPI #

Before we dive into the implementation of Dubbo SPI, let’s first clarify two concepts:

  • Extension Point: An interface that is found and loaded through the SPI mechanism (also known as “extension interface”). The Log interface and com.mysql.cj.jdbc.Driver interface mentioned in the previous examples are both extension points.
  • Extension Point Implementation: The implementation class that implements the extension interface.

Through the previous analysis, we can see that during the process of finding extension implementations, JDK SPI needs to iterate through all the implementation classes defined in the SPI configuration file, instantiating all of them. If the SPI configuration file defines multiple implementation classes and we only need to use one of them, unnecessary objects will be created. For example, the org.apache.dubbo.rpc.Protocol interface has multiple implementations such as InjvmProtocol, DubboProtocol, RmiProtocol, HttpProtocol, HessianProtocol, and ThriftProtocol. If we use JDK SPI, all implementations will be loaded, resulting in resource waste.

Dubbo SPI not only solves the problem of resource waste mentioned above, but also provides extension and modification to the SPI configuration file.

First, Dubbo divides the SPI configuration file into three categories based on its purpose.

  • META-INF/services/ directory: The SPI configuration files in this directory are used to be compatible with JDK SPI.
  • META-INF/dubbo/ directory: This directory is used to store user-defined SPI configuration files.
  • META-INF/dubbo/internal/ directory: This directory is used to store SPI configuration files used internally by Dubbo.

Then, Dubbo changes the SPI configuration file to KV format, for example:

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

In this format, the key is called the extension name (also known as ExtensionName). When we are looking for a specific implementation class for an interface, we can specify the extension name to select the corresponding extension implementation. For example, by specifying the extension name as “dubbo”, Dubbo SPI knows that we want to use the org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol as the extension implementation class. Only this extension implementation needs to be instantiated, without instantiating other extension implementations defined in the SPI configuration file.

Another advantage of using KV format for the SPI configuration file is that it makes it easier for us to locate problems. For example, if the jar package containing a specific extension implementation we are using is not imported into the project, when Dubbo SPI throws an exception, it will carry the extension name information instead of simply indicating that the extension implementation class cannot be loaded. These more accurate exception messages reduce the difficulty of troubleshooting and improve the efficiency of problem-solving.

Now let’s get into the core implementation of Dubbo SPI.

1. @SPI Annotation #

When a certain interface in Dubbo is annotated with @SPI, it means that the interface is an extension interface. For example, the org.apache.dubbo.rpc.Protocol interface mentioned in the previous examples is an extension interface:

Drawing 0.png

The value of the @SPI annotation specifies the default extension name. For example, when loading Protocol interface implementation through Dubbo SPI, if the extension name is not explicitly specified, the value of the @SPI annotation will be used as the extension name by default. In this case, it will load the org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol as the extension implementation of the “dubbo” extension name. The related SPI configuration file is located in the dubbo-rpc-dubbo module, as shown in the following image:

Drawing 1.png

How does ExtensionLoader handle the @SPI annotation?

ExtensionLoader is located in the extension package of the dubbo-common module, which functions similarly to java.util.ServiceLoader in JDK SPI. Most of the core logic of Dubbo SPI is encapsulated in ExtensionLoader (including the processing logic of @SPI annotation). Here is an example of how it is used:

Protocol protocol = ExtensionLoader

   .getExtensionLoader(Protocol.class).getExtension("dubbo");

First, let’s understand the three core static fields in ExtensionLoader.

  • strategies (LoadingStrategy[] type): LoadingStrategy interface has three implementations (loaded through JDK SPI), as shown in the following image. They correspond to the three Dubbo SPI configuration files mentioned earlier, and all inherit the Prioritized interface. The default priority is

    DubboInternalLoadingStrategy > DubboLoadingStrategy > ServicesLoadingStrateg

Drawing 2.png

  • EXTENSION_LOADERS (ConcurrentMap type): In Dubbo, each extension interface corresponds to an ExtensionLoader instance. This collection caches all the ExtensionLoader instances, where the key is the extension interface and the value is the ExtensionLoader instance that loads its extension implementation.
  • EXTENSION_INSTANCES (ConcurrentMap type): This collection caches the mapping between extension implementation classes and their instance objects. In the previous examples, the key is the Class and the value is the DubboProtocol object.

Now let’s take a closer look at the instance fields of ExtensionLoader.

  • type (Class type): The current ExtensionLoader instance is responsible for loading the extension interface.
  • cachedDefaultName (String type): It records the value of the @SPI annotation on the type, which is the default extension name.
  • cachedNames (ConcurrentMap type): It caches the mapping between the extension implementation classes loaded by this ExtensionLoader and the extension names.
  • cachedClasses (Holder > type): It caches the mapping between the extension names loaded by this ExtensionLoader and the extension implementation classes. It is the reverse relationship cache of the cachedNames collection.
  • cachedInstances (ConcurrentMap type): It caches the mapping between the extension names loaded by this ExtensionLoader and the extension implementation objects.

The getExtensionLoader.getExtensionLoader() method looks for the corresponding ExtensionLoader instance based on the extension interface from the EXTENSION_LOADERS cache. The core implementation is as follows:

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {

    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;

}

After obtaining the ExtensionLoader object corresponding to the interface, the getExtension() method is called. It looks for the implementation instance of the extension from the cachedInstances cache based on the given extension name, and finally instantiates and returns it:

public T getExtension(String name) {

    // The getOrCreateHolder() method encapsulates the logic of looking up the cachedInstances cache

    Holder<Object> holder = getOrCreateHolder(name);

    Object instance = holder.get();

    if (instance == null) { // double-check to prevent concurrency issues

        synchronized (holder) {

            instance = holder.get();

            if (instance == null) {

                // Look up the corresponding extension implementation class from the SPI configuration file based on the extension name

                instance = createExtension(name);

                holder.set(instance);

            }

        }

    }

    return (T) instance;

}

In the createExtension() method, the SPI configuration file is searched, and the corresponding extension implementation class is instantiated. It also implements features such as autowiring and automatic wrapper. The core process is as follows:

  1. Retrieves the cachedClasses cache and gets the extension implementation class from the cachedClasses cache based on the extension name. If cachedClasses is not initialized, it scans the three SPI directories mentioned earlier to find the corresponding SPI configuration files, loads the extension implementation classes from them, and records the mapping between the extension name and the extension implementation class in the cachedClasses cache. This logic is implemented in the loadExtensionClasses() and loadDirectory() methods.
  2. Looks for the corresponding instance from the EXTENSION_INSTANCES cache based on the extension implementation class. If the lookup fails, it creates the extension implementation object using reflection.
  3. Autowires the properties in the extension implementation object (i.e., calls its setters). This involves the ExtensionFactory and related autowiring content, which will be explained in detail later.
  4. Automatically wraps the extension implementation object. This involves the Wrapper class and the automatic wrapping feature, which will be explained in detail later.
  5. If the extension implementation class implements the Lifecycle interface, the initialize() method is called for initialization in the initExtension() method.
private T createExtension(String name) {

    Class<?> clazz = getExtensionClasses().get(name); // --- 1

    if (clazz == null) {

        throw findException(name);

    }

    try {
private boolean isWrapperClass(Class<?> clazz) {
    try {
        // 判断是否包含拷贝构造函数
        clazz.getConstructor(type);
        return true;
    } catch (NoSuchMethodException e) {
        return false;
    }
}

所以,Wrapper 类的定义必须要满足以下条件:

  • 实现了扩展接口
  • 构造函数只有一个参数且为扩展接口类型
  1. 在 cacheWrapperClass() 方法中,将 Wrapper 类缓存到 cachedWrapperClasses 这个 Set 集合中:
private void cacheWrapperClass(Class<?> clazz) {
  if (cachedWrapperClasses == null) {
      cachedWrapperClasses = new ConcurrentHashSet<>();
  }
  cachedWrapperClasses.add(clazz);
}

之后,Dubbo 通过动态生成适配器类的方式解决了 ExtensionFactory、Protocol 这两个扩展的适配问题,但是像 Stub、Mock 这样的 Wrapper 类缓存起来之后,Dubbo 并没有像前两个适配器那样生成适配器类,是因为 Stub、Mock 这两个类具有特殊的意义。

  • Stub 用于在客户端存根中实现自定义的逻辑,一般在服务消费方使用。
  • Mock 用于在服务提供方提供默认的实现,一般在服务提供方使用。

接下来,我们来看一下如何使用自动包装特性。

当获取扩展实例的时候,会先从 cachedWrapperClasses 中获取所有 Wrapper 类,然后根据 Wrapper 类的拷贝构造函数,将真正的扩展实例包装一层 Wrapper,按照一定的顺序进行包装,获取到最终的扩展实例。

具体的过程是这样的:

  • 首先,将扩展实例封装为一个 Wrapper 实例,例如在调用 DubboProtocol.export() 方法时,会封装为一个 ServiceWrapper 实例,并缓存起来。
  • 接着,将 Wrapper 实例按照优先级从高到低排序。
  • 最后,通过调用 Wrapper 的 getExtension() 方法,传入扩展名和类型,来获取到最终的扩展实例。

这样,就实现了多个扩展实例的自动包装功能。

值得注意的是,调用 Wrapper 实例的逻辑,是按照包装顺序的逆序进行的。例如,在消费端,按照 Wrapper1 -> Wrapper2 -> Wrapper3 的顺序调用,在提供端是按照 Wrapper3 -> Wrapper2 -> Wrapper1 的顺序调用。

需要知道的是,Dubbo 中通过动态生成适配器类的方式解决了 ExtensionFactory、Protocol 这两个扩展的适配问题,但是像 Stub、Mock 这样的 Wrapper 类缓存起来之后,并没有像前两个适配器那样生成适配器类,是因为 Stub、Mock 这两个类具有特殊的意义。 2. The Wrapper class is cached in the instance field cachedWrapperClasses (Set<Class> type) for caching.

In the code snippet below, which is mentioned when introducing the createExtension() method, all Wrapper classes are iterated one by one and wrapped around the actual extension instance object:

Set<Class<?>> wrapperClasses = cachedWrapperClasses;

if (CollectionUtils.isNotEmpty(wrapperClasses)) {

    for (Class<?> wrapperClass : wrapperClasses) {

        instance = injectExtension((T) wrapperClass
            .getConstructor(type).newInstance(instance));

    }

}

4. Autowiring Feature #

In the createExtension() method, after obtaining the object of the extension implementation class (as well as the object of the Wrapper class), Dubbo SPI calls the injectExtension() method to scan all the setter methods, and based on the name and type of the setter method’s parameters, loads the corresponding extension implementation and fills the properties by invoking the setter methods, achieving the autowiring feature of Dubbo SPI. In simple terms, autowiring properties means that when loading an extension point, its dependent extension points are also loaded and autowired.

Now let’s take a look at the implementation of the injectExtension() method:

private T injectExtension(T instance) {

    if (objectFactory == null) { // check the objectFactory field
        return instance;
    }

    for (Method method : instance.getClass().getMethods()) {
        ... // if it is not a setter method, ignore this method

        if (method.getAnnotation(DisableInject.class) != null) {
            continue; // if the method is marked with @DisableInject annotation, ignore this method
        }

        // determine the extension interface based on the parameter of the setter method
        Class<?> pt = method.getParameterTypes()[0];
        ... // if the parameter is a primitive type, ignore this setter method

        // determine the property name based on the name of the setter method
        String property = getSetterProperty(method);

        // load and instantiate the extension implementation class
        Object object = objectFactory.getExtension(pt, property);

        if (object != null) {
            method.invoke(instance, object); // invoke the setter method to perform autowiring
        }
    }

    return instance;

}

The implementation of automatic wiring in the injectExtension() method relies on the ExtensionFactory (i.e., the objectFactory field). As mentioned earlier, ExtensionFactory has two real implementations: SpringExtensionFactory and SpiExtensionFactory (there is also an AdaptiveExtensionFactory that acts as an adapter). Now let’s introduce these two real implementations separately.

The first one, SpiExtensionFactory, obtains the corresponding adapter based on the extension interface and does not consider the property name:

@Override

public <T> T getExtension(Class<T> type, String name) {

    if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {

        // find the ExtensionLoader instance corresponding to the type
        ExtensionLoader<T> loader = ExtensionLoader
          .getExtensionLoader(type);

        if (!loader.getSupportedExtensions().isEmpty()) {
            return loader.getAdaptiveExtension(); // get the adaptive extension implementation
        }

    }

    return null;

}

The second one, SpringExtensionFactory, uses the property name as the name of the Spring Bean and obtains the Bean from the Spring container:

public <T> T getExtension(Class<T> type, String name) {

    ... // check if type is an interface and has the @SPI annotation (omitted)

    for (ApplicationContext context : CONTEXTS) {

        // find the Bean from the Spring container
        T bean = BeanFactoryUtils.getOptionalBean(context,name,type);

        if (bean != null) {
            return bean;
        }

    }

    return null;

}

5. @Activate Annotation and Automatic Activation Feature #

Here we take the Filter in Dubbo as an example to explain the meaning of the automatic activation feature. The org.apache.dubbo.rpc.Filter interface has many extension implementation classes, and in one scenario, several Filter extension implementation classes may need to work together, while in another scenario, several other implementation classes may need to work together. In this case, a set of configuration is needed to specify which Filter implementation classes are available in the current scenario, and this is what the @Activate annotation does.

The @Activate annotation is used to annotate the extension implementation class, and it has three attributes: group, value, and order.

  • The group attribute indicates whether the annotated implementation class is activated on the Provider side or the Consumer side.
  • The value attribute indicates that the annotated implementation class is only activated when the specified key appears in the URL parameters.
  • The order attribute is used to determine the order of the extension implementation class.

Let’s first look at the scanning of @Activate in the loadClass() method, where the implementation classes containing the @Activate annotation are cached in the cachedActivates instance field (Map type, Key is the extension name, Value is the @Activate annotation):

private void loadClass() {

    if (clazz.isAnnotationPresent(Adaptive.class)) {
        // process @Adaptive annotation
        cacheAdaptiveClass(clazz, overridden);
    } else if (isWrapperClass(clazz)) {
        // process Wrapper class
        cacheWrapperClass(clazz);
    } else {
        // process actual extension implementation class
        clazz.getConstructor(); // the extension implementation class must have a public parameterless constructor
        ...// fallback: if the extension name is not specified in the SPI configuration file, use the simple name of the class as the extension name (omitted)
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
// Cache the implementation classes that contain the @Activate annotation in the cachedActivates set
cacheActivateClass(clazz, names[0]);

for (String n : names) {

    // Cache the mapping of implementation class to extension name in the cachedNames set
    cacheName(clazz, n);

    // Cache the mapping of extension name to implementation class in the cachedClasses set
    saveInExtensionClass(extensionClasses, clazz, n, overridden);
}

The cachedActivates set is used in the getActivateExtension() method. First, let’s focus on the parameters of the getActivateExtension() method: the url contains the configuration information, values are the specified extension names in the configuration, and group is either Provider or Consumer. Here is the core logic of the getActivateExtension() method:

  1. Firstly, get the set of default activated extensions. The conditions for default activated extension implementation classes are: ① they exist in the cachedActivates set; ② the group attribute specified in the @Activate annotation matches the current group; ③ the extension name does not appear in values (i.e., not explicitly specified in the configuration nor explicitly specified for removal in the configuration); ④ the Key specified in the @Activate annotation is present in the URL.
  2. Then, sort the set of default activated extensions according to the order attribute in the @Activate annotation.
  3. Finally, add the objects of custom extension implementations in order.
public List<T> getActivateExtension(URL url, String[] values, String group) {

    List<T> activateExtensions = new ArrayList<>();

    // The values configuration is the extension names

    List<String> names = values == null ?
        new ArrayList<>(0) : asList(values);

    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) { // ---1

        getExtensionClasses(); // Trigger the loading of cachedActivates and other cached fields

        for (Map.Entry<String, Object> entry :
            cachedActivates.entrySet()) {

            String name = entry.getKey(); // Extension name
            Object activate = entry.getValue(); // @Activate annotation

            String[] activateGroup, activateValue;

            if (activate instanceof Activate) { // Configuration in @Activate annotation
                activateGroup = ((Activate) activate).group();
                activateValue = ((Activate) activate).value();
            } else {
                continue;
            }

            if (isMatchGroup(group, activateGroup) // Match the group
                // Default activated extension implementation, not present in the values configuration
                && !names.contains(name)
                // Explicitly specified extension implementation to be deactivated using "-"
                && !names.contains(REMOVE_VALUE_PREFIX + name)
                // Check whether the specified Key is present in the URL
                && isActive(activateValue, url)) {
                // Load the instance of the extension implementation, which is activated
                activateExtensions.add(getExtension(name));
            }
        }

        // Sort ---2
        activateExtensions.sort(ActivateComparator.COMPARATOR);
    }

    List<T> loadedExtensions = new ArrayList<>();

    for (int i = 0; i < names.size(); i++) { // ---3
        String name = names.get(i);
        // Ignore extension implementation explicitly specified as deactivated using "-"
        if (!name.startsWith(REMOVE_VALUE_PREFIX)
            && !names.contains(REMOVE_VALUE_PREFIX + name)) {
            if (DEFAULT_KEY.equals(name)) {
                if (!loadedExtensions.isEmpty()) {
                    // Add custom extensions to the front of the default extension set in order
                    activateExtensions.addAll(0, loadedExtensions);
                    loadedExtensions.clear();
                }
            } else {
                loadedExtensions.add(getExtension(name));
            }
        }
    }

    if (!loadedExtensions.isEmpty()) {
        // Add custom extensions to the end of the default extension set in order
        activateExtensions.addAll(loadedExtensions);
    }

    return activateExtensions;
}

Finally, let’s use a simple example to illustrate the above processing flow. Suppose the cachedActivates set caches the following extension implementations:

Extension Name Activate Annotation
demoFilter1 group = “Provider”
demoFilter2 group = “Consumer”
demoFilter3 group = “Provider”
demoFilter4 group = “Provider”
demoFilter6 group = “Provider”

When the getActivateExtension() method is called at the Provider side and the values configuration is set to “demoFilter3, -demoFilter2, default, demoFilter1”, according to the above logic:

  1. The default activated extension implementations will be [demoFilter4, demoFilter6].
  2. After sorting, the order will be [demoFilter6, demoFilter4].
  3. After adding custom extension implementations in order, the final result will be [demoFilter3, demoFilter6, demoFilter4, demoFilter1].

Summary #

In this lesson, we have thoroughly explained the core implementation of Dubbo SPI: firstly, we introduced the underlying implementation of the @SPI annotation, which is the core foundation of Dubbo SPI; then we explained the core principles and implementation of the @Adaptive annotation and the dynamic generation of adaptive class; finally, we analyzed the automatic wrapping and autowiring features in Dubbo SPI, as well as the principles of the @Activate annotation.

Dubbo SPI is the core of Dubbo framework’s extension mechanism. We hope that you carefully study its implementation to lay a foundation for the subsequent analysis of the source code.

We also welcome you to share your learning experiences and practical knowledge in the comments section.