A, SPI

The SPI is called Service Provider Interface, which corresponds to the Service discovery mechanism. SPI is similar to a pluggable mechanism in that an interface or a convention needs to be defined, and then different scenarios can implement it, and callers can use it without paying much attention to the implementation details. In Java, SPI embodies the idea of interface-oriented programming and meets the open closed design principle.

1.1 The SPI implementation comes with the JDK

Since the introduction of SPI mechanism in JDK1.6, we can see many cases of using SPI. For example, the most common database Driver implementation, the JDK only defines the interface of java.sql.Driver, and the specific implementation is provided by each database vendor. Here’s a quick example of how Java SPI can be used:

1) Define an interface

package com.vivo.study
public interface Car {
void getPrice();
}

2) Interface implementation

Package com.vivo.study.impl /** * implements a ** / public class AudiCar implements Car {@override public void getPrice() { System.out.println("Audi A6L's price is 500000 RMB."); }} package com.vivo.study.impl /** ** */ public class implements Car {@override public void getPrice() { System.out.println("BYD han's price is 220000 RMB."); }}

3) Mount the extended class information

In the meta-INF /services directory, create a text file named com.vivo. Study. Car, and the contents of the file are as follows:

com.vivo.study.impl.AudiCar
com.vivo.study.impl.BydCar

4) use

public class SpiDemo { public static void main(String[] args) { ServiceLoader<Car> load = ServiceLoader.load(Car.class);  Iterator<Car> iterator = load.iterator(); while (iterator.hasNext()) { Car car = iterator.next(); car.getPrice(); }}}

The above example simply introduces the use of JDK SPI mechanism, the most critical class is ServiceLoader, through the ServiceLoader class to load the interface implementation class, ServiceLoader is the implementation class of Iterable interface, The detailed procedure for loading ServiceLoader is not expanded here.

The LOADING implementation of JDK to SPI has a prominent shortcoming, that is, it cannot load the implementation class on demand. When loading through Serviceloader.load, all the implementations in the file will be instantiated. If you want to obtain a specific implementation class, you need to make traversal judgment.

1.2 Dubbo SPI

SPI extensions are one of the biggest benefits of Dubbo, supporting protocol extensions, call interception extensions, reference listening extensions, and so on. In Dubbo, the extension files are placed under meta-INF/Dubbo /internal/, meta-INF/Dubbo /, and Meta-INF /services/, depending on the location of the extension.

Is directly used in Dubbo JDK SPI implementation way, such as org.apache.dubbo.com. Mon extension. LoadingStrategy in meta-inf/services/directory, But mostly it’s an optimization using its own implementation of the JDK SPI, called Dubbo SPI, which is the point of this article.

Compared to the JDK SPI implementation, Dubbo SPI has the following features:

The configuration format is flexible: You can configure a file in the form of key:value, such as name: XXX.XXX.xxx. xx. You can use the name to accurately obtain the extension class as required.

Use of caching: Use caching to improve performance and ensure that an extension implementation class is loaded at most once.

Subdivide extension for extension class: support extension point automatic Wrapper (Wrapper), extension point automatic assembly (assembly), extension point Adaptive (@adaptive), extension point automatic activation (@activate).

Dubbo’s loading of extension points is mainly expanded by the ExtensionLoader class.

Load -ExtensionLoader

The role of ExtensionLoader in Dubbo is similar to that of ServiceLoader in the JDK for loading extended classes. In Dubbo source code, ExtensionLoader can be seen everywhere, such as in the service exposure key class ServiceConfig, it is very helpful to understand the implementation details of ExtensionLoader to read Dubbo source code.

2.1 Obtaining an Instance of ExtensionLoader

ExtensionLoader does not provide a common constructor,

Only through ExtensionLoader. GetExtensionLoader (Class < T > type) to obtain ExtensionLoader instance.

Public // ConcurrentHashMap cache,key -> Class value -> ExtensionLoader instance >, ExtensionLoader<? >> EXTENSION_LOADERS = new ConcurrentHashMap<>(64); private ExtensionLoader(Class<? > type) { this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); } public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { if (type == null) { throw new IllegalArgumentException("Extension type == null"); } // Check whether it is an interface and throw an exception if it is not. type.isInterface()) { throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!" ); } // Check whether the @SPI annotation is modified, and throw an exception if not. withExtensionAnnotation(type)) { throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!" ); } // Fetch from cache, 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; }}

