Source Code Analysis - SOFARPC Extensible Mechanism SPI

Keywords: Java Dubbo Attribute

Links to the original text: https://www.cnblogs.com/luozh...

After leaving home these days, I just can output something crazily. I wanted to write the source code of DUBBO. But I found that there were too many sources of DUBBO. So I found a framework that didn't write so much. So I chose SOFARPC as the framework.

SOFARPC is an open source RPC framework of Ant Golden Clothing. Compared with DUBBO, it has no so many historical burdens, the code is more concise, the design idea is clearer, and the code is easier to understand.

So why rewrite the native SPI? The official explanation is as follows:

  1. On-demand loading
  2. Can have aliases
  3. Priorities can be prioritized for sorting and coverage
  4. Can control whether it is singleton or not
  5. Coding can be used in some scenarios
  6. Extension configuration location can be specified
  7. Other extension points can be excluded

The whole process is as follows:

Let's take Consumer Bootstrap as an example:

First, there must be an abstract class:

@Extensible(singleton = false)
public abstract class ConsumerBootstrap<T> {
    ....
}

Specify an extended implementation class:

@Extension("sofa")
public class DefaultConsumerBootstrap<T> extends ConsumerBootstrap<T> {
    ...
}

Extended description file META-INF/services/sofa-rpc/com.alipay.sofa.rpc.bootstrap.ConsumerBootstrap

sofa=com.alipay.sofa.rpc.bootstrap.DefaultConsumerBootstrap

When these preparations are complete, call them directly.

ConsumerBootstrap sofa =  ExtensionLoaderFactory.getExtensionLoader(ConsumerBootstrap.class).getExtension("sofa");

Next, let's look at the source code for Extension LoaderFactory

    /**
     * All extension loader {Class : ExtensionLoader}
     * This map contains all Extension Loader
     */
    private static final ConcurrentMap<Class, ExtensionLoader> LOADER_MAP = new ConcurrentHashMap<Class, ExtensionLoader>();


    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz, ExtensionLoaderListener<T> listener) {
        ExtensionLoader<T> loader = LOADER_MAP.get(clazz);
        if (loader == null) {
            //Lock if not get
            synchronized (ExtensionLoaderFactory.class) {
                //Prevent other thread operations from get ting again
                loader = LOADER_MAP.get(clazz);
                if (loader == null) {
                    loader = new ExtensionLoader<T>(clazz, listener);
                    LOADER_MAP.put(clazz, loader);
                }
            }
        }
        return loader;
    }

Then let's look at the constructor of the Extension Loader class.

    protected ExtensionLoader(Class<T> interfaceClass, boolean autoLoad, ExtensionLoaderListener<T> listener) {
        //If closing is being performed, the property is emptied and returned directly
        if (RpcRunningState.isShuttingDown()) {
            this.interfaceClass = null;
            this.interfaceName = null;
            this.listener = null;
            this.factory = null;
            this.extensible = null;
            this.all = null;
            return;
        }
        // Interfaces are empty, neither interfaces nor abstract classes
        if (interfaceClass == null ||
                !(interfaceClass.isInterface() || Modifier.isAbstract(interfaceClass.getModifiers()))) {
            throw new IllegalArgumentException("Extensible class must be interface or abstract class!");
        }
        //The name of the interface class currently loaded
        this.interfaceClass = interfaceClass;
        //Interface name
        this.interfaceName = ClassTypeUtils.getTypeStr(interfaceClass);
        this.listener = listener;
        //Extensible annotations are required on the interface
        Extensible extensible = interfaceClass.getAnnotation(Extensible.class);
        if (extensible == null) {
            throw new IllegalArgumentException(
                    "Error when load extensible interface " + interfaceName + ", must add annotation @Extensible.");
        } else {
            this.extensible = extensible;
        }
        // If it is a singleton, then factory is not empty
        this.factory = extensible.singleton() ? new ConcurrentHashMap<String, T>() : null;
        //Inside this property are all the implementation classes of the interface.
        this.all = new ConcurrentHashMap<String, ExtensionClass<T>>();
        if (autoLoad) {
            //Get the path to the extension point load
            List<String> paths = RpcConfigs.getListValue(RpcOptions.EXTENSION_LOAD_PATH);
            for (String path : paths) {
                //Load files according to path
                loadFromFile(path);
            }
        }
    }

