SOFA source code analysis - extension mechanism

foreword

We have already learned a little about the extension mechanism of SOFA in previous articles, and we have also said that a good framework must be easy to extend. So how exactly is SOFA implemented?

Take a look.

how to use?

See the official demo:

1. Define extension points.

@Extensible
public interface Person {
    void getName();
}

2. Define the extension implementation

@Extension("A")
public class PersonA implements Person{
    @Override
    public void getName() {
        System.out.println("li wei");
    }
}

3. Write the extension description file: META-INF/services/sofa-rpc/com.alipay.sofa.rpc.extension.Person. The contents of the file are as follows:

A=com.alipay.sofa.rpc.extension.PersonA

4. Load the extension point and obtain the extension implementation class for use.

Person person = ExtensionLoaderFactory.getExtensionLoader(Person.class).getExtension("A");

It's very simple, right? You only need 2 annotations, a configuration file, and then use the factory method to get the instance through the interface name and extension point name. So, what we are going to look at today is these things. In fact, it is quite simple. If you have used the SPI that comes with Java, you will be familiar with it.

source code implementation

Where to start? Of course this factory method.

The source code is as follows:

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz) {
        return getExtensionLoader(clazz, null);
    }

Another overloaded method is called:

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz, ExtensionLoaderListener<T> listener) {
    ExtensionLoader<T> loader = LOADER_MAP.get(clazz);
    if (loader == null) {
        synchronized (ExtensionLoaderFactory.class) {
            loader = LOADER_MAP.get(clazz);
            if (loader == null) {
                loader = new ExtensionLoader<T>(clazz, listener);
                LOADER_MAP.put(clazz, loader);
            }
        }
    }
    return loader;
}

Very simple, take it from the Map, if not, create one and put it in the cache. Double-checked locks are used here.

Note that there is a parameter, ExtensionLoaderListener, which is used to call back its onLoad method when the loading is completed, which is usually used asynchronously.

So, see it, the key process is in new ExtensionLoader().

The creation process of ExtensionLoader

Construction method:

    public ExtensionLoader(Class<T> interfaceClass, ExtensionLoaderListener<T> listener) {
        this(interfaceClass, true, listener);
    }

Overload:

protected ExtensionLoader(Class<T> interfaceClass, boolean autoLoad, ExtensionLoaderListener<T> listener) {
    if (RpcRunningState.isShuttingDown()) {
        this.interfaceClass = null;
        this.interfaceName = null;
        this.listener = null;
        this.factory = null;
        this.extensible = null;
        this.all = null;
        return;
    }
    // 接口为空,既不是接口,也不是抽象类
    if (interfaceClass == null ||
        !(interfaceClass.isInterface() || Modifier.isAbstract(interfaceClass.getModifiers()))) {
        throw new IllegalArgumentException("Extensible class must be interface or abstract class!");
    }
    this.interfaceClass = interfaceClass;
    this.interfaceName = ClassTypeUtils.getTypeStr(interfaceClass);
    this.listener = listener;
    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;
    }
    // 非单例则是空
    this.factory = extensible.singleton() ? new ConcurrentHashMap<String, T>() : null;
    this.all = new ConcurrentHashMap<String, ExtensionClass<T>>();
    if (autoLoad) {
        List<String> paths = RpcConfigs.getListValue(RpcOptions.EXTENSION_LOAD_PATH);
        for (String path : paths) {
            loadFromFile(path);
        }
    }
}

There is one thing to note here, the autoLoad property defaults to true, which means automatic loading by default. Of course, it is basically automatically loaded, and the parameters here are used for testing.

Look at this construction method. The first is a wave of assignment operations.

Then check the parameters. Check whether it is an abstract class or interface, and check whether there is an Extensible annotation.

If it is a singleton, create a Map to save the object, if not, the Map is null and create a new one every time.

Among them, there will be RpcConfigs.getListValue(RpcOptions.EXTENSION_LOAD_PATH)an operation used to obtain the configured path from the global search to find rpc-config-default.json. Contains the following:

  // 扩展点加载的路径
  "extension.load.path": [
    "META-INF/services/sofa-rpc/",
    "META-INF/services/"
  ],

two paths. So the return value is a List.

The for loop parses the path in the list, that is, calls the loadFromFile method.

The content of the method is as follows:

protected synchronized void loadFromFile(String path) {
    // 默认如果不指定文件名字,就是接口名
    String file = StringUtils.isBlank(extensible.file()) ? interfaceName : extensible.file().trim();
    String fullFileName = path + file;
    ClassLoader classLoader = ClassLoaderUtils.getClassLoader(getClass());
    loadFromClassLoader(classLoader, fullFileName);  
}

First judge whether the file attribute of the annotation is empty, if it is empty, use the interface name, otherwise use the name specified by file.

Then, concatenate the path and file attributes in the configuration file. In fact, StringBuilder can be used here.

and then? Get the ClassLoader. By default, the ClassLoader of the current thread is used. If it is empty, the ClassLoader of the current ExtensionLoader is used. If the given class is empty, the SystemClassLoader is used.

From this, we can see the benefits of SPI design. If implemented in strategy mode, then ClassLoader must be the same, and SPI-like design can isolate the ClassLoader of upper-layer application and lower-layer application.

After getting the ClassLoader and the interface name of the full path, start loading the file.

code show as below:

protected void loadFromClassLoader(ClassLoader classLoader, String fullFileName) throws Throwable {
    Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fullFileName)
        : ClassLoader.getSystemResources(fullFileName);
    // 可能存在多个文件。
    if (urls != null) {
        while (urls.hasMoreElements()) {
            // 读取一个文件
            URL url = urls.nextElement();
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"));
                String line;
                while ((line = reader.readLine()) != null) {
                    readLine(url, line);
                }
            } finally {
                if (reader != null) {
                    reader.close();
                }
            }
        }
    }
}

