A, SPI

SPI is called Service Provider Interface, corresponding to Service discovery mechanism. SPI is similar to a pluggable mechanism in that an interface or a convention needs to be defined, which can then be implemented in different scenarios without requiring the caller to pay too much attention to the implementation details. In Java, SPI embodies the idea of interface oriented programming and meets the principles of open and closed design.

1.1 JDK comes with SPI implementation

Since THE SPI mechanism was introduced in JDK1.6, you can see many cases of using SPI. For example, the most common database-driven implementation, java.sql.Driver interface is defined in JDK, and the implementation is provided by various database vendors. Here is a simple example to quickly understand how the Java SPI is used:

1) Define an interface

package com.vivo.study
public interface Car {
void getPrice(a);
}
Copy the code

2) Interface implementation

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

3) Mount extended class information

In the meta-INF /services directory, create a file named com.vivo. Study. Car.

com.vivo.study.impl.AudiCar
com.vivo.study.impl.BydCar
Copy the code

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(); }}}Copy the code

The ServiceLoader class is used to load the implementation class of the interface. The ServiceLoader class is the implementation class of the Iterable interface. The details of ServiceLoader loading are not covered here.

The JDK’s implementation of SPI loading has a minor drawback. It cannot load implementation classes on demand. When loaded through serviceloader.load, all implementations in the file are instantiated.

1.2 Dubbo SPI

The SPI extension is one of Dubbo’s biggest strengths, supporting protocol extension, call interception extension, reference listening extension, and more. In Dubbo, the extension files are placed under meta-INF/Dubbo /internal/, meta-INF/Dubbo /, and meta-INF /services/, depending on the extension location.

Is directly used in Dubbo JDK SPI implementation way, such as org.apache.dubbo.com. Mon extension. LoadingStrategy in meta-inf/services/directory, Most of the time, however, this is an optimized way to use its own implementation of the JDK SPI, called the Dubbo SPI, which is the focus of this article.

Compared to the IMPLEMENTATION of THE JDK’s SPI, Dubbo SPI has the following features:

More flexible configuration: You can configure a file in the form of key:value, for example, name: XXX.XXX.xxx. xx. You can use name to obtain extended classes as required.

Use of caching: Use caching to improve performance by ensuring that an extended implementation class is loaded at most once.

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

Dubbo’s loading of extension points is primarily carried out 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 the key class ServiceConfig in the service exposure, make clear the implementation details of ExtensionLoader to read Dubbo source code has a great help.

2.1 Obtaining an instance of ExtensionLoader

ExtensionLoader does not provide a common constructor,

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

public
 
    // ConcurrentHashMap cache,key -> Class Value -> ExtensionLoader instance
    private static finalConcurrentMap<Class<? >, 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 if it is an interface and throw an exception if it is not
        if(! type.isInterface()) {throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
 
        // Check if the interface is decorated with an @spi annotation and throw an exception if it is not
        if(! withExtensionAnnotation(type)) {throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
 
        // fetch one from the cache. If not, initialize one and place it in the cache EXTENSION_LOADERS
        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);
        }
        returnloader; }}Copy the code

The code above shows the process of getting an instance of ExtensionLoader. You can see that each interface decorated by @spi corresponds to the same instance of ExtensionLoader, and the corresponding ExtensionLoader is initialized only once. And cached in ConcurresntHashMap.

2.2 Loading Extended Classes

Load the extension class entry. When using ExtensionLoader, getExtensionName, getActivateExtension, or getDefaultExtension are loaded through the getExtensionClasses method. The following figure;

GetExtensionClasses is a starting point for loading the extension class. It is fetched from the cache first. If it is not in the cache, the loadExtensionClasses method is used to load the extension class. So the actual loading logic entry is in loadExtensionClasses.

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

Copy the code

2.2.1 loadExtensionClasses Loading extension classes

Because the whole loading process design source more, so with a flow chart to describe, specific details can be viewed in combination with the source code.

LoadExtensionClasses do a few things:

Default extension:

Extract the default extension implementation name and cache it in cachedDefaultName in ExtensionLoader. The default extension configuration is configured on the interface via the @spi annotation. If @spi (“defaultName”) is configured, the default extension is defaultName.

Loading extension class information:

META-INF/dubbo/internal/ META-INF/dubbo/ META-INF/services/ META-INF/dubbo/internal/ META-INF/dubbo/ META-INF/services/

Load class and cache:

The extension class is divided into Adaptive extension implementation class (the implementation class modified by @Adaptive), wrapper class (with a constructor that has only one parameter for this interface type), ordinary extension class, which contains automatic activation extension class (the class modified by @Activate) and really ordinary class. The adaptive extension implementation class, wrapper class, and automatic activation extension class were loaded and cached to cachedAdaptiveClass, cachedWrapperClasses, respectively.

Map<String, Class<? > > :

The result is a Map, where the key corresponds to the name configured in the extension file and the value corresponds to the extended class. Finally, the getExtensionClasses method will put the result into the cache cachedClasses. The resulting Map contains mappings between the names of the extension classes used except for the adaptive extension implementation classes and the wrapper implementation classes.

The loadExtensionClasses method is used to loadExtensionClasses (Class objects) into the corresponding cache to facilitate the subsequent instantiation of extension Class objects through newInstance() and other methods.

2.2.2 Extend the wrapper class

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

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

The wrapper extension class acts like AOP, facilitating extension enhancement. The specific implementation code is as follows:

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

The Protocol implementation classes ProtocolFilterWrapper and ProtocolListenerWrapper are extension 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 {};
}
Copy the code