Get all the extension point loading paths and go to loadFromFile to load the files.

    protected synchronized void loadFromFile(String path) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Loading extension of extensible {} from path: {}", interfaceName, path);
        }
        // By default, if no file name is specified, it is the interface name.
        String file = StringUtils.isBlank(extensible.file()) ? interfaceName : extensible.file().trim();
        String fullFileName = path + file;
        try {
            ClassLoader classLoader = ClassLoaderUtils.getClassLoader(getClass());
            loadFromClassLoader(classLoader, fullFileName);
        } catch (Throwable t) {
            if (LOGGER.isErrorEnabled()) {
                LOGGER.error("Failed to load extension of extensible " + interfaceName + " from path:" + fullFileName,
                    t);
            }
        }
    }
    
    
    protected void loadFromClassLoader(ClassLoader classLoader, String fullFileName) throws Throwable {
        Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fullFileName)
            : ClassLoader.getSystemResources(fullFileName);
        // There may be multiple files.
        if (urls != null) {
            while (urls.hasMoreElements()) {
                // Read a file
                URL url = urls.nextElement();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Loading extension of extensible {} from classloader: {} and file: {}",
                        interfaceName, classLoader, url);
                }
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        readLine(url, line);
                    }
                } catch (Throwable t) {
                    if (LOGGER.isWarnEnabled()) {
                        LOGGER.warn("Failed to load extension of extensible " + interfaceName
                            + " from classloader: " + classLoader + " and file:" + url, t);
                    }
                } finally {
                    if (reader != null) {
                        reader.close();
                    }
                }
            }
        }
    }

Next, enter readLine, which reads each row of records in the prop file, loads the class file of the implementation class and adds the file to all attribute after verification.

    protected void readLine(URL url, String line) {
        //Read a row of records in the file and divide the row with a = sign
        String[] aliasAndClassName = parseAliasAndClassName(line);
        if (aliasAndClassName == null || aliasAndClassName.length != 2) {
            return;
        }
        //alias
        String alias = aliasAndClassName[0];
        //Package name
        String className = aliasAndClassName[1];
        // Implementation Class of Read Configuration
        Class tmp;
        try {
            tmp = ClassUtils.forName(className, false);
        } catch (Throwable e) {
            if (LOGGER.isWarnEnabled()) {
                LOGGER.warn("Extension {} of extensible {} is disabled, cause by: {}",
                    className, interfaceName, ExceptionUtils.toShortString(e, 2));
            }
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Extension " + className + " of extensible " + interfaceName + " is disabled.", e);
            }
            return;
        }
        if (!interfaceClass.isAssignableFrom(tmp)) {
            throw new IllegalArgumentException("Error when load extension of extensible " + interfaceName +
                " from file:" + url + ", " + className + " is not subtype of interface.");
        }
        Class<? extends T> implClass = (Class<? extends T>) tmp;

        // Check for extensible identifiers
        Extension extension = implClass.getAnnotation(Extension.class);
        if (extension == null) {
            throw new IllegalArgumentException("Error when load extension of extensible " + interfaceName +
                " from file:" + url + ", " + className + " must add annotation @Extension.");
        } else {
            String aliasInCode = extension.value();
            if (StringUtils.isBlank(aliasInCode)) {
                // The extended implementation class has no @Extension tag configured
                throw new IllegalArgumentException("Error when load extension of extensible " + interfaceClass +
                    " from file:" + url + ", " + className + "'s alias of @Extension is blank");
            }
            if (alias == null) {
                // There is no configuration in the spi file.
                alias = aliasInCode;
            } else {
                // Inconsistencies between configuration and code in spi files
                if (!aliasInCode.equals(alias)) {
                    throw new IllegalArgumentException("Error when load extension of extensible " + interfaceName +
                        " from file:" + url + ", aliases of " + className + " are " +
                        "not equal between " + aliasInCode + "(code) and " + alias + "(file).");
                }
            }
            // Interfaces need to be numbered, implementation classes are not set
            if (extensible.coded() && extension.code() < 0) {
                throw new IllegalArgumentException("Error when load extension of extensible " + interfaceName +
                    " from file:" + url + ", code of @Extension must >=0 at " + className + ".");
            }
        }
        // Can't be default sum*
        if (StringUtils.DEFAULT.equals(alias) || StringUtils.ALL.equals(alias)) {
            throw new IllegalArgumentException("Error when load extension of extensible " + interfaceName +
                " from file:" + url + ", alias of @Extension must not \"default\" and \"*\" at " + className + ".");
        }
        // Check for homonyms
        ExtensionClass old = all.get(alias);
        ExtensionClass<T> extensionClass = null;
        if (old != null) {
            // If the current extension can cover other extensions of the same name
            if (extension.override()) {
                // If the priority is not as high as the old one, ignore it
                if (extension.order() < old.getOrder()) {
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Extension of extensible {} with alias {} override from {} to {} failure, " +
                            "cause by: order of old extension is higher",
                            interfaceName, alias, old.getClazz(), implClass);
                    }
                } else {
                    if (LOGGER.isInfoEnabled()) {
                        LOGGER.info("Extension of extensible {} with alias {}: {} has been override to {}",
                            interfaceName, alias, old.getClazz(), implClass);
                    }
                    // If the current extension can cover other extensions of the same name
                    extensionClass = buildClass(extension, implClass, alias);
                }
            }
            // If old extensions are covered
            else {
                if (old.isOverride() && old.getOrder() >= extension.order()) {
                    // If coverage extensions are loaded, then the original extensions are loaded
                    if (LOGGER.isInfoEnabled()) {
                        LOGGER.info("Extension of extensible {} with alias {}: {} has been loaded, ignore origin {}",
                            interfaceName, alias, old.getClazz(), implClass);
                    }
                } else {
                    // If it cannot be overridden, throw an exception that already exists
                    throw new IllegalStateException(
                        "Error when load extension of extensible " + interfaceClass + " from file:" + url +
                            ", Duplicate class with same alias: " + alias + ", " + old.getClazz() + " and " + implClass);
                }
            }
        } else {
            extensionClass = buildClass(extension, implClass, alias);
        }
        if (extensionClass != null) {
            // Check for mutually exclusive extension points
            for (Map.Entry<String, ExtensionClass<T>> entry : all.entrySet()) {
                ExtensionClass existed = entry.getValue();
                if (extensionClass.getOrder() >= existed.getOrder()) {
                    // New Priority > = Old Priority, check whether new extensions exclude old ones
                    String[] rejection = extensionClass.getRejection();
                    if (CommonUtils.isNotEmpty(rejection)) {
                        for (String rej : rejection) {
                            existed = all.get(rej);
                            if (existed == null || extensionClass.getOrder() < existed.getOrder()) {
                                continue;
                            }
                            ExtensionClass removed = all.remove(rej);
                            if (removed != null) {
                                if (LOGGER.isInfoEnabled()) {
                                    LOGGER.info(
                                        "Extension of extensible {} with alias {}: {} has been reject by new {}",
                                        interfaceName, removed.getAlias(), removed.getClazz(), implClass);
                                }
                            }
                        }
                    }
                } else {
                    String[] rejection = existed.getRejection();
                    if (CommonUtils.isNotEmpty(rejection)) {
                        for (String rej : rejection) {
                            if (rej.equals(extensionClass.getAlias())) {
                                // Excluded by other extensions
                                if (LOGGER.isInfoEnabled()) {
                                    LOGGER.info(
                                        "Extension of extensible {} with alias {}: {} has been reject by old {}",
                                        interfaceName, alias, implClass, existed.getClazz());
                                    return;
                                }
                            }
                        }
                    }
                }
            }

            loadSuccess(alias, extensionClass);
        }
    }

