Dubbo SPI core implementation

  • Introduction to Dubbo SPI implementation
  • ExtensionFactory interface
  • ExtensionLoader SPI core implementation
  • conclusion

Dubbo SPI implementation

Due to Java SPI implementation is insufficient, so Dubbo own implementation of a set of SPI, Dubbo SPI implementation application startup will implement the SPI interface extension class load into the cache, the following is a simple look at the core source code Dubbo SPI implementation

ExtensionFactory interface

ExtensionFactory interface, which provides the core interface to access the SPI factory

@SPI
public interface ExtensionFactory {

    /**
     * Get extension.
     *
     * @param type object type.
     * @param name object name.
     * @return object instance.
     */
    <T> T getExtension(Class<T> type, String name);

}
Copy the code

ExtensionLoader Dubbo SPI core implementation class

getExtensionLoader

Dubbo’s SPI is loaded when the program is started and the service is registered. The getExtensionLoader method is called through a private constructor. The main logic of this method is as follows:

  1. typeWhether is empty
  2. typeInterface type
  3. typeClass with@SPIannotations
  4. After passing the verification will be fromEXTENSION_LOADERSGets the extension instance from the cache of the extension implementation
  5. The code doesn’t get it at first, so it goes toEXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));To initialize and cache the extended instance.
Private static final ConcurrentMap<Class<? >, ExtensionLoader<? >> EXTENSION_LOADERS = new ConcurrentHashMap<>(64); @SuppressWarnings("unchecked") 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!" ); } if (! withExtensionAnnotation(type)) { throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!" ); } // Implement ExtensionLoader<T> loader = (ExtensionLoader<T>) extension_loaders.get (type); PutIfAbsent (type, new ExtensionLoader<T>(type)); if (loader == null) {// Add extension_loaders. putIfAbsent(type, new ExtensionLoader<T>(type)); loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); } return loader; }Copy the code

ExtensionLoader private constructor

The ExtensionLoader class is initialized when getExtensionLoader calls the private constructor. The code logic is as follows:

  1. Instantiate properties that initially instantiate classes but are empty collections;
  2. When the private constructor is first executedExtensionLoaderWhen,typeIt will be something elseSPIInterface, that’s when you go toExtensionLoader.getExtensionLoader(ExtensionFactory.class)“, will speak at this pointExtensionFactoryAdd to cache;
  3. callgetAdaptiveExtensionMethod to obtain an instance of the adaptive extension point and copy the obtained instance to the objectFactory, which is contained in the collection returned by debugSpiExtensionFactoryandSpringExtensionFactory.
private final ConcurrentMap<Class<? >, String> cachedNames = new ConcurrentHashMap<Class<? >, String>(); private final Holder<Map<String, Class<? >>> cachedClasses = new Holder<Map<String, Class<? > > > (); private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>(); private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>(); private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>(); private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<String, IllegalStateException>(); // Private ExtensionLoader(Class<? > type) { this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); }Copy the code

GetAdaptiveExtension method

The method obtains the adaptive extension point, and the code is as follows:

