What is the SPI

Java itself provides a set of SPI mechanism. The essence of SPI is to configure the fully qualified name of the Interface implementation class in a file, and the Service loader reads the configuration file and loads the implementation class. In this way, the implementation class can be loaded at runtime. Dynamic substitution of implementation classes for interfaces is also a means by which many framework components implement extended functionality.

The Dubbo SPI mechanism is a bit different from the Java SPI. Instead of using Java’s native SPI mechanism, Dubbo has improved and enhanced it, making it easy to extend Dubbo’s functionality.

Learn things with questions to learn, let’s ask a few questions, and then look at

1. What is SPI (explained at the beginning)

2. What is the difference between Dubbo SPI and Java native

3. How should the two implementations be written

How is the Java SPI implemented

Define an interface:

public interface Car {
	void startUp(a);
}
Copy the code

Then create two classes that implement the Car interface

public class Truck implements Car{
	@Override
	public void startUp(a) {
		System.out.println("The truck started"); }}public class Train implements Car{
	@Override
	public void startUp(a) {
		System.out.println("The train started"); }}Copy the code

Then create a fully qualified name for the interface, com.example.demo.spi.car, under the project meta-INF /services folder.

The file contents write the fully qualified name of the implementation class as follows:

com.example.demo.spi.Train
com.example.demo.spi.Truck
Copy the code

Finally, write a test code:

public class JavaSPITest {
	@Test
	public void testCar(a) { ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class); serviceLoader.forEach(Car::startUp); }}Copy the code

The following output is displayed:

The train started
The truck started
Copy the code

How is Dubbo SPI implemented

The SPI Dubbo uses is not native to Java, but is a re-implementation of the ExtensionLoader class. The main logic is in the ExtensionLoader class, and the logic is not difficult, as we’ll see later

Interface classes need to be annotated with @spi annotations based on the previous example:

@SPI
public interface Car {
	void startUp(a);
}
Copy the code

The implementation class does not need to change

The configuration file needs to be placed under meta-INF /dubbo.

train = com.example.demo.spi.Train
truck = com.example.demo.spi.Truck
Copy the code

Finally, the test class, first look at the code:

public class JavaSPITest {
	@Test
	public void testCar(a) {
		ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
		Car car = extensionLoader.getExtension("train"); car.startUp(); }}Copy the code

Execution Result:

The train started
Copy the code

Common annotations in Dubbo SPI

  • @SPI is marked as an extension interface

  • Adaptive extends the implementation class flag

  • @activate Flag for automatic activation conditions

To summarize the differences:

  • Differences in use Dubbo useExtensionLoaderRather thanServiceLoaderThe main logic is encapsulated in this class
  • The configuration files are stored in different directoriesMETA-INF/servicesDubbo inMETA-INF/dubbo.META-INF/dubbo/internal
  • Java SPI instantiates all implementations of extension points at once, and if an extension implementation is time consuming to initialize and not needed, a lot of resources can be wasted
  • Dubbo SPI adds support for extension points IOC and AOP, where one extension point can directly setter for injection of other extension points
  • The Java SPI loading process failed and the name of the extension point is not available. Such as: JDK standard ScriptEngine, getName() to get the name of the script type, if RubyScriptEngine depends on jruby.jar does not exist, RubyScriptEngine class load failure, There is no indication of the cause of the failure. When the user executes ruby scripts, it will report that ruby is not supported, not the actual cause of the failure

Can you answer the three questions already? Isn’t that simple

Dubbo SPI source code analysis

Dubbo SPI uses the ExtensionLoader getExtensionLoader method to get an instance of ExtensionLoader. The extension class object is then obtained through the getExtension method of the ExtensionLoader. The getExtensionLoader method is used to retrieve the ExtensionLoader corresponding to the extended class from the cache. If there is no cache, a new instance is created and the code is directly attached:

public T getExtension(String name) {
    if (name == null || name.length() == 0) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        // Get the default extension implementation class
        return getDefaultExtension();
    }
    // Used to hold the target object
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // DCL
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // Create an extension instance
                instance = createExtension(name);
                // Set the instance to the holderholder.set(instance); }}}return (T) instance;
}
Copy the code

The main thing this code does is check the cache first. The cache does not create extension objects

Let’s look at the creation process:

private T createExtension(String name) {
    // Load all extension classes from the configuration file to obtain the mapping table from "configuration item name" to "configuration class"Class<? > clazz = getExtensionClasses().get(name);if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // reflection creates an instance
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // Inject dependencies into the instanceinjectExtension(instance); Set<Class<? >> wrapperClasses = cachedWrapperClasses;if(wrapperClasses ! =null && !wrapperClasses.isEmpty()) {
            // Loop to create the Wrapper instance
            for(Class<? > wrapperClass : wrapperClasses) {// Pass the current instance as an argument to the Wrapper constructor and create the Wrapper instance through reflection.
                // Then inject the dependency into the Wrapper instance, and finally assign the Wrapper instance to the instance variable againinstance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); }}return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                    type + ") couldn't be instantiated: "+ t.getMessage(), t); }}Copy the code

This code looks cumbersome, but it’s not too difficult, and it only does four things:

1. Use getExtensionClasses to get all configuration extension classes

2. Reflection creates objects

3. Inject dependencies into extended classes

4. Wrap the extension class object inside the corresponding Wrapper object

Before obtaining the extension class by name, we need to resolve the mapping relation table between the extension class name and the extension class according to the configuration file, and then extract the corresponding extension class from the mapping relation table according to the extension name. The code for the relevant process is as follows:

privateMap<String, Class<? >> getExtensionClasses() {// Get the loaded extended class from the cacheMap<String, Class<? >> classes = cachedClasses.get();// DCL
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // Load the extension classclasses = loadExtensionClasses(); cachedClasses.set(classes); }}}return classes;
}
Copy the code

In this case, the cache is checked first. If the cache does not exist, the cache is checked by a double lock and nulled. If classes are still null, the extended class is loaded through loadExtensionClasses. Here is the code for the loadExtensionClasses method

privateMap<String, Class<? >> loadExtensionClasses() {// Get the SPI annotation, where the type variable is passed in when the getExtensionLoader method is called
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if(defaultAnnotation ! =null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // Shards the SPI annotation content
            String[] names = NAME_SEPARATOR.split(value);
            // Check if the SPI annotation content is valid, throw an exception if it is not
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }

            // Set the default name, referring to the getDefaultExtension method
            if (names.length == 1) {
                cachedDefaultName = names[0]; } } } Map<String, Class<? >> extensionClasses =newHashMap<String, Class<? > > ();// Load the configuration file in the specified folder
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}
Copy the code