After loading the file, we'll go back to it.

ConsumerBootstrap sofa =  ExtensionLoaderFactory.getExtensionLoader(ConsumerBootstrap.class).getExtension("sofa");

Enter the getExtension method

    public ExtensionClass<T> getExtensionClass(String alias) {
        return all == null ? null : all.get(alias);
    }

    public T getExtension(String alias) {
        //Get the loaded class from the all attribute
        ExtensionClass<T> extensionClass = getExtensionClass(alias);
        if (extensionClass == null) {
            throw new SofaRpcRuntimeException("Not found extension of " + interfaceName + " named: \"" + alias + "\"!");
        } else {
            //When loading the class, verify whether it is a singleton, and if it is a singleton, then factory is not null
            if (extensible.singleton() && factory != null) {
                T t = factory.get(alias);
                if (t == null) {
                    synchronized (this) {
                        t = factory.get(alias);
                        if (t == null) {
                            //instantiation
                            t = extensionClass.getExtInstance();
                            //Put it in the factory, and the class of the singleton will be ready next time. No need to recreate it.
                            factory.put(alias, t);
                        }
                    }
                }
                return t;
            } else {
                //instantiation
                return extensionClass.getExtInstance();
            }
        }
    }

Let's go into Extension Class and see the getExtInstance method

    /**
     * Server-side instance objects (reserved only when they are singletons)
     * Modified with volatile to ensure visibility
     */
    private volatile transient T       instance;
    
    /**
     * Get the server-side instance object, return the singleton object if it is singleton, and return the newly created instance object if it is not.
     *
     * @param argTypes Constructor parameter type
     * @param args     Constructor parameter values
     * @return Extended Point Object Instance Extinstance
     */
    public T getExtInstance(Class[] argTypes, Object[] args) {
        if (clazz != null) {
            try {
                if (singleton) { // If it is a singular case
                    if (instance == null) {
                        synchronized (this) {
                            if (instance == null) {
                                //Create instances by reflection
                                instance = ClassUtils.newInstanceWithArgs(clazz, argTypes, args);
                            }
                        }
                    }
                    return instance; // Retention of singletons
                } else {
                    //Create instances by reflection
                    return ClassUtils.newInstanceWithArgs(clazz, argTypes, args);
                }
            } catch (Exception e) {
                throw new SofaRpcRuntimeException("create " + clazz.getCanonicalName() + " instance error", e);
            }
        }
        throw new SofaRpcRuntimeException("Class of ExtensionClass is null");
    }

After looking at the implementation of SOFARPC extension class, I feel that the code is written very neatly and the logic is very clear. There are many things to learn, such as thread security using double check lock and volatile to ensure visibility.

Posted by Daveg on Thu, 15 Aug 2019 03:10:48 -0700