Dubbo - SPI mechanism

Keywords: Java Dubbo

Dubbo SPI

dubbo adopts SPI idea instead of using the SPI mechanism of jdk, but a set of SPI mechanism implemented by itself.

Disadvantages of jdk SPI

  • The JDK standard SPI will load all implementations of instantiated extension points at one time. If N implementation classes are added to the file under META-INF/service, all implementations will be loaded at one time when JDK is started. If the initialization of some extension points is time-consuming or if some implementation classes are not used, it will be a waste of resources
  • If the extension point fails to load, the caller will report an error, which is difficult to locate

Dubbo SPI

In Dubbo's source code, the following three codes exist in many places: adaptive extension point, extension point with specified name and active extension point.

//For adaptive extension points, the default class will be used. For example, the following Protocol uses DubboProtocol by default
ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();
//To implement the extension point of the name, we can customize the implementation class and specify its name in the configuration file (/META_INF/dubbo/interal/**), and then call this method to get the specified extension class.
ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);
ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);

For example:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

Protocol interface. When running, dubbo will judge which implementation class of the protocol interface should be selected to instantiate the object.

It will find the Protocol you configured, load the Protocol implementation class you configured into the JVM, and then instantiate the object. Just use the Protocol implementation class you configured. The above line of code is widely used in dubbo, that is, for many components, one interface and multiple implementations are reserved, and then dynamically find them according to the configuration when the system is running The corresponding implementation class. If you don't configure it, go to the default implementation class.

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

    void destroy();  
  
} 

The following possible extension classes are stored in the meta-inf / Dubbo / international / org.apache.dubbo.rpc.protocol file

filter=org.apache.dubbo.rpc.cluster.filter.ProtocolFilterWrapper
listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=org.apache.dubbo.rpc.support.MockProtocol
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol
rest=org.apache.dubbo.rpc.protocol.rest.RestProtocol
grpc=org.apache.dubbo.rpc.protocol.grpc.GrpcProtocol
tri=org.apache.dubbo.rpc.protocol.tri.TripleProtocol
registry=org.apache.dubbo.registry.integration.InterfaceCompatibleRegistryProtocol
service-discovery-registry=org.apache.dubbo.registry.integration.RegistryProtocol
qos=org.apache.dubbo.qos.protocol.QosProtocolWrapper

ExtensionLoaders cached in ExtensionLoader after execution

It stores a map

Protocol protocol = extensionloader. Getextensionloader (protocol. Class). Getadapteextension(); the extension classes in the above files will be read during operation, and then each class will be loaded one by one (not instantiated), and then a class file will be created through javassist. Let's take a look at the automatically generated code.

package com.chouxiaozi.dubboprovider;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
	
    public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        org.apache.dubbo.common.URL url = arg0.getUrl();
        //In fact, it will be instantiated again through ExtensionLoader.getExtensionLoader according to the name marked in the @ SPI annotation
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }

    public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }

    public java.util.List getServers() {
        throw new UnsupportedOperationException("The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
}

So you can see how Dubbo's SPI mechanism works by default. In fact, it is the Protocol interface, @ SPI("Dubbo") That is, the SPI mechanism is used to provide the implementation class. The implementation class is found in the configuration file through Dubbo as the default key. The name of the configuration file is the same as the fully qualified name of the interface. The default implementation class can be found through Dubbo as the key. The default implementation class is org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

If you want to dynamically replace the default implementation class, you need to use the @ Adaptive interface. In the Protocol interface, two methods are annotated with @ Adaptive annotation, that is, those two interfaces will be implemented by agents.

For example, the protocol interface has two @ Adaptive annotations marked with methods. When running, a proxy class will be generated for the protocol. There will be proxy code in the two methods of the proxy class. The proxy code will dynamically obtain the key according to the protocol in the url when running. The default is dubbo. You can also specify it yourself. If you specify another key, Then you will get instances of other implementation classes

In fact, there is another way,

  • The above Protocol interface uses @ Adaptive to modify the interface method, and then spi will generate a Protocol$Adaptive proxy class to execute the final DubboProtocol.
  • Another way is to use @ Adaptive modified classes, which will directly use @ Adaptive modified classes to create objects, such as
@SPI
public interface ExtensionFactory {
    <T> T getExtension(Class<T> type, String name);
}

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {}

If @ SPI has no annotation name here, the implementation class decorated with @ Adaptive will be used to create the object

Source code analysis

Important members and construction methods
//Store all created extensionloaders
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
//Store all instances
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
//Store all non
private final Map<String, Object> cachedActivates = Collections.synchronizedMap(new LinkedHashMap<>());
private volatile Class<?> cachedAdaptiveClass = null;
private ExtensionLoader(Class<?> type) {
    this.type = type;//Interface type
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); 
}
getExtensionLoader method
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
	//Omit check...
    
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));//If it cannot be obtained, a new ExtensionLoader will be created
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}
getAdaptiveExtension method to get an instance
public T getAdaptiveExtension() {
    //Get from cache first
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                try {
                    //Create adaptive instance
                    instance = createAdaptiveExtension();
                    //Add instance to cache
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                    createAdaptiveInstanceError = t;
                    throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                }
            }
        }
    }

    return (T) instance;
}
createAdaptiveExtension
private T createAdaptiveExtension() {
    try {
    	//Call getAdaptiveExtensionClass to get the class object   
        //Call newInstance to create an instance
        //injectExtension
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}
getAdaptiveExtensionClass
private Class<?> getAdaptiveExtensionClass() {
    getExtensionClasses();//Get all classes from the configuration file and load them. At the same time, put them into different containers (that is, the member properties of the object) according to the different characteristics of the class
    //If there is a cached Adaptive class (that is, the implementation class of @ Adaptive annotation) after loading, it will be returned directly.
    /*
    	This is the reason why @ Adaptive directly uses this class after modifying it
    */
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    //If there is no class decorated with @ Adaptive, create the class
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
// #createAdaptiveExtensionClass
private Class<?> createAdaptiveExtensionClass() {
    //This is where the Protocl interface finally generates classes.
    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);
}

The interface for generating classes is more cumbersome. We only need to know that the final generated class is just a proxy. Finally, the name marked with @ SPI will be used as the key by default to find the corresponding class in the container

getExtensionClasses gets all extension implementation classes
private Map<String, Class<?>> getExtensionClasses() {
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                classes = loadExtensionClasses();//Load extension classes from configuration file
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}
private Map<String, Class<?>> loadExtensionClasses() {
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();

    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());//Load files in directory
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }
    return extensionClasses;
}

After loadDirectory, the depth of the call stack is larger. We directly look at the last call method; after finding the corresponding file, read the configuration of each row (format key=value,key is the key value, value is the corresponding class).

loadClass
//clazz is a full class name for the candidate class, loaded into the jvm through class.forName, and then called loadClass method.
loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                           boolean overridden) throws NoSuchMethodException {
    //a key:
    //If the class has Adaptive annotation, it is assigned to the global variable cacheddadaptive class
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz, overridden);
    } else if (isWrapperClass(clazz)) {//If it is a wrapper class, put it into cachedWrapperClasses (determine whether it is a wrapper class by judging whether it contains the parameter constructor of this type)
        cacheWrapperClass(clazz);
    } else {
        //Other are saved in the member property cachedActivates
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }

        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                cacheName(clazz, n);
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}
//This is all loaded

reference resources:

https://blog.csdn.net/yangbaggio/article/details/97617750

Posted by Robert Plank on Fri, 08 Oct 2021 00:44:37 -0700