Dubbo SPI extension mechanism

introduction

SPI is called Service Provider Interface, which is a service discovery mechanism. The essence of SPI is to configure the fully qualified name of the interface implementation class in a file, and the service loader reads the configuration file and loads the implementation class. This can dynamically replace the implementation class for the interface at runtime. Because of this feature, we can easily provide extended functions for our programs through the SPI mechanism.

Before talking about the SPI extension mechanism of dubbo, we need to understand the native SPI mechanism of java to help us better understand the SPI of dubbo.

java primitive SPI

First example:

1. Define the interface Animal:

public interface Animal {
 void run();
}

2. Write 2 implementation classes, Cat and Dog

public class Cat implements Animal{
 @Override
 public void run() {
      System.out.println("小猫步走起来~");
   }
}
public class Dog implements Animal {
 @Override
 public void run() {
      System.out.println("小狗飞奔~");
   }
}

3. Next, create a file in the META-INF/services folder, the name is Animal's fully qualified name com.sunnick.animal.Animal, and the content of the file is the fully qualified class name of the implementation class, as follows:

com.sunnick.animal.impl.Dog
com.sunnick.animal.impl.Cat

4. Write method to test

public static void main(String[] s){
   System.out.println("======this is SPI======");
   ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);  
       Iterator<Animal> animals = serviceLoader.iterator();  
 while (animals.hasNext()) {  
           animals.next().run();
       }
}

The directory structure is as follows:

The test results are as follows:

======this is SPI======
小狗飞奔~
小猫步走起来~

It can be seen from the test results that our two implementation classes were successfully loaded and the corresponding content was output. But we did not show the type of the specified Animal in the code. This is where the native SPI mechanism of java is at work.

The SPI mechanism is as follows:

SPI is actually a dynamic loading mechanism implemented by "interface + strategy mode + configuration file". In system design, modules are usually based on interface programming, and the specified implementation classes are not directly displayed. Once the implementation class is specified in the code, it cannot be replaced with another implementation without modifying the code. In order to achieve the effect of dynamic pluggability, java provides SPI to realize service discovery.

In the above example, the implementation class of Animal is dynamically loaded through the ServiceLoader.load(Animal.class) method. By tracking the source code of the method, it is found that the program will read the configuration file named class name in the META-INF/services directory (For example, the META-INF/services/com.sunnick.animal.Animal file in the above example), as follows, where the PREFIX constant value is "META-INF/services/":

try {
    String fullName = PREFIX + service.getName();
 if (loader == null)
 configs = ClassLoader.getSystemResources(fullName);
 else
 configs = loader.getResources(fullName);
} catch (IOException x) {
 fail(service, "Error locating configuration files", x);
}

Then the class object is loaded by reflection Class.forName(), and the class is instantiated with the instance() method, thus completing the service discovery.

String cn = nextName;
nextName = null;
Class<?> c = null;
try {
    c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
 fail(service,
 "Provider " + cn + " not found");
}

Many commonly used frameworks use SPI mechanisms, such as slf log facade and log4j, logback and other log implementations, jdbc's java, sql.Driver interfaces, and various database connector implementations.

dubbo's SPI usage

Dubbo did not use Java SPI, but re-implemented a more powerful SPI mechanism. The related logic of Dubbo SPI is encapsulated in the ExtensionLoader class. Through the ExtensionLoader, we can load the specified implementation class. The configuration files required by Dubbo SPI need to be placed in the META-INF/dubbo path, and the configuration content is as follows:

dog=com.sunnick.animal.impl.Dog
cat=com.sunnick.animal.impl.Cat

Different from Java SPI implementation class configuration, Dubbo SPI is configured through key-value pairs, so that the specified implementation class can be loaded on demand. In addition, when using Dubbo SPI, you need to mark the @SPI annotation on the Animal interface, and the Cat and Dog classes remain unchanged. Let's demonstrate the usage of Dubbo SPI:

@SPI
public interface Animal {
 void run();
}

Write test method:

public void testDubboSPI(){
   System.out.println("======dubbo SPI======");
   ExtensionLoader<Animal> extensionLoader =
         ExtensionLoader.getExtensionLoader(Animal.class);
   Animal cat = extensionLoader.getExtension("cat");
   cat.run();
   Animal dog = extensionLoader.getExtension("dog");
   dog.run();
}

The test results are as follows:

======dubbo SPI======
小猫步走起来~
小狗飞奔~

SPI source code analysis of dubbo

Dubbo obtains the instance through the ExtensionLoader. getExtensionLoader( Animal. class ). getExtension( "cat" ) method. In this method, the instance will be obtained from the cache list first, and if it is missed, the instance will be created:

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 获取默认的拓展实现类
        return getDefaultExtension();
    }
    // Holder,顾名思义,用于持有目标对象
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // 双重检查
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 创建拓展实例
                instance = createExtension(name);
                // 设置实例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

The process of creating an instance is as follows, namely the createExtension() method:

private T createExtension(String name) {
    // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 通过反射创建实例
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        //此处省略一些源码......
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

Get all the SPI configuration files and parse the key-value pairs in the configuration file. The source code of getExtensionClasses() is as follows:

private Map<String, Class<?>> getExtensionClasses() {
    // 从缓存中获取已加载的拓展类
    Map<String, Class<?>> classes = cachedClasses.get();
    // 双重检查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加载拓展类
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

The cache is also checked first. If the cache misses, the lock is locked through synchronized. After locking, check the cache again and judge it to be empty. At this time, if classes are still null, load extension classes through loadExtensionClasses. The logic of the loadExtensionClasses method is analyzed below.

 private Map<String, Class<?>> loadExtensionClasses() {
    // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的,即示例中的Animal
    SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
    if(defaultAnnotation != null) {
        String extensionClasses = defaultAnnotation.value();
        if(extensionClasses != null && (extensionClasses = extensionClasses.trim()).length() > 0) {
	  // 对 SPI 注解内容进行切分
            String[] names = NAME_SEPARATOR.split(extensionClasses);
	  // 检测 SPI 注解内容是否合法,不合法则抛出异常
            if(names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names));
            }

            if(names.length == 1) {
                this.cachedDefaultName = names[0];
            }
        }
    }
    HashMap extensionClasses1 = new HashMap();
    // 加载指定文件夹下的配置文件
    this.loadFile(extensionClasses1, "META-INF/dubbo/internal/");
    this.loadFile(extensionClasses1, "META-INF/dubbo/");
    this.loadFile(extensionClasses1, "META-INF/services/");
    return extensionClasses1;
}

It can be seen that the loadFile method is finally called. This method is to read the specified file name from the specified directory, parse the content, and put the key-value pair into the map. The process is not repeated.

The above is the process of dubbo's SPI loading instance.

Comparison of Dubbo SPI and native SPI

Java native SPI has the following disadvantages:

  • Need to traverse all implementations and instantiate them, it is impossible to load only a specified implementation class, and the loading mechanism is not flexible enough;
  • The implementation classes are not named in the configuration file, and they cannot be accurately referenced in the program;
  • No cache is used, it needs to be reloaded every time the load method is called

If you want to use Dubbo SPI, the interface must be marked with @SPI annotation. In contrast, Dubbo SPI has the following improvements:

  • The configuration file is changed to the form of key-value pairs, which can obtain any implementation class without loading all the implementation classes, saving resources;
  • Increase the cache to store the instance, improve the read performance;

In addition, dubbo SPI also provides a way to specify default values ​​(for example, the default implementation class of Animal can be specified as Cat by @SPI ("cat")). At the same time, dubbo SPI also provides support for advanced functions such as IOC and AOP to achieve more types of expansion.

to sum up

SPI is a service discovery mechanism that provides the ability to dynamically discover implementation classes and embodies the idea of ​​hierarchical decoupling.

In the process of architecture design and code writing, the modules should be programmed for the interface, avoid directly referencing specific implementation classes, and achieve pluggable effects.

Dubbo provides an enhanced version of the SPI mechanism. During use, you need to add @SPI annotations on the interface to take effect.

Guess you like

Origin blog.csdn.net/suifeng629/article/details/107140371