Dubbo SPI mechanism involves @SPI, @Adaptive and @Activate annotations. As the core of Dubbo SPI mechanism, ExtensionLoader is responsible for loading and managing extension points and their implementation. In this paper, ExtensionLoader source code as the main line of analysis, and then lead to the role of three annotations and working mechanism.

ExtensionLoader is designed to get instances only through the getExtensionLoader(Class

type) method. The parameter type indicates the type of extension point that the instance is responsible for loading. To avoid confusion in later source analysis, keep this in mind: Each ExtensionLoader can only load concrete implementations of its bound extension point type (that is, the type of Type). That is, if type is protocol. class, then the ExtensionLoader instance can only load the implementation of the Protocol interface, not the implementation of the Compiler interface.

How do I get the extension implementation

In Dubbo, if an interface annotates the @spi annotation, it represents an extension point type, and the implementation of the interface is the implementation of the extension point. For example, the Protocol interface declaration:

@SPI("dubbo")
public interface Protocol {}
Copy the code

There can be multiple implementations of an extension point, and you can use the value attribute of the @SPI annotation to specify the default implementation to choose. Dubbo automatically selects the default implementation when the user does not explicitly specify which implementation to use.

The getExtension(String Name) method gets an instance of the extension implementation of the specified name. The extension implementation must be of the extension type of the current ExtensionLoader binding. This method checks to see if there is an instance of the extension implementation in the cache, and if there is no instance created through createExtension(String name). Dubbo sets up multiple layers of caching in this section, and calls getExtensionClasses() after createExtension(String name) to retrieve all extension implementations that have been loaded by the current ExtensionLoader. If not, call loadExtensionClasses() to actually load it.

privateMap<String, Class<? >> loadExtensionClasses() {// Take the value on the @spi annotation (only one value is allowed) and save it to cachedDefaultNamecacheDefaultExtensionName(); Map<String, Class<? >> extensionClasses =new HashMap<>();
  // Different policies represent different directories and load iteratively
  for (LoadingStrategy strategy : strategies) {
    // loadDirectory(...)
    // Execute different policies
  }
  return extensionClasses;
}
Copy the code

CacheDefaultExtensionName () method from the current ExtensionLoader binding type up get @ SPI annotations, And save its value to the cachedDefaultName field of the ExtensionLoader that represents the name of the default extension implementation for the extension point.

The loading policy configured by SPI

Then iterate over the three extension implementation load strategies. Strategies are loaded using the loadLoadingStrategies() method, in which the three strategies have been prioritized, with the low-priority strategy being placed first. A brief look at the LoadingStrategy interface:

public interface LoadingStrategy extends Prioritized {
    String directory(a);
    default boolean preferExtensionClassLoader(a) {
        return false;
    }
    default String[] excludedPackages() {
        return null;
    }
    default boolean overridden(a) {
        return false; }}Copy the code

The overridden() method indicates whether the current ridden policy extension implementation can override the ridden policy extension implementation with a lower priority. Priority is controlled by the Prioritized interface. It is important to pre-order load policies in order to make it easy to override when loading an extension implementation. This is why the loadLoadingStrategies() method is sorted.

Find and parse SPI configuration files