The above code shows how to obtain an instance of ExtensionLoader. As you can see, every @SPI modified interface corresponds to the same instance of ExtensionLoader, and the corresponding ExtensionLoader is initialized only once. And cached in ConcurresntHashMap.

2.2 Loading an Extension Class

When using the ExtensionLoader, getExtensionName, getActivateExtension, or getDefaultexVector will go through the getExtensionClasses method to load the extension class. The following figure;

The path to getExtensionClasses is shown below. GetExtensionClasses is a starting point for loading the extended class. If it is not in the cache, loadExtensionClasses is used to load the extended class. So the actual loading logic entry is in loadExtensionClasses.

getExtensionClasses
  |->loadExtensionClasses
    |->cacheDefaultExtensionName
    |->loadDirectory
      |->loadResource
        |->loadClass

2.2.1 loadExtensionClasses Loads extended classes

Because the whole loading process design of the source code is more, so use a flowchart to describe, specific details can be combined with the source code to view.

LoadExtensionClasses does a few things:

Default extension:

The default extension implementation name is extracted and cached in cachedDefaultName in the ExtensionLoader. The default extension configuration is configured on the interface through the @SPI annotation. If @SPI(“defaultName”) is configured, the default extension name is defaultName.

Load extension class information:

META-INF/dubbo/internal/,META-INF/ dubbo/,META-INF/services/ and META-INF/services/ find the file with the full pathname of the class and read the contents line by line.

Load class and cache:

There are Adaptive extension implementation classes (implementation classes that are modified by @Adaptive), wrapper classes (which have a constructor with only one parameter for the interface type), normal extension classes, which in turn contain auto-activated extension classes (classes that are modified by @Activate), and true normal classes. The adaptive extension implementation class, wrapper class, and auto-activated extension class are loaded and cached to cachedAdaptiveClass, cachedAPperClasses, and cachedWrapperClasses, respectively.

Return the result Map<String, Class<? > > :

The result is a Map where the key corresponds to the name configured in the extension file, the value corresponds to the extended class, and the getExtensionClasses method places the result in the cached cachedClasses. The resulting Map contains mapping relationships between the extension class names used for all but the adaptive extension implementation class and the wrapper implementation class.

The loadExtensionClasses method is used to load the extended Class (Class objects) into the corresponding cache to make it easier to instantiate the extended Class objects later, using methods such as newInstance().

2.2.2 Extending the wrapper class

What is an extension wrapper class? Is a class with a Wrapper at the end of its class name an extension Wrapper class?

In the implementation extension class of the Dubbo SPI interface, the class is an extension wrapper class if it contains a constructor that takes this interface as a parameter. The purpose of an extension wrapper class is to hold a specific extension implementation class that can be wrapped layer by layer, similar to AOP.

Wrapper extension classes function like AOP to facilitate extension enhancements. The specific implementation code is as follows:

As you can see from the code, you can select whether to use a wrapper class with Boolean wrap, which is true by default; If there is an extension wrapper class, the actual implementation class is wrapped by the wrapper class layer by layer in a certain order.

For example, Protocol implementations ProtocolFilterWrapper and ProtocolListenerWrapper are extended wrapper classes.

2.2.3 Adaptive extension implementation class

2.2.3.1 @ the Adaptive

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
String[] value() default {};
}

The following points can be drawn from the source code and the source code comments:

Adaptive is an annotation that modifies classes (interfaces, enumerations) and methods.

The purpose of this annotation is to provide useful information for the ExtensionLoader to inject the extension instance.

Understand what value does from comments:

Value can choose to use a concrete extension class.

Through the key value configuration values, in the modified method into the org.apache.dubbo.com mon refs. Through key access to the corresponding values in the URL value, according to the value as an extension of the extensionName to decide to use the corresponding class.

If no corresponding extension is found through 2, the default extension class will be selected and configured via @SPI.

