This article will have two parts: Dubbo SPI extensions and adaptive extensions

Dubbo SPI extension

Let’s take a look at an example to understand what an SPI extension is. This example is also available on the website

// Specify a Root interface
public interface Root {

    // There is only one method
    void sayHello(a); } the implementation classpublic class SpringRoot implements Root {
    @Override
    public void sayHello(a) {
        System.out.println("SpringRoot..."); }}public class DubboRoot implements Root {
    @Override
    public void sayHello(a) {
        System.out.println("DubboRoot..."); }}Copy the code

Root: dubo.learn.basic. Root: dubo.learn.basic. Root: dubo.learn.basic. Root: dubo.learn.

dubbo.learn.basic.SpringRoot
dubbo.learn.basic.DubboRoot
Copy the code

Finally, write a startup class

public class RootTest {

    public static void main(String[] args) { ServiceLoader<Root> serviceLoader = ServiceLoader.load(Root.class); serviceLoader.forEach(Root::sayHello); }}Copy the code

The printed result is:

SpringRoot…

DubboRoot…

ServiceLoader loads all the implementation classes, which is not what Dubbo wanted. Dubbo loads on demand, so Dubbo has implemented an SPI mechanism of its own.

The Dubbo SPI is somewhat different from the above examples in terms of usage:

  1. The first step is to annotate the Root interface with @spi

  2. Dubbo. Learn. Basic. The content of the Root file form is the key – value, such as springRoot = com. Hzed. Dubbo. Learn. Basic. SpringRoot

And then we’re going to change the startup class

public class RootTest {

    public static void main(String[] args) {
        ExtensionLoader<Root> extensionLoader = ExtensionLoader.getExtensionLoader(Root.class);
        Root springRoot = extensionLoader.getExtension("springRoot"); springRoot.sayHello(); }}Copy the code

The printed result is:

SpringRoot…

The core of Dubbo SPI is ExtensionLoader, so let’s analyze ExtensionLoader first

getExtensionLoader

The first step is to create the ExtensionLoader, passing in the Class type

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 interface!");
	}
	if(! withExtensionAnnotation(type)) {throw new IllegalArgumentException("Extension type(" + type +
				") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
	}
	
	// Get it from the cache first, if the cache does not have it, just new
	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;
}
Copy the code

The constructor of ExtensionLoader is private, so we can’t directly new the object. Instead, we get the object through static methods. Let’s look at the constructor of ExtensionLoader