The loadDirectory() method looks up the SPI configuration file in the directory specified by the current policy and loads it as a java.net.URL object. The loadResource() method then parses the configuration file line by line. The Dubbo SPI configuration file is in the form of key=value, where key represents the name of the extension implementation and value is the specific class name of the extension implementation. In this case, the extension implementation is loaded directly after split, and finally handed over to the loadClass() method.

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("...");
  }
  / / class
  if (clazz.isAnnotationPresent(Adaptive.class)) {
    cacheAdaptiveClass(clazz, overridden);
  } else if (isWrapperClass(clazz)) { / / wrapper classes
    cacheWrapperClass(clazz);
  } else {
    clazz.getConstructor(); // Checkpoint: Extension classes must have a no-argument constructor
    // Backpocket strategy: If the configuration file is not written as key=value, take the simple name of the class as key, i.e. name
    if (StringUtils.isEmpty(name)) {
      name = findAnnotationName(clazz);
      if (name.length() == 0) {
        throw new IllegalStateException("..." + resourceURL);
      }
    }

    String[] names = NAME_SEPARATOR.split(name);
    if (ArrayUtils.isNotEmpty(names)) {
      // If the current implementation class annotates @activate, it will be cached
      cacheActivateClass(clazz, names[0]);
      // The extension implementation can separate a number of names with commas (a,b,c= com.xxx.yyy)
      for (String n : names) {
        // Cache extension implementation instance -> name
        cacheName(clazz, n);
        // Cache name -> instance of the extension implementationsaveInExtensionClass(extensionClasses, clazz, n, overridden); }}}}Copy the code

The cacheAdaptiveClass() method is a handle on @adaptive, which I’ll cover later.

A wrapper class

Look at the isWrapperClass() method, which determines whether the currently instantiated extension implementation is a wrapper class. The criterion is simple: a class is a wrapper class as long as it has a constructor with a single parameter of the same type as the extension type of the current ExtensionLoader binding.

In Dubbo, Wrapper classes end in a Wrapper, such as QosProtocolWrapper:

public class QosProtocolWrapper implements Protocol {
  private Protocol protocol;
	// Wrap the necessary constructor for the class
  public QosProtocolWrapper(Protocol protocol) {
    if (protocol == null) {
      throw new IllegalArgumentException("protocol == null");
    }
    this.protocol = protocol;
  }
  
  @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (UrlUtils.isRegistry(invoker.getUrl())) { // Some extra logic
            startQosServer(invoker.getUrl());
            return protocol.export(invoker);
        }
        return protocol.export(invoker);
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (UrlUtils.isRegistry(url)) { // Some extra logic
            startQosServer(url);
            return protocol.refer(type, url);
        }
        returnprotocol.refer(type, url); }}Copy the code

As you can see, the wrapper classes in Dubbo are really an implementation of AOP, and multiple wrapper classes can be continuously nested, similar to the design of the Java I/O class library. Back to the loadClass() method, if it is currently a wrapper class, it is stored in the cachedWrapperClasses collection.

Backstop non-standard SPI profile

In the last else branch of the loadClass() method, the no-argument constructor for the current extension implementation is first fetched, since it will be needed later to instantiate the extension implementation, which is a pre-check. The next step is to take a backstop, since the SPI configuration file may not be written as key=value as Dubbo requires, so the class name of the extension implementation class is used as the key. The cacheActivateClass() method is used to determine whether the current extension implementation carries an @Activate annotation and, if so, to cache it, the usefulness of which will be discussed later.

Multiple caches of extension implementations and their names

Finally, the name of the extension implementation and the Class object of the extension implementation are bidirectional cache. The cacheName() method maps the Class object to the extended implementation name, and saveInExtensionClass() maps the extended implementation name to the Class object.

The overridden() parameter of saveInExtensionClass() method actually comes from the overridden() method of LoadingStrategy LoadingStrategy. As mentioned above, the three loading strategies are iterated in ascending order of priority. Therefore, as long as the current LoadingStrategy allows the extended implementation created by the previous strategy to be overridden, then overridden is true.

This is essentially all the execution logic for the loadExtensionClasses() method, When the method completes, all implementation classes of the extension type bound by the current ExtensionLoader are loaded into Class objects and placed into cachedClasses.

Instantiate the extension implementation

Return to createExtension(String name) and throw an exception if the current extension implementation is not found in the loaded extension implementation class. Then try to fetch the corresponding instance from the cache, if not instantiate it and put it in the cache. The injectExtension() method is used to initialize and assign values to other extension implementations on which the currently instantiated extension implementation depends through reflection.

An ExtensionFactory objectFactory is used, and AdaptiveExtensionFactory is implemented as an AdaptiveExtensionFactory. SpiExtensionFactory and SpringExtensionFactory are adapted. When we want to get an extension implementation, we call the AdaptiveExtensionFactory getExtension(Class

type, String name) method.

public <T> T getExtension(Class<T> type, String name) {
  for (ExtensionFactory factory : factories) {
    T extension = factory.getExtension(type, name);
    if(extension ! =null) {
      returnextension; }}return null;
}
Copy the code

This method attempts to call the getExtension() method of each implementation to get the extension implementation. SpiExtensionFactory looks for an extension implementation from Dubbo’s own container, essentially calling the ExtensionLoader method to implement it, as a facade. SpringExtensionFactory, as its name implies, looks up extension implementations from within the Spring container, since Dubbo is often used in conjunction with Spring.

Going back to the createExtension(String Name) method, we iterate over the wrapper class that was saved when the extension implementation was loaded, scrolling through the last wrapped instance as the constructor parameter for the next wrapper class, which means that the final extension implementation is the last wrapper instance. Finally, if the extension implementation has a Lifecycle interface, its Initialize () method is called to initialize the Lifecycle. At this point, an extension implementation is created!

How do I choose which extension implementation to use

As mentioned in the loadClass() method, if the loaded extension implementation has the @adaptive annotation, The cacheAdaptiveClass() method will assign this extended implementation to cachedAdaptiveClass according to the overridden setting of the loading strategy.

@ the Adaptive function

Extension points in Dubbo generally have many extension implementations, which simply means that there are many implementations of an interface. Interfaces cannot be instantiated, so find a concrete implementation class to instantiate at run time. Adaptive is used to decide which implementation to choose at run time. If the annotation on the class indicates that the class is an adaptation class, it can be directly assigned to the cachedAdaptiveClass field of the ExtensionLoader when loading the extension implementation, such as AdaptiveExtensionFactory mentioned above.

So to summarize, the adaptation class is the class that is used to select the specific extension implementation when the extension point is actually used.

Adaptive can also be marked on the interface method, indicating that the method body should be dynamically generated at runtime by bytecode generation tool, and specific implementation should be selected within the method body to be used, such as Protocol interface:

@SPI("dubbo")
public interface Protocol {
  @Adaptive
  <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
 	@Adaptive
  <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}
Copy the code

Obviously, each implementation of Protocol has its own logic for exposing and referencing services, and it is not a good choice to parse and instantiate the Protocol to be used directly based on the URL. As a Spring application engineer, you should immediately think that IoC is the right way to go. Dubbo’s developers (probably) thought the same thing, but it didn’t seem right to create an IoC of their own, so bytecode enhancement was used to do it.

Creation of dynamic adaptation classes

If an extension point does not carry @Adaptive annotations on any of its implementation classes, but does carry @Adaptive annotations on some of its methods, this means that Dubbo needs to use bytecode enhancement tools to dynamically create a proxy class for the extension point at run time. Select the specific extension implementation in the method of the same name of the proxy class.

A bit abstract, let’s look at the getAdaptiveExtension() method of ExtensionLoader. This method gets the adaptation class of the current ExtensionLoader binding, first from cachedAdaptiveInstance, which holds the result of the cachedAdaptiveClass instantiation mentioned above. If not, the createAdaptiveExtension() method is called after a double-checked lock to create the adaptation class.

CreateAdaptiveExtension () method is called getAdaptiveExtensionClass () method to get the adapter Class Class object, namely cachedAdaptiveClass mentioned above, The Class is then instantiated and injected with the injectExtension() method.

After cachedAdaptiveClass getAdaptiveExtensionClass () method found no value to call createAdaptiveExtensionClass dynamically generated an adapter class () method. The methods covered here are very simple and will not post code, let’s look at the method of dynamically generating adaptation.

privateClass<? > createAdaptiveExtensionClass() { String code =new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
  ClassLoader classLoader = findClassLoader();
  org.apache.dubbo.common.compiler.Compiler compiler = 
    ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class)
    .getAdaptiveExtension();
  return compiler.compile(code, classLoader);
}
Copy the code

First call the generate AdaptiveClassCodeGenerator Class to adapter Class generation () method, and then also need the Compiler of walking mechanism of SPI get adapter classes compiling, finally the compiled adapter Class returns the Class object.

Dubbo using javassist framework for dynamically generated adapter classes, AdaptiveClassCodeGenerator class of the generate () method actually do is string concatenation of adapter class files. There is nothing special about the specific generation logic, it is all string operations, here is a simple example:

@SPI
interface TroubleMaker {
    @Adaptive
    Server bind(arg0, arg1);
  
    Result doSomething(a);
}
public class TroubleMaker$Adaptive implements TroubleMaker {
  
  	public Result doSomething(a) {
      throw new UnsupportedOperationException("The method doSomething of interface TroubleMaker is not adaptive method!");
    }
  
    public Server bind(arg0, arg1) {
        TroubleMaker extension =
            (TroubleMaker) ExtensionLoader
                .getExtensionLoader(TroubleMaker.class)
                .getExtension(extName);
        returnextension.bind(arg0, arg1); }}Copy the code

If there is an extension point called TroubleMaker, then the dynamically generated Adaptive class is called TroubleMaker$Adaptive. The Adaptive class will throw an exception for methods that are not annotated with @Adaptive. The @Adaptive annotation actually uses ExtensionLoader to find the specific extension implementation to use, and then calls the extension implementation’s method of the same name.

The choice of extension implementation follows the following logic:

  • read@AdaptiveannotationsvalueProperty, ifvalueWithout a value, the current extension point interface name is converted to a dot-separated form, such as TroubleMaker to trouble. Maker. This is then used as the key to get the concrete extension implementation to use from the URL.
  • If not obtained in the previous step, the extension point interface@SPIannotationsvalueThe value is taken as the key and retrieved from the URL.

How do I enable the extension implementation

Some extension implementations of extension points can be used concurrently and can be enabled as required, such as the Filter extension point’s multiple extension implementations. This brings up two questions, one is how to enable the extension and the other is whether the extension can be enabled. Dubbo provides the @Activate annotation to mark the enabling conditions of the extension.

public @interface Activate {
  String[] group() default {};
  String[] value() default {};
}
Copy the code

Dubbo is known to be on both client and server sides, and the group is used to specify where the extension can be enabled. The value can only be consumer or Provider, and the corresponding constant is located in CommonConstants. Value specifies the condition under which the extension implementation is enabled, that is, if the URL can get A value that is not null (that is, not false, 0, null, or N/A) through the getParameter(value) method, the extension implementation will be enabled.

For example, there is an extension implementation FilterX with a Filter extension point:

@Activate(group = {CommonConstants.PROVIDER}, value = "x")
public class FilterX implements Filter {}
Copy the code

The FilterX extension implementation is enabled if the server side is currently loading the extension implementation and url.getParameter(“x”) gets a non-null value. Note that the value attribute of @activate does not need to have the same value as the key in the SPI configuration file, and the value can be an array.

Enable the way the extension is implemented

The first way to enable this is to use value as the key of the URL and not empty, Enabling another extension implementation goes back to the getActivateExtension(URL URL, String Key, String Group) method of ExtensionLoader.

The key parameter represents a parameter that exists on the URL, whose value specifies the extension implementation to enable, separated by commas, and the group parameter indicates whether it is currently on the server side or the client side. This method splits the value obtained by the key argument and calls the overloaded getActivateExtension(URL URL, String[] values, String group) method, which is the key to enabling the extension implementation.

The first is to determine whether there is -default in the list of extension implementation names to be enabled, where – is a minus sign, meaning “to remove”, default means the default enabled extension implementation, so -default means to remove the default enabled extension implementation. Extension implementations that are enabled by default are those that carry an @Activate annotation but have no value for the annotation’s value, such as ConsumerContextFilter. As a corollary, if the extension implementation name is preceded by -, the extension implementation is not enabled.

If -default is not available, iterate through cachedattributes to determine which attributes are available. The key is isActive(String[] keys, URL URL). This method has no comments in the source code and can be difficult to understand. It’s essentially checking whether the keys passed in exist on the URL.

Note that cachedattributes the map value not a Class object or instance of the aspartame attribute that extends the attributes attributes of the aspartame attribute. Because cachedClasses and cachedInstances already hold both, you can get them with the name of the extension implementation, and there is no need to save a separate copy.

Going back to the other branch of the method, if there is -default, it turns on only the extension implementation specified on the URL, and handles names that carry -. The method finally returns all the extension implementations to be enabled into the activateExtensions collection.

Example of enabling an extended implementation

Personally, I think Dubbo SPI is suitable for video source analysis, because there is a lot of logic involved in it, which is not easy to explain in words. So here is an example to illustrate the extension implementation enablement logic described above. Suppose the following five custom filters now exist:

public class FilterA implements Filter {}

@Activate(group = {CommonConstants.PROVIDER}, order = 2)
public class FilterB implements Filter {}

@Activate(group = {CommonConstants.CONSUMER}, order = 3)
public class FilterC implements Filter {}

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 4)
public class FilterD implements Filter {}

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 5, value = "e")
public class FilterE implements Filter {}
Copy the code

