5

One, SPI

The full name of SPI is Service Provider Interface, and the corresponding Chinese is the service discovery mechanism. SPI is similar to a pluggable mechanism. First, an interface or a convention needs to be defined, and then different scenarios can be implemented. The caller does not need to pay too much attention to specific implementation details when using it. In Java, SPI embodies the idea of interface-oriented programming and satisfies the principle of opening and closing design.

1.1 JDK comes with SPI implementation

After introducing the SPI mechanism from JDK1.6, you can see many cases of using SPI, such as the most common database driver implementation. Only the java.sql.Driver interface is defined in the JDK, and the specific implementation is provided by the database vendors. Here is a simple example to quickly understand how to use Java SPI:

1) Define an interface

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

2) Interface implementation

package com.vivo.study.impl
 
/**
 * 实现一
 *
 */
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 BydCar implements Car {
    @Override
    public void getPrice() {
        System.out.println("BYD han's price is 220000 RMB.");
    }
}

3) Mount extension information

A text file with the full name of the interface in the META-INF/services directory. Correspondingly, create a file named com.vivo.study.Car in the META-INF/services directory. The content of the file is 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 briefly introduces the use of the JDK SPI mechanism. The most critical class is ServiceLoader. The implementation class of the interface is loaded through the ServiceLoader class. ServiceLoader is the implementation class of the Iterable interface. The detailed process of loading the ServiceLoader is here Does not expand.

JDK has a relatively prominent shortcoming in the loading implementation of SPI. The implementation class cannot be loaded on demand. When loading through ServiceLoader.load, all implementations in the file will be instantiated. If you want to get a specific one The implementation class needs to be traversed to judge.

1.2 Dubbo SPI

SPI extension is one of Dubbo's biggest advantages, supporting protocol extension, call interception extension, reference monitoring extension, etc. In Dubbo, according to different extension locations, the extension files are placed in the three paths of META-INF/dubbo/internal/, META-INF/dubbo/, META-INF/services/.

There is a way to directly use JDK SPI in Dubbo, for example, org.apache.dubbo.common.extension.LoadingStrategy is placed in the META-INF/services/ path, but in most cases it uses its own implementation of JDK SPI. This optimization method can be called Dubbo SPI, which is the point to be explained in this article.

Compared with the implementation of JDK's SPI, Dubbo SPI has the following characteristics:

The configuration form is more flexible: support the configuration of similar name: xxx.xxx.xxx.xx in the file in the form of key: value, and then you can use the name to accurately obtain the extended class on demand.

Use of cache: Use cache to improve performance and ensure that an extended implementation class will be loaded at most once.

Subdivision and extension of extensions: support for automatic wrapping of extension points (Wrapper), automatic assembly of extension points, adaptation of extension points (@Adaptive), and automatic activation of extension points (@Activate).

Dubbo's loading of extension points is mainly carried out by the ExtensionLoader class.

Two, load-ExtensionLoader

The role of ExtensionLoader in Dubbo is similar to that of ServiceLoader in JDK, which is used to load extension classes. In the Dubbo source code, you can see ExtensionLoader everywhere, such as the key class ServiceConfig in the service exposure. It is very helpful to understand the implementation details of ExtensionLoader to read the Dubbo source code.

2.1 Obtain an instance of ExtensionLoader

ExtensionLoader does not provide a shared constructor,

The ExtensionLoader instance can only be obtained through ExtensionLoader.getExtensionLoader(Class<T> type).

public
 
    // ConcurrentHashMap缓存,key -> Class value -> ExtensionLoader实例
    private static final ConcurrentMap<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");
        }
        // 检查是否是接口,如果不是则抛出异常
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
 
        // 检查接口是否是被@SPI注解修饰,如果没有则抛出异常
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
 
        // 从缓存里取,没有则初始化一个并放入缓存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);
        }
        return loader;
    }
}

The above code shows the process of obtaining an ExtensionLoader instance. It can be seen that each interface modified by @SPI corresponds to the same ExtensionLoader instance, and the corresponding ExtensionLoader will only be initialized once and cached in ConcurresntHashMap.

2.2 Load extension classes

Load the extension class entry. When using ExtensionLoader, getExtensionName, getActivateExtension or getDefaultExtension must go through the getExtensionClasses method to load the extension class, as shown in the figure below;

The call path of the getExtensionClasses method is as shown in the figure below. getExtensionClasses is a starting point for loading extension classes. It will first be obtained from the cache. If there is no cache, the extension classes will be loaded through the loadExtensionClasses method. So the actual loading logic entry is in loadExtensionClasses.

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