private ExtensionLoader(Class
        type) {
	this.type = type;
	Root.class; root.class; The second is extensionFactory.class; But in our demo, objectFactory is empty, why is it empty? We'll talk about adaptive scaling later
	objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
Copy the code

The constructor is actually very simple. In this case, you can actually ignore objectFactory, so the type of the current object is root.class. Next, look at the getExtension method

public T getExtension(String name) {
	if (name == null || name.length() == 0)
		throw new IllegalArgumentException("Extension name == null");
	if ("true".equals(name)) {
		return getDefaultExtension();
	}
	// The target object is encapsulated as a holder
	Holder<Object> holder = cachedInstances.get(name);
	if (holder == null) {
		cachedInstances.putIfAbsent(name, new Holder<Object>());
		holder = cachedInstances.get(name);
	}
	// Double check
	Object instance = holder.get();
	if (instance == null) {
		synchronized (holder) {
			instance = holder.get();
			if (instance == null) {
				// If the cache does not exist, create the extension classinstance = createExtension(name); holder.set(instance); }}}return (T) instance;
}
Copy the code

createExtension

private T createExtension(String name) {
	/ / is to load the file first, and then get extension Class, according to the names of the specified Class, about load and parse the file https://dubbo.apache.org/zh/docs/v2.7/dev/source/dubbo-spi/ website has a detailed analysisClass<? > clazz = getExtensionClasses().get(name);if (clazz == null) {
		throw findException(name);
	}
	try {
		// First fetch from cache
		T instance = (T) EXTENSION_INSTANCES.get(clazz);
		if (instance == null) {
			// If the cache does not exist, create an instance
			EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
			instance = (T) EXTENSION_INSTANCES.get(clazz);
		}
		// dependency injectioninjectExtension(instance); Set<Class<? >> wrapperClasses = cachedWrapperClasses;if(wrapperClasses ! =null && !wrapperClasses.isEmpty()) {
			for (Class<?> wrapperClass : wrapperClasses) {
				instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
			}
		}
		return instance;
	} catch (Throwable t) {
		throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
				type + ") could not be instantiated: "+ t.getMessage(), t); }}Copy the code

We can skip getExtensionClasses() as long as the details don’t interfere with our understanding. GetExtensionClasses () does not affect our understanding of this method. So you can skip it. If you’re interested in this, you can go ahead and explore it

There’s an injectExtension here, which is dependency injection for Dubbo

private T injectExtension(T instance) {
	try {
		// objectFactory is ExtensionFactory, which may be Spring ApplicationContext and Dubbo's SpiExtensionFactory, AdaptiveExtensionFactory, Find dependent objects through these containers
		if(objectFactory ! =null) {
			for (Method method : instance.getClass().getMethods()) {
				if (method.getName().startsWith("set")
						&& method.getParameterTypes().length == 1
						&& Modifier.isPublic(method.getModifiers())) {
					/**
					 * Check {@link DisableInject} to see if we need auto injection for this property
					 */
					if(method.getAnnotation(DisableInject.class) ! =null) {
						continue; } Class<? > pt = method.getParameterTypes()[0];
					try {
						String property = method.getName().length() > 3 ? method.getName().substring(3.4).toLowerCase() + method.getName().substring(4) : "";
						Object object = objectFactory.getExtension(pt, property);
						if(object ! =null) {
							// Dubbo uses the set method to inject the dependent object into instancemethod.invoke(instance, object); }}catch (Exception e) {
						logger.error("fail to inject via method " + method.getName()
								+ " of interface " + type.getName() + ":" + e.getMessage(), e);
					}
				}
			}
		}
	} catch (Exception e) {
		logger.error(e.getMessage(), e);
	}
	return instance;
}
Copy the code

At this point, the overall framework of Dubbo SPI is basically laid out

SPI adaptive extension

What is adaptive? The description on the website reads as follows:

Sometimes, extensions do not want to be loaded during the framework startup phase, but rather want to be loaded based on runtime parameters when the extension method is called. This may sound contradictory. Extension methods cannot be called unless the extension is loaded (except for static methods). The extension cannot be loaded until the extension method is called. Dubbo solves this paradox with an adaptive extension mechanism.

Then there’s WheelMaker’s example. (You may still not know what adaptive is after reading it.)

When an extension method is called, it is loaded according to runtime parameters. In other words, when we use an extension class, we decide which extension class to use as the implementation class of the interface when we call the interface method

Look at the ServiceConfig

public class ServiceConfig<T> extends AbstractServiceConfig {

	/ /...
	
    private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
	
	/ /...
	
	private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {... Exporter<? > exporter = protocol.export(wrapperInvoker); . }/ /...
}

Copy the code

The doExportUrlsFor1Protocol method is used for exporting services, etc. <dubbo:protocol name=”dubbo” port=”2880″ /> <dubbo:protocol name=”dubbo” port=”2880″ />

That is, the code doesn’t know until it reaches the protocol.export(wrapperInvoker) line that the specific extension class to use is DubboProtocol. Protocol is assigned at the beginning of the code. What class is assigned? With this question in mind, start analyzing the adaptive extension framework.

The key point of entry, is above the Protocol Protocol = ExtensionLoader. GetExtensionLoader (Protocol. The class). GetAdaptiveExtension ()

Because getExtensionLoader creates ExtensionLoader objects, let’s look at the constructor again

private ExtensionLoader(Class
        type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
Copy the code

Said earlier that the constructor is called twice, that’s because the second is performed ExtensionLoader getExtensionLoader (ExtensionFactory. Class), when to perform here, Type is extensionFactory. class, objectFactory is null, that is, the extension class of ExtensionFactory is loaded first

The following code analysis is based on the extension class to load ExtensionFactory

getAdaptiveExtension

public T getAdaptiveExtension(a) {
	Object instance = cachedAdaptiveInstance.get();
	if (instance == null) {
		if (createAdaptiveInstanceError == null) {
			synchronized (cachedAdaptiveInstance) {
				instance = cachedAdaptiveInstance.get();
				if (instance == null) {
					try {
						// Nothing else to say, here is to create the extension class
						instance = createAdaptiveExtension();
						cachedAdaptiveInstance.set(instance);
					} catch (Throwable t) {
						createAdaptiveInstanceError = t;
						throw new IllegalStateException("fail to create adaptive instance: "+ t.toString(), t); }}}}else {
			throw new IllegalStateException("fail to create adaptive instance: "+ createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); }}return (T) instance;
}
Copy the code

createAdaptiveExtension

private T createAdaptiveExtension(a) {
	try {
                / / dependency injection is not the point, see getAdaptiveExtensionClass directly
		return injectExtension((T) getAdaptiveExtensionClass().newInstance());
	} catch (Exception e) {
		throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: "+ e.getMessage(), e); }}Copy the code

getAdaptiveExtensionClass

privateClass<? > getAdaptiveExtensionClass() {// The extension class is also loaded through a file
	getExtensionClasses();
	if(cachedAdaptiveClass ! =null) {
		return cachedAdaptiveClass;
	}
	return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

privateClass<? > createAdaptiveExtensionClass() {// This method is very long with many details. I suggest you go directly to the official website. Details are not our purpose this time.
	// But the result returned is exactly what we care about
	String code = createAdaptiveExtensionClassCode();
	ClassLoader classLoader = findClassLoader();
	com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
	return compiler.compile(code, classLoader);
}
Copy the code

When the code executes getExtensionClasses(), the ExtensionFactory implementation class AdaptiveExtensionFactory, which is annotated @Adaptive, has a branch judgment in the method. If the class is annotated by this annotation, CachedAdaptiveClass, so cachedAdaptiveClass is not empty, it just returns cachedAdaptiveClass, which is AdaptiveExtensionFactory.

At this point, the method getAdaptiveExtensionClass () returns cachedAdaptiveClass, then through getAdaptiveExtensionClass (). The newInstance () to create objects, then complete the dependency injection.

At this point, the objectFactory completes the assignment

AdaptiveExtensionFactory

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory(a) {
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        for (String name : loader.getSupportedExtensions()) {
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if(extension ! =null) {
                returnextension; }}return null; }}Copy the code

The constructor is called getExtensionClasses(), to add ExtensionFactory to the list. Or all extension classes of ExtensionFactory

The getExtension method directly calls the getExtension method of ExtensionFactory. Let’s look at the implementation of SpiExtensionFactory

public class SpiExtensionFactory implements ExtensionFactory {

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if(! loader.getSupportedExtensions().isEmpty()) {returnloader.getAdaptiveExtension(); }}return null; }}Copy the code

GetAdaptiveExtension () calls ExtensionLoader

createAdaptiveExtensionClass

This might get a little convoluted, but let’s rearrange it:

Back to ServiceConfig: When a private static final Protocol Protocol = ExtensionLoader. GetExtensionLoader (Protocol. The class). GetAdaptiveExtension ();

When performing ExtensionLoader. GetExtensionLoader (Protocol. The class), the method into the constructor ExtensionLoader (class <? > type), where type = protocol. class, when executing the constructor objectFactory = (type == extensionFactory.class? null : ExtensionLoader. GetExtensionLoader (ExtensionFactory. Class). GetAdaptiveExtension ()), again enters the constructor, When type = extensionFactory.class, this makes sense. The normal extension class needs to be created. Extensionfact-ry also needs to be created, but note that it is the constructor of both classes

When the second constructor returns, the code goes to the first constructor, and when the first constructor returns, the code continues to execute getAdaptiveExtens-ion ()

Have been analyzed getAdaptiveExtension front, will not repeat here, want to talk here about createAdaptiveExtensionClass (), to paste the code here

privateClass<? > createAdaptiveExtensionClass() {// This method is very long with many details. I suggest you go directly to the official website. Details are not our purpose this time.
	// But the result returned is exactly what we care about
	String code = createAdaptiveExtensionClassCode();
	ClassLoader classLoader = findClassLoader();
	com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
	return compiler.compile(code, classLoader);
}
Copy the code

CreateAdaptiveExtensionClassCode () is to create the extension of the class, we don’t see the inside of the details, but it returned

If Protocol is used as an example, return:

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;


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

    public int getDefaultPort(a) {
        throw new UnsupportedOperationException(
            "method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public com.alibaba.dubbo.rpc.Exporter export( com.alibaba.dubbo.rpc.Invoker arg0)
        throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument == null");
        }

        if (arg0.getUrl() == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        }

        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = ((url.getProtocol() == null)?"dubbo"
                                                      : url.getProtocol());

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
                url.toString() + ") use keys([protocol])");
        }

        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
                                                                                                   .getExtension(extName);

        return extension.export(arg0);
    }

    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1)
        throws com.alibaba.dubbo.rpc.RpcException {
        if (arg1 == null) {
            throw new IllegalArgumentException("url == null");
        }

        com.alibaba.dubbo.common.URL url = arg1;
        String extName = ((url.getProtocol() == null)?"dubbo"
                                                      : url.getProtocol());

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
                url.toString() + ") use keys([protocol])");
        }

        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
                                                                                                   .getExtension(extName);

        returnextension.refer(arg0, arg1); }}Copy the code