Public T getAdaptiveExtension () {/ / adaptive extension point was obtained from the cache Object instance Object instance = cachedAdaptiveInstance. The get (); // Not available. Adopt double check if the instance (= = null) {if (createAdaptiveInstanceError = = null) {/ / the cache using the object's monitor (lock) synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get(); If (instance == null) {try {// Create an adaptive extension point object instance = createAdaptiveExtension(); / / the object cache 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 method

Adaptive extension point object, and create the HTML code is as follows, here, to watch injectExtension method later will look at the source of this method, and analyzes in detail its functions, the following code through getAdaptiveExtensionClass (). The newInstance () to create the adaptive extension point of the object

private T createAdaptiveExtension() { try { 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 method

Will first call getExtensionClasses () to load the extension of class (detailed process analysis), if the adaptive extended class cache is not null return adaptive extended class, otherwise call createAdaptiveExtensionClass () method to create adaptive extension class.

private Class<? > getAdaptiveExtensionClass() { getExtensionClasses(); if (cachedAdaptiveClass ! = null) { return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass(); }Copy the code

CreateAdaptiveExtensionClass method

The code for this method is as follows, which creates an adaptive extension class. Code analysis can be done by referring to the comments in the code

private Class<? > createAdaptiveExtensionClass () {/ / to create adaptive class code, unknown look here, this method is through the dynamic proxy generated proxy class, Using reflection for the bytecode enhancement operation String code = createAdaptiveExtensionClassCode (); ClassLoader ClassLoader = findClassLoader(); // Get the bytecode enhanced compiler, where the bytecode enhanced compiler is also extensible, Here the default use javassist com.alibaba.dubbo.common.compiler.Com piler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); Return compiler.compile(code, classLoader); // Use the bytecode enhanced compiler to create class return compiler.compile(code, classLoader). }Copy the code

Load extensions from configuration files to implement source code analysis

This method starts by fetching the supported extensions from the cachedClasses cache. If not, load the extension point from the fixed path extension configuration file. The code is as follows:

  1. GetExtensionClasses: Attempts to retrieve an extension point from the cache. The extension point’s class information is stored in a Java bean that you define and is called when it is not available from the cacheloadExtensionClassesMethod loads the extension point class and caches it
  2. LoadExtensionClasses: First determines whether the Type class existsSPIAnnotations, if there is a value it checks the value of the annotation if there is a value it caches the default extension class name, and then it loads the extension point from the fixed path, which is hereMETA-INF/dubbo/internal/.META-INF/dubbo/.META-INF/services/
  3. LoadDirectory: gets the class loader, according toURLLoad extension point
  4. LoadResource: Loads extensible interfaces tocachedNamesIn the
  5. LoadClass: Determines whether the loaded class is a Wapper class. If yes, it is put into the Wapper class cache. Otherwise, load the class directly tocachedNamesIn the
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; } // synchronized in getExtensionClasses private Map<String, Class<? >> loadExtensionClasses() { final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation ! = null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { String[] names = NAME_SEPARATOR.split(value); if (names.length > 1) { throw new IllegalStateException("more than 1 default extension name on extension " + type.getName() + ": " + Arrays.toString(names)); } if (names.length == 1) cachedDefaultName = names[0]; } } Map<String, Class<? >> extensionClasses = new HashMap<String, Class<? > > (); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); loadDirectory(extensionClasses, DUBBO_DIRECTORY); loadDirectory(extensionClasses, SERVICES_DIRECTORY); return extensionClasses; } private void loadDirectory(Map<String, Class<? >> extensionClasses, String dir) { String fileName = dir + type.getName(); try { Enumeration<java.net.URL> urls; ClassLoader classLoader = findClassLoader(); if (classLoader ! = null) { urls = classLoader.getResources(fileName); } else { urls = ClassLoader.getSystemResources(fileName); } if (urls ! = null) { while (urls.hasMoreElements()) { java.net.URL resourceURL = urls.nextElement(); loadResource(extensionClasses, classLoader, resourceURL); } } } catch (Throwable t) { logger.error("Exception when load extension class(interface: " + type + ", description file: " + fileName + ").", t); } } private void loadResource(Map<String, Class<? >> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { try { BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8")); try { String line; while ((line = reader.readLine()) ! = null) { final int ci = line.indexOf('#'); if (ci >= 0) line = line.substring(0, ci); line = line.trim(); if (line.length() > 0) { try { String name = null; int i = line.indexOf('='); if (i > 0) { name = line.substring(0, i).trim(); line = line.substring(i + 1).trim(); } if (line.length() > 0) { loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); } } catch (Throwable t) { IllegalStateException e = new IllegalStateException("Failed to load extension class(interface:  " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t); exceptions.put(line, e); } } } } finally { reader.close(); } } catch (Throwable t) { logger.error("Exception when load extension class(interface: " + type + ", class file: " + resourceURL + ") in " + resourceURL, t); } } private void loadClass(Map<String, Class<? >> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { if (! type.isAssignableFrom(clazz)) { throw new IllegalStateException("Error when load extension class(interface: " + type + ", class line: " + clazz.getName() + "), class " + clazz.getName() + "is not subtype of interface."); } if (clazz.isAnnotationPresent(Adaptive.class)) { if (cachedAdaptiveClass == null) { cachedAdaptiveClass = clazz; } else if (! cachedAdaptiveClass.equals(clazz)) { throw new IllegalStateException("More than 1 adaptive class found: " + cachedAdaptiveClass.getClass().getName() + ", " + clazz.getClass().getName()); } } else if (isWrapperClass(clazz)) { Set<Class<? >> wrappers = cachedWrapperClasses; if (wrappers == null) { cachedWrapperClasses = new ConcurrentHashSet<Class<? > > (); wrappers = cachedWrapperClasses; } wrappers.add(clazz); } else { clazz.getConstructor(); if (name == null || name.length() == 0) { 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 (names ! = null && names.length > 0) { Activate activate = clazz.getAnnotation(Activate.class); if (activate ! = null) { cachedActivates.put(names[0], activate); } for (String n : names) { if (! cachedNames.containsKey(clazz)) { cachedNames.put(clazz, n); } Class<? > c = extensionClasses.get(n); if (c == null) { extensionClasses.put(n, clazz); } else if (c ! = clazz) { throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName()  + " and " + clazz.getName()); } } } } } private boolean isWrapperClass(Class<? > clazz) { try { clazz.getConstructor(type); return true; } catch (NoSuchMethodException e) { return false; }}Copy the code

InjectExtension method

The main purpose of this method is to implement dependency injection. The code and process are as follows:

  1. Filtered out by reflectionsetThe way to start
  2. When judging byDisableInjectEmbellishment, if embellished then no dependency injection
  3. Obtain the class information and obtain the attribute information based on the class informationproperty
  4. Instantiate the extension class
  5. callinvokeMethod, which calls the instance objectsetterMethod, in this case by reflection
private T injectExtension(T instance) { try { 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) { method.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

The AdaptiveExtensionFactory class getExtension method

The createExtension() method is used to load the object instance. The code and processing flow are as follows:

  1. The extension name is as followsDubbo in dubbo (@ SPI (" "))Don’t empty
  2. If the extension is microtrueReturn the default extension, the default extension code is not shown, see the code logic will returnnull
  3. Get the extension object from the extension implementation instance cacheHolder, if it is empty, the extension name andHolderObject into the cache and return the holder with the extension
  4. get()Extend the object, using double check, if the object is empty then passcreateExtension(name)Create this object andsettoholderIn the
    public T getExtension(String name) {
        if (name == null || name.length() == 0)
            throw new IllegalArgumentException("Extension name == null");
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        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.set(instance);
                }
            }
        }
        return (T) instance;
    }
Copy the code

CreateExtension method

The code and process are as follows:

  1. Gets the name of the extension class based on the extension nameClass
  2. Gets an object instance of the extended class from the cache
  3. If the instance is empty, create it by reflection and place itEXTENSION_INSTANCESThe cache
  4. injectExtensionThe method is explained earlier
  5. If the wrapper class cache is not empty, it traverses the wrapper cache and creates a wrapper class objectorg.apache.dubbo.rpc.protocol.ProtocolFilterWrapper.listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapperEtc.
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); } injectExtension(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

conclusion

I have spent two days on and off to sort out part of Dubbo SPI. In fact, there are still many source codes to be disassembled. If you are interested, you can debug the code privately to understand the process of Dubbo SPI. If you want to implement an SPI mechanism modeled after Dubbo, I recommend reading the SPI implementation of soul Gateway, another open source middleware. The SPI implementation of Soul Gateway is essentially a replica of dubbo SPI implementation, but it is much simpler than Dubbo. More friendly than Dubbo for beginners and friends who quickly understand SPI.