2.2.1 loadExtensionClasses load extension classes

Since there are many source codes designed for the entire loading process, a flowchart is used to describe it, and the specific details can be viewed in conjunction with the source code.

loadExtensionClasses mainly does the following things:

default extension:

Extract the default extension implementation name and cache it in the cachedDefaultName in ExtensionLoader. The default extension configuration is configured on the interface through the @SPI annotation. If @SPI("defaultName") is configured, the default extension is defaultName.

Load extended class information:

Find the file named with the full path name of the class from the three paths of META-INF/dubbo/internal/, META-INF/dubbo/, META-INF/services/, and read the contents of the file line by line.

load class and cache:

The extension classes are divided into adaptive extension implementation classes (implementation classes modified by @Adaptive), packaging classes (with a construction method with only one parameter for this interface type), and ordinary extension classes. The ordinary extension classes include automatic Activated extension classes (classes modified by @Activate) and really ordinary classes are respectively loaded and cached to cachedAdaptiveClass, cachedActivates, and cachedWrapperClasses for the three types of adaptive extension implementation class, packaging class, and automatic activation extension class.

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

The result is returned to the Map, where the key corresponds to the name configured in the extension file, and the value corresponds to the extended class class. Finally, the result will be put into the cachedClasses in the getExtensionClasses method. This result Map contains the mapping relationship between the extension class name and the corresponding class in addition to the adaptive extension implementation class and the packaging implementation class.

All extension classes (Class objects) are loaded into the corresponding cache through the loadExtensionClasses method, in order to facilitate the instantiation of the extension class objects later, and instantiate them through methods such as newInstance().

2.2.2 Extended packaging

What is an extended packaging class? Is the class with Wrapper at the end of the class name an extended wrapper class?

In the implementation extension class of the Dubbo SPI interface, if the class contains a construction method with this interface as a parameter, it is an extension wrapper class. The role of the extended packaging class is to hold a specific extended implementation class, which can be packaged layer by layer, similar to AOP.

The function of packaging extension classes is similar to AOP, which facilitates extension and enhancement. The specific implementation code is as follows:

It can be concluded from the code that you can choose whether to use the wrapper class through boolean wrap, which is true by default; if there is an extended wrapper class, the actual implementation class will be wrapped layer by layer by the wrapper class in a certain order.

For example, the implementation classes of Protocol ProtocolFilterWrapper and ProtocolListenerWrapper are all extended wrapper classes.

2.2.3 Adaptive extension implementation class

2.2.3.1 @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 source code comments:

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

The function of this annotation is to provide useful information for the ExtensionLoader to inject extension instances.

Understand the role of value from the comments:

The value can decide to choose to use a specific extension class.

Through the value key configured by value, the corresponding value value is obtained through the key in the input parameter org.apache.dubbo.common.URL of the modified method, and the corresponding extension class is determined to be used according to the value as the extensionName.

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

2.2.3.2 @Adaptive simple example

Since @Adaptive modifies a class is easier to understand, here is an example of @Adaptive modification method. The use of @Adaptive modification method is also seen everywhere in Dubbo.

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

If you call

ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension().yell(url, s) method, the final instance of which extension class is called to execute the yell method is roughly as follows: first obtain the name of the extension class extName (corresponding to the above name: name in class), and then obtain the corresponding class Class through extName, and then instantiate it to call. So the key step is how to get extName. The process of getting extName in the above example is:

Get through url.getParameters.get("key1"),

If it is not obtained, use url.getParameters.get("key2"). If it is still not obtained, use the implementation class corresponding to impl1. Finally, if it is still not obtained, an exception IllegalStateException will be thrown.

It can be seen that the advantage of @Adaptive is that you can determine which implementation class to call specifically through method input. The specific implementation of @Adaptive will be analyzed in detail below.

2.2.3.3 @Adaptive loading process

Description of key points of the process:

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

2) Green marked, if there is a class modified by @Adaptive in the Extension class, this class will be used to initialize the instance.

3) For the ones marked in red, if there is no class modified by @Adaptive in the Extension class, the code needs to be dynamically generated, and the Xxxx$Adaptive class is instantiated by compiling and generating the Xxxx$Adaptive class through javassist (default).

4) After instantiation, the Extension of the Adaptive instance is injected (attribute injection) through injectExtension.

The follow-up will focus on the key point 3 mentioned above in detail, and the key point 4 will not be expanded here.

dynamically generate Xxx$Adaptive class : The following code is the relevant code for dynamically generated Adaptive class. The details of the specific generated code are in AdaptiveClassCodeGenerator#generate