The loadExtensionClasses method does two things altogether: it parses SPI annotations and calls the loadDirectory method to load the specified folder configuration file. The SPI annotation parsing process is simple and needless to say. Let’s take a look at what loadDirectory does

private void loadDirectory(Map
       
        > extensionClasses, String dir)
       ,> {
    // fileName = folder path + type Fully qualified name
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        // Load all files with the same name according to the filename
        if(classLoader ! =null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if(urls ! =null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // Load resourcesloadResource(extensionClasses, classLoader, resourceURL); }}}catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t); }}Copy the code

The loadDirectory method first obtains all resource links through the classLoader and then loads the resources through the loadResource method. Let’s follow along and look at the implementation of the loadResource method

private void loadResource(Map
       
        > extensionClasses, ClassLoader classLoader, java.net.URL resourceURL)
       ,> {
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            // Read the configuration line by line
            while((line = reader.readLine()) ! =null) {
                // Position the # character
                final int ci = line.indexOf(The '#');
                if (ci >= 0) {
                    // Cut the string before #. The content after # is a comment and should be ignored
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // Cut off the key and value with the equal sign = bound
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // Load the class and cache the class using the loadClass method
                            loadClass(extensionClasses, resourceURL, 
                                      Class.forName(line, true, classLoader), name); }}catch (Throwable t) {
                        IllegalStateException e =
                          new IllegalStateException("Failed to load extension class..."); }}}}finally{ reader.close(); }}catch (Throwable t) {
        logger.error("Exception when load extension class..."); }}Copy the code

The loadResource method is used to read and parse configuration files, load classes through reflection, and finally call the loadClass method for other operations. The loadClass method is used primarily for operational caching. The logic of this method is as follows:

private void loadClass(Map
       
        > extensionClasses, java.net.URL resourceURL, Class
         clazz, String name)
       ,> throws NoSuchMethodException {
    
    if(! type.isAssignableFrom(clazz)) {throw new IllegalStateException("...");
    }

    // Check whether there are Adaptive annotations on the target class
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            // Set the cachedAdaptiveClass cache
            cachedAdaptiveClass = clazz;
        } else if(! cachedAdaptiveClass.equals(clazz)) {throw new IllegalStateException("...");
        }
        
    // Check whether clazz is a Wrapper type
    } else if(isWrapperClass(clazz)) { Set<Class<? >> wrappers = cachedWrapperClasses;if (wrappers == null) {
            cachedWrapperClasses = newConcurrentHashSet<Class<? > > (); wrappers = cachedWrapperClasses; }// Store clazz in the cachedWrapperClasses cache
        wrappers.add(clazz);
        
    Clazz is a common extension class
    } else {
        // Tests whether Clazz has a default constructor and throws an exception if it does not
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // If name is empty, try to get name from the Extension annotation, or use a lowercase class name as name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("..."); }}/ / shard name
        String[] names = NAME_SEPARATOR.split(name);
        if(names ! =null && names.length > 0) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if(activate ! =null) {
                // If there is an Activate annotation on the class, use the first element of the NAMES array as the key,
                // Store the mapping between name and the Activate annotation object
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if(! cachedNames.containsKey(clazz)) {// Store the mapping between classes and namescachedNames.put(clazz, n); } Class<? > c = extensionClasses.get(n);if (c == null) {
                    // Store the mapping between names and classes
                    extensionClasses.put(n, clazz);
                } else if(c ! = clazz) {throw new IllegalStateException("...");
                }
            }
        }
    }
}
Copy the code

To sum up, the loadClass method operates on different caches, such as cachedAdaptiveClass, cachedWrapperClasses, and cachedNames

Here basically about the cache class loading process is finished analysis, other logic is not difficult, read down and Debug can understand.

conclusion

In terms of design philosophy, SPI is an implementation of Demeter’s law and the open and close principle.

Open closed principle: Closed for modifications and open for extensions. This principle is common in many open source frameworks and is heavily used by Spring’s IOC container.

Demeter’s Law: also known as the principle of least knowledge, can be interpreted as, should not directly depend on the relationship between classes, do not rely on; Try to rely on only the necessary interfaces between classes that have dependencies.

Then why does Dubbo’s SPI not directly use Spring? This can be seen from many open source frameworks, because as an open source framework itself, it needs to be integrated into other frameworks or run together, and cannot exist as a dependent dependent object. Furthermore, for Dubbo, using Spring IOC AOP directly is a bit bloated and unnecessary, so implementing a set of lightweight solutions is optimal