First obtain the file URL collection under the classpath through ClassLoader. It then traverses these URLs, reads the file through the stream, and parses the string read from each line through the readLine method. The string here is in the SPI configuration file, similar to the following:

file name:com.alipay.sofa.rpc.extension.Person

A=com.alipay.sofa.rpc.extension.PersonA

By loading the specified path + interface name (or Extensible specified file), get the file name, and then read the text in the file. Since it is read line by line, it may be read multiple times.

Then watch the readLine method process the parsed string.

This method is relatively long, mainly for data verification, so I won't post it, and talk about the logic.

  1. First parse the data of the line, parseAliasAndClassName method, if it is #, it is processing such as comments. According to the = sign, get ClassName, create a data, subscript 0 is an alias, such as the above A, subscript 1 is the full class name.

  2. Load the class using Class.forName reflection.

  3. Get the Extension annotation of the class,
  • If it is null, throw an exception,
  • If not, get the value, which cannot be empty.
  • If there is no alias configured in the SPI file, use the one on the annotation.
  • If the SPI file is configured, whether the checksum annotations are consistent,
  • If inconsistent, throw an exception.
  • If the annotation declaration of the interface requires encoding and the implementation class is not configured, an exception is thrown.
  • Throws an exception if the alias is default or *.
  1. Check whether the current system already contains a class with the same alias. Note: here is an interface corresponding to a Map, so the verification here is relative to this interface, that is, checking the implementation of the same name (alias) of the current interface.
  • If there is the same name, and the current implementation class extension override is true (can be overridden), and the priority of the new implementation class is not as high as the old one, ignore the new one, otherwise, load the new class.
  • If the current implementation class extension cannot be overridden, judge if the old implementation class extension can be overridden, and the old priority is greater than or equal to the new one. The new one is ignored, on the contrary, if the old extension class cannot be overridden or the priority is lower than the new one, an existing exception is thrown, because the system does not know what to do (the new one cannot be overridden, the old one cannot be overridden and the priority is low).
  1. If there is no old one, directly load the new implementation class and create an extensionClass object.

  2. If the load is created successfully, check for mutually exclusive extension points and loop over all implementations cached in the interface.
  • If the priority of the current implementation class is greater than or equal to the existing one, check whether the new extension excludes the old one. This 排除扩展is an array of aliases. If so, delete the extensionClass in the cache cyclically.
  • If the priority of the current implementation class is lower than the existing one, check 排除扩展whether the existing one contains the current extension point. If it does, it will not be put into the cache.
  1. Call the loadSuccess method to put the newly created extensionClass and the corresponding alias into the all map, that is, the cache. If a listener is configured, call the listener's onLoad method to inform the listener: Loading is complete, please indicate!

Close the stream.

At this point, a complete extension point is loaded! ! !

Back to the getExtesionLoader method of the ExtensionLoaderFactory, the construction method ends, and the implementation class is successfully loaded from the SPI file through the interface name, and then what? Put the interface and the corresponding ExtensionLoader object into the cache and use it next time.

Finally, the ExtensionLoader object is returned.

Usually, this object's getExtension method is called right after. Something like this:

Person person = ExtensionLoaderFactory.getExtensionLoader(Person.class).getExtension("A");

Get instance by alias.

As you can guess, the ExtensionClass object corresponding to the alias must be obtained from the cache of this instance. If not, a Not Found exception is thrown.

Code:

public T getExtension(String alias) {
    ExtensionClass<T> extensionClass = getExtensionClass(alias);
    if (extensionClass == null) {
        throw new SofaRpcRuntimeException("Not found extension of " + interfaceName + " named: \"" + alias + "\"!");
    } else {
        if (extensible.singleton() && factory != null) {
            T t = factory.get(alias);
            if (t == null) {
                synchronized (this) {
                    t = factory.get(alias);
                    if (t == null) {
                        t = extensionClass.getExtInstance();
                        factory.put(alias, t);
                    }
                }
            }
            return t;
        } else {
            return extensionClass.getExtInstance();
        }
    }
}
  • If the interface identifier is a singleton and the cache is not empty, it will be taken from the cache. Double-checked lock is used here. After getting the ExtensionClass object, it corresponds to his getExtInstance method. The content of the method is to use reflection to create an object instance. And put the alias and the corresponding object into the cache.

  • Note: ExtensionLoader has 2 caches, one is the ConcurrentHashMap<String, ExtensionClass<T>> allcache is the alias and the corresponding ExtensionClass, indicating that an interface can have multiple implementations. Another is ConcurrentHashMap , this Map holds the singleton object corresponding to the alias.

  • If it's not a singleton, use reflection to create a new one.

Well, here, a complete object is created.

Summarize

Borrow from SOFA's official introduction to extension points:

> In order to have sufficient extensibility for all aspects of SOFARPC, SOFA-RPC defines a very flexible extension mechanism, and all extension implementations are equal.

This mechanism is very useful for both developers and users of SOFA-RPC itself. SOFA-RPC abstracts itself into multiple modules, each module has no explicit dependencies, and interacts through SPI.

SOFA's extension point does not use Java's SPI, but is extended using Java's design. for example:

  • aliases can be used,
  • can have priority (sorting),
  • can cover,
  • You can control whether it is a singleton,
  • whether to encode.
  • The file location can be customized.
  • Whether to exclude other extension points.

Compared with JDK's SPI, it is much more powerful. Worth learning.

All right. That's it for the design analysis of SOFA extension points.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325242840&siteId=291194637