public class ExtensionLoader<T> {
// ...
private Class<?> getAdaptiveExtensionClass() {
// 根据对应的SPI文件加载扩展类并缓存,细节此处不展开
        getExtensionClasses();
// 如果存在被@Adaptive修饰的类则直接返回此类
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
        }
// 动态生成Xxxx$Adaptive类
return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }
private Class<?> createAdaptiveExtensionClass() {
// 生成Xxxx$Adaptive类代码,可自行加日志或断点查看生成的代码
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        ClassLoader classLoader = findClassLoader();
// 获取动态编译器,默认为javassist
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
    }
}

AdaptiveClassCodeGenerator#generate generates code by concatenating strings and using String.format extensively. The entire code process is cumbersome. You can use debug to understand the 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 extension instance can be dynamically selected through the parameters. The analysis of this part is as follows:

public class AdaptiveClassCodeGenerator {
// ...
/**
     * generate method content
     */
private String generateMethodContent(Method method) {
// 获取方法上的@Adaptive注解
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
// 方法时没有@Adaptive注解,生成不支持的代码
return generateUnsupported(method);
        } else {
// 方法参数里URL是第几个参数,不存在则为-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));
            }
// 获取方法上@Adaptive配置的value
// 比如 @Adaptive({"key1","key2"}),则会返回String数组{"key1","key2"}
// 如果@Adaptive没有配置value,则会根据简写接口名按驼峰用.分割,比如SimpleExt对应simple.ext
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
// 参数里是否存在org.apache.dubbo.rpc.Invocation
            boolean hasInvocation = hasInvocationArgument(method);
            code.append(generateInvocationArgumentNullCheck(method));
// 生成String extName = xxx;的代码 ,extName用于获取具体的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 most critical step in the content of the above method to generate the Adaptive class is to generate the extName part, which is generateExtNameAssignment(value, hasInvocation). There are too many ifs in this method (a bit dazzling).

Here are a few examples to briefly demonstrate the implementation process of this method: Assuming that the parameters in the method do not include org.apache.dubbo.rpc.Invocation, the situation that includes org.apache.dubbo.rpc.Invocation will be more complicated.

1) The method is modified by @Adaptive, the value is not configured, and the default implementation is configured on the interface @SPI


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

The corresponding code to generate extName is:

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

2) The method is modified by @Adaptive, the value is not configured, 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);
}

The corresponding code to generate extName is:

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

3) The method is modified by @Adaptive, the value is configured (assuming two, and so on), and the default implementation is configured on the interface @SPI

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

The corresponding code to generate extName is:

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

4) The method is modified by @Adaptive, the value is configured (assuming two, and so on), and the default implementation is not configured in the interface @SPI

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

The corresponding code to generate extName is:

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

The complete generation class can be found in the appendix.

2.2.4 Automatic activation of extensions

If you have extended to implement Dubbo's Filter, you will be familiar with @Activate. The function of @Activate annotation is to automatically activate the extension implementation class through the given conditions, and the list of extension classes that need to be activated under the specified conditions can be found through the ExtensionLoader#getActivateExtension(URL,String, String) method.

The following is an example to illustrate the role of @Activate. When Consumer calls the Dubbo interface, it will pass through the consumer's filter chain and the provider's filter chain. In the process of Provider exposing services, it will splice which filters need to be used.

The position in the corresponding 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) {
    // 在Provider暴露服务服务export时,会根据获取Url中的service.filter对应的值和group=provider来获取激活对应的Filter
    List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
}

ExtensionLoader#getActivateExtension(URL, String, String) is how to automatically activate the corresponding extension class list according to the conditions. You can check the code of this method by yourself. It will not be expanded here.

Three, summary

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

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

2. Analyze the extension load process of ExtensionLoader, load the files under the three paths of META-INF/dubbo/internal/, META-INF/dubbo/, META-INF/services/, and classify and cache them in the ExtensionLoader instance.

3. Introduce the extended packaging class and its implementation process. The extended packaging class implements functions similar to AOP.

4. Adaptive extension class, analyze the process of dynamically generating Xxx$Adaptive class when @Adptive modifies the method, and introduce the case of completing method call through parameter adaptive selection extension implementation class.

A brief introduction to the automatic activation extension class and the role of @Activate.

Four, appendix

4.1 Xxx$Adaptive complete case

@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 generated class code Adaptive

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.ext1.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: vivo internet server team-Ning Peng

vivo互联网技术
3.3k 声望10.2k 粉丝