2.2.3.2@adaptive Simple example

Since @adaptive modifiers are easy to understand, here is an example of @adaptive modifiers, which can be seen everywhere in Dubbo.

/** * Dubbo SPI interface */ @spi ("impl1") public interface SimpleExt {@adaptive ({"key1", "key2"}) String yell(URL) String s); }

If the call

ExtensionLoader. GetExtensionLoader (SimpleExt. Class). GetAdaptiveExtension (). Yell (url, s) method, which eventually call extension class instance to execute yell method process roughly: ExtName = extName; extName = extName; extName = extName; extName = extName; So the key step is how to get an extName. The above example gets an extName as follows:

Getparameters.get (“key1”)

If not, use URl.getParameters.get (“key2”). If not, use impl1’s implementation class. If not, throw an IllegalStateException.

As you can see, the advantage of @Adaptive is that you can use the method parameters to determine which implementation class is called. The specific implementation of @adaptive will be analyzed in detail below.

2.2.3.3@adaptive Loading process

Key points of the process:

1) marked yellow, cachedAdaptiveClass is cached when the Extension class is loaded in the ExtensionLoader#loadClass method.

2) Labeled in green, this class will be used to initialize the instance if there is a @adaptive modified class in the Extension class.

3) Marked in red, if there is no @adaptive modified class in the Extension class, the code needs to be dynamically generated, which is compiled by JavAssist (default) to generate Xxxx$Adaptive class for instantiation.

4) Inject Adaptive instance Extension (attribute injection) by injectExtension after instantiation.

The following chapters will focus on the above mentioned key point 3 in detail. Key point 4 will not be discussed here.

Dynamically generated $Xxx the Adaptive class: the following code to dynamically generate the Adaptive class related code, the specific details of the generated code in AdaptiveClassCodeGenerator# generate

public class ExtensionLoader<T> { // ... private Class<? > getAdaptiveExtensionClass () {/ / according to the corresponding SPI file extension class loading and caching, details here not spread getExtensionClasses (); // Return class if (cachedAdaptiveClass! = null) { return cachedAdaptiveClass; } / / dynamically generated Xxxx $the Adaptive return cachedAdaptiveClass = createAdaptiveExtensionClass (); } private Class<? > createAdaptiveExtensionClass () {/ / generated Xxxx $the Adaptive type of code, Can add log or breakpoint view generated code String code = new AdaptiveClassCodeGenerator (type, cachedDefaultName). The generate (); ClassLoader classLoader = findClassLoader(); // Get the dynamic compiler, The default for javassist org.apache.dubbo.common.compiler.Com piler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); return compiler.compile(code, classLoader); }}

AdaptiveClassCodeGenerator# generate generated code way is through the String concatenation, extensive use of the String. Format, the whole process more complicated code, through the debug to know details.

The key part is to generate the contents of the @adaptive modified method, so that when the @adaptive method of the instance is finally called, the parameters can be used to dynamically select which extension instance to use. This section is analyzed as follows:

public class AdaptiveClassCodeGenerator { // ... /** * generate method content */ private String generateMethodContent(method method) adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); If (adaptiveAnnotation == null) {return generateUnsupported(method); Int urlTypeIndex = getUrlTypeIndex(method); int urlTypeIndex = getUrlTypeIndex(method); // found parameter in URL type if (urlTypeIndex ! = -1) { // Null Point check code.append(generateUrlNullCheck(urlTypeIndex)); } else { // did not find parameter in URL type code.append(generateUrlAssignmentIndirectly(method)); } // Obtain the value configured on @adaptive // for example @adaptive ({"key1","key2"}), then the String array {"key1","key2"} // If @adaptive is not configured with a value, The abbreviated interface name is used according to the hump. Split, e.g. SimpleExt versus simple.ext String[] value = getMethodAdaptiveValue(adaptiveAnnotation); / / parameters in the presence of org. Apache. Dubbo. RPC. The Invocation Boolean hasInvocation = hasInvocationArgument (method); code.append(generateInvocationArgumentNullCheck(method)); // Generate String extName = XXX; Code, extName for specific instances of the Extension code. Append (generateExtNameAssignment (value, hasInvocation)); // check extName == null? code.append(generateExtNameNullCheck(value)); code.append(generateExtensionAssignment()); // return statement code.append(generateReturnAndInvocation(method)); } return code.toString(); }}

The generated the most critical step in the Adaptive method content of a class in the generated extName part, namely generateExtNameAssignment (value, hasInvocation), this method if too much (a bit dazzled).

Following are a few examples show this method is simple in implementation process: hypothesis method of parameter does not contain org. Apache. Dubbo. RPC. The Invocation, including org. Apache. Dubbo. RPC. The Invocation of the situation will be more complex.

1) The method is modified by @adaptive, with no value configured and a default implementation configured on the @SPI interface


@SPI("impl1")
public interface SimpleExt {
@Adaptive
String echo(URL url, String s);
}

The corresponding code for generating extName is:

String extName = url.getParameter("simple.ext", "impl1")

2) The method is modified by @adaptive, with no value configured and no default implementation configured on the @SPI interface