The following points can be drawn from the source code and from 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 injection of extension instances by ExtensionLoader.

Understand the role of value from the comment:

Value can choose to use a specific 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 is selected and configured via @spi.

2.2.3.2@Adaptive Simple example

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

/** * Dubbo SPI interface */
@SPI("impl1")
public interface SimpleExt {
    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);
}
Copy the code

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 to get an extName flow is as follows:

Get from url.getparameters.get (“key1”),

Get (“key2”). If it doesn’t, use the impl1 implementation class. IllegalStateException is thrown.

As you can see, the advantage of @Adaptive is that you can use method inputs to decide which implementation class to call. 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 in yellow, cachedAdaptiveClass is cached when the Extension class is loaded in ExtensionLoader#loadClass.

2) Marked in green, the Extension class will be used to initialize instances if there is a class modified by @Adaptive in the Extension class.

3) In red, if the Extension class does not exist that is modified by @Adaptive, the code needs to be dynamically generated and compiled by Javassist (default) to generate Xxxx$Adaptive class for instantiation.

4) Inject the Adaptive instance’s Extension (property injection) through injectExtension after instantiation.

The follow-up will focus on key point 3 above, but key point 4 is not expanded 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> {
// ...
privateClass<? > getAdaptiveExtensionClass() {// Load and cache the extension class according to the corresponding SPI file. The details are not expanded here
        getExtensionClasses();
// If there is a class modified by @adaptive, return this class directly
if(cachedAdaptiveClass ! =null) {
return cachedAdaptiveClass;
        }
// Dynamically generate Xxxx$Adaptive
return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }
privateClass<? > createAdaptiveExtensionClass() {// Generate Xxxx$Adaptive class code, you can add log or breakpoint to view the generated code
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        ClassLoader classLoader = findClassLoader();
// Get the dynamic compiler, javassist by default
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
returncompiler.compile(code, classLoader); }}Copy the code

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 most critical part is to generate the content of the method modified by @Adaptive, that is, when the @adaptive method of the instance is finally called, the specific extended instance can be dynamically selected through parameters. The following part is analyzed:

public class AdaptiveClassCodeGenerator {
// ...
/** * generate method content */
private String generateMethodContent(Method method) {
// Get @adaptive annotations on methods
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
// Method without @adaptive annotation, generating unsupported code
return generateUnsupported(method);
        } else {
// If the URL is not present, it is -1
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));
            }
// Get the value of the @adaptive configuration on the method
/ / such as @ the Adaptive ({" key1 ", "key2"}), will return a String array {" key1 ", "key2}"
// If @adaptive is not configured with value, it will be humped according to the shorthand interface name. Split, such as SimpleExt for simple.ext
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
/ / parameters in the presence of org. Apache. Dubbo. RPC. Invocation
            boolean hasInvocation = hasInvocationArgument(method);
            code.append(generateInvocationArgumentNullCheck(method));
// Generate String extName = XXX; ExtName is used to get a specific Extension instance
            code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
            code.append(generateExtNameNullCheck(value));
            code.append(generateExtensionAssignment());
// return statement
            code.append(generateReturnAndInvocation(method));
        }
returncode.toString(); }}Copy the code

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 the default implementation configured on the interface @spi


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

The corresponding code for generating extName is:

String extName = url.getParameter("simple.ext"."impl1")
Copy the code

2) The method is decorated by @adaptive, without value configuration, and the default implementation is not configured on the interface @spi

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

The corresponding code for generating extName is:

String extName = url.getParameter( "simple.ext")
Copy the code

3) The method is decorated by @adaptive, configured with value (assuming two, and so on), and configured with a default implementation on the interface @spi

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

The corresponding code for generating extName is:

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

4) The method is modified by @adaptive, with value configured (assuming two, and so on), and no default implementation configured on the interface @spi

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

The corresponding code for generating extName is:

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

See appendix for the complete generated classes.

2.2.4 Automatically activate extended classes

If you’ve ever extended a Filter that implements Dubbo, you’ll be familiar with @Activate. The @activate annotation is used to automatically Activate the extension implementation class with a given condition. The ExtensionLoader#getActivateExtension(URL,String, String) method is used to find a list of extension classes that need to be activated under the specified condition.

When a Consumer calls the Dubbo interface, it passes through the Filter chain of the Consumer and the Filter chain of the Provider. When the Provider exposes the service, it concatenates the Filter to be used.

The location in the source code is in the ProtocolFilterWrapper#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 the service export, it obtains and activates the corresponding filter according to the value of service.filter in the Url and group= Provider
    List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
}
Copy the code

ExtensionLoader#getActivateExtension(URL, String, String) how to automatically activate the corresponding extension class list according to the condition.

Third, summary

This article mainly summarizes the extended class loading process of Dubbo SPI mechanism through ExtensionLoader class source code, which can be summarized as follows:

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

META-INF/dubbo/internal/ META-INF/dubbo/ META-INF/services/ META-INF/dubbo/internal/ META-INF/dubbo/ META-INF/services/ And the class is cached in an ExtensionLoader instance.

3. The extension wrapper class and its implementation process are introduced. The extension wrapper class realizes functions similar to AOP.

4. Adaptive extension class, analyze the process of dynamically generating Xxx$Adaptive class in @adabound modification method, and introduce the case of realizing class to complete method call through parameter Adaptive selection extension.

A brief introduction to the automatic activation extension class and the functions of @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);
}
Copy the code

Generated Adaptive class code

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!"); }}Copy the code

Ning Peng, Vivo Internet Server Team