Configuration file meta-inf/dubbo/internal/org. Apache. Dubbo. RPC. The Filter

fa=org.apache.dubbo.rpc.demo.FilterA
fb=org.apache.dubbo.rpc.demo.FilterB
fc=org.apache.dubbo.rpc.demo.FilterC
fd=org.apache.dubbo.rpc.demo.FilterD
fe=org.apache.dubbo.rpc.demo.FilterE
Copy the code

Start by looking directly at the extended implementation of the Filter extension point available to the Consumer:

public static void main(String[] args) {
  ExtensionLoader<Filter> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
  URL url = new URL(""."".10086);
  List<Filter> activate = extensionLoader.getActivateExtension(url, "", CommonConstants.CONSUMER);
  activate.forEach(a -> System.out.println(a.getClass().getName()));
}
/ / output
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
Copy the code

You can see that C and D are enabled in the custom extension implementation. A is not enabled by default because there is no @activate annotation. B restricts enabling only on the Provider side. The value attribute of E’s @Activate annotation restricts enabling A parameter named E on the URL.

Next add an argument to try to enable E:

URL url = new URL(""."".10086).addParameter("e", (String) null);
/ / output
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
Copy the code

You can see that E is still not enabled. This is because the URL contains A parameter named E, but the value is null, which does not comply with the enable rule. In this case, you can enable E by setting the value to any value that is not null (that is, not false, 0, null, or N/A).