@SPI("impl1")
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

The corresponding code for generating extName is:

String extName = url.getParameter( "simple.ext")

3) The method is modified by @adaptive, with values configured (suppose two, and so on) and a default implementation configured on the @SPI interface

@SPI
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

The corresponding code for generating extName is:

String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));

4) The method is modified by @adaptive, with values configured (suppose two, and so on) and no default implementation configured on the @SPI interface

@SPI
public interface SimpleExt {
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
}

The corresponding code for generating extName is:

String extName = url.getParameter("key1", url.getParameter("key2"));

The complete generation class is in the appendix.

2.2.4 Automatically activating extension classes

If you’ve implemented Dubbo’s Filter extension, you’ll be familiar with @activate. The @activate annotation automatically activates an extension implementation class based on a given condition. The ExtensionLoader#getActivateExtension(URL,String, String) method can be used to find the list of extension classes that need to be activated under the specified condition.

The following is an example of @activate’s effect. When a Consumer invokes the Dubbo interface, it passes through the Consumer’s Filter chain and the Provider’s Filter chain. The Provider will use the Filter to expose the service.

The corresponding source location is in the ProtocolFilterWraper #buildInvokerChain(Invoker, key, Group) method.

// export:key-> service.filter ; group-> provider private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {// When the Provider exposes service export, The filter List< filter > filters = is obtained based on the value of service.filter in the Url and group=provider ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group); }

ExtensionLoader#getActivateExtension(URL, String, String) specifies how the extension (URL, String, String) automatically activates the corresponding extension class list based on the condition.

Third, summary

In this paper, the extended class loading process of Dubbo SPI mechanism is summarized by ExtensionLoader class source code, which can be summarized as follows:

1.Dubbo SPI combines the implementation of JDK SPI and is optimized on this basis, such as accurate on-demand loading of extended classes and caching to improve performance.

META-INF/dubbo/internal/,META-INF/ dubbo/,META-INF/services/ And cache the ExtensionLoader instance.

3. Introduce the extension wrapper class and its implementation process. The extension wrapper class implements functions similar to AOP.

4. Adaptive extension class. Analyze the process of dynamically generating Xxx$Adaptive class when @adptive modifiers are used, and introduce the case of method invocation through Adaptive parameter selection of extended implementation class.

This section describes how to automatically Activate the extension class and @activate.

Fourth, the appendix

4.1 Complete case of Xxx$Adaptive

@SPI interface definition

@SPI("impl1")
public interface SimpleExt {
    // @Adaptive example, do not specify a explicit key.
    @Adaptive
    String echo(URL url, String s);
 
    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);
 
    // no @Adaptive
    String bang(URL url, int i);
}

The Adaptive class code generated

package org.apache.dubbo.common.extension.ext1; import org.apache.dubbo.common.extension.ExtensionLoader; public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt { public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("key1", url.getParameter("key2", "impl1")); if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])"); org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.e xt1.SimpleExt.class).getExtension(extName); return extension.yell(arg0, arg1); } public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("simple.ext", "impl1"); if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])"); org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName); return extension.echo(arg0, arg1); } public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) { throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!" ); }}

Author: Ning Peng, Vivo Internet Server Team