If ProxyFactory is used as an example, return:

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;


public class ProxyFactory$Adaptive implements com.alibaba.dubbo.rpc.ProxyFactory {
    public com.alibaba.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, com.alibaba.dubbo.common.URL arg2)
        throws com.alibaba.dubbo.rpc.RpcException {
        if (arg2 == null) {
            throw new IllegalArgumentException("url == null");
        }

        com.alibaba.dubbo.common.URL url = arg2;
        String extName = url.getParameter("proxy"."javassist");

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" +
                url.toString() + ") use keys([proxy])");
        }

        com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class)
                                                                                                           .getExtension(extName);

        return extension.getInvoker(arg0, arg1, arg2);
    }

    public java.lang.Object getProxy(com.alibaba.dubbo.rpc.Invoker arg0,
        boolean arg1) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument == null");
        }

        if (arg0.getUrl() == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        }

        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("proxy"."javassist");

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" +
                url.toString() + ") use keys([proxy])");
        }

        com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class)
                                                                                                           .getExtension(extName);

        return extension.getProxy(arg0, arg1);
    }

    public java.lang.Object getProxy(com.alibaba.dubbo.rpc.Invoker arg0)
        throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument == null");
        }

        if (arg0.getUrl() == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        }

        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("proxy"."javassist");

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" +
                url.toString() + ") use keys([proxy])");
        }

        com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class)
                                                                                                           .getExtension(extName);

        returnextension.getProxy(arg0); }}Copy the code

If we compare the two classes to see what they have in common, a careful eye might notice that the logic of the methods is the same, except for the variables and variable types, which are classes that are concatenated with strings and then generated by JavAssist. Protocol Adaptive and ProxyFactoryAdaptive and ProxyFactoryAdaptive are essentially proxy classes.

Let’s focus on this line:

 com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
                                                                                                   .getExtension(extName);
Copy the code

When an extension method is called, it is loaded according to runtime parameters.

The previous question has been answered: Protocol Protocol is an instance of Protocol$Adaptive. When protocol.export() is called, the agent class loads the implementation class and then calls export() of the implementation class. The whole process is called SPI Adaptive extension.

conclusion

In this article we introduced the fundamentals of SPI and Dubbo SPI and adaptive extension. From beginning to end, we focused on the overall framework. As for some details, we didn’t cover them here, because I think you can either look at the code, check out the debug, or check out the official website.

The Dubbo SPI adaptive extension is not that mysterious, and it may seem a bit confusing at first. The essence of the extension is that it generates proxy classes for each interface at runtime, and then loads the extension classes through Dubbo SPI, and the extension classes perform the real logic.