Enable E in another way:

URL url = new URL(""."".3).addParameter("filterValue"."fe");
List<Filter> activate = extensionLoader.getActivateExtension(url, "filterValue", CommonConstants.CONSUMER);
/ / output
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
// org.apache.dubbo.rpc.demo.FilterE
Copy the code

Add the filterValue parameter and specify the value fe, which must be the same as key in the SPI configuration file. You can see that E is enabled when you specify the name of this parameter when you call the getActivateExtension() method.

Next try removing the default extension implementation and specifying A to enable:

URL url = new URL(""."".3).addParameter("filterValue"."fa,-default");
List<Filter> activate = extensionLoader.getActivateExtension(url, "filterValue", CommonConstants.CONSUMER);
/ / output
// org.apache.dubbo.rpc.demo.FilterA
Copy the code

With -default the ConsumerContextFilter and C and D are disabled because they are the default implementations. Once again, the extension implementations that are enabled by default are those that carry an @Activate annotation but have no value for the annotation’s value. Although A does not carry the @activate annotation, it is specified that it needs to be enabled, so A is enabled.

The last

Ok, finally finished analyzing the SPI mechanism of Dubbo. In fact, it is not too complicated, but there is a logical twist. I will record this article as a video to explain it when I have the opportunity, hoping to make you have a better understanding.