Dubbo adopts the “microkernel + plug-in” approach to achieve the closure of extension development and modification, effectively coupling the framework kernel and extension. Dubbo extends the JDK’s native SPI mechanism to implement dependency injection, AOP, and on-demand instance creation. This article will start with the JDK’s native SPI mechanism and step up an in-depth analysis of the SPI mechanism in Dubbo. The SPI mechanism in Dubbo utilizes the policy pattern + factory pattern + singleton pattern + configuration file, and a deep understanding of its principle is conducive to the daily development of high quality code with higher scalability and lower coupling. In-depth analysis of Dubbo SPI mechanism implementation principle will be divided into two “in-depth analysis of Dubbo SPI mechanism implementation principle of practical application” and “in-depth analysis of Dubbo SPI mechanism implementation principle source interpretation”, “In-depth analysis of Dubbo SPI mechanism implementation principle of practical application” that is, this article focuses on the analysis of Dubbo SPI mechanism as a whole how to achieve and specific practical application, “in-depth analysis of Dubbo SPI mechanism implementation principle of source code interpretation” focuses on the combination of source code in-depth analysis.

1 SPI mechanism in JDK

Service Provider Interface (SPI) is a built-in Service definition and provision mechanism in JDK. Service definition is usually completed by authoritative organizations or agencies, that is, the definition of core interfaces related to services. Service provision is generally implemented by the third party vendors, that is, the concrete implementation of service-related core interfaces. The consumer of the service does not care about the specific implementation differences of different vendors, but implements the business logic according to the standard interface. The most representative of SPI is the JDBC specification, which provides the core interface definition for data operation in JDK, while database vendors such as mysql, SQL Server and Oraclet provide the Connector JAR implementation for specific implementation. Take JDBC as an example. The JDBC specification defines the Driver interface. Vendors such as mysql provide a specific Connector JAR containing a meta-INF /services directory containing a file named java.sql.Driver. The Driver interface is defined by the JDBC specification, and its content is the concrete implementation class of mysql vendor:

com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver

In the JDBC4 specification, a user can retrieve a connection to the mysql database by using the following code as long as the corresponding mysql connector is on the classpath.

Connection conn = 
  DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb"."test"."test");
Copy the code

Similarly, if it is SQL Server, you can obtain the Connection of THE SQL Server database by modifying the URL parameter corresponding to the Connection and changing the connector of classpath to SQL Server. In this way, the difference between different vendors in database Driver implementation is shielded, and the purpose of separation of concerns is achieved. As long as the business developer writes the business logic according to the JDBC specification, Of course ORM frameworks like Mybatis and Hiberante add more encapsulation to the JDBC specification to make operating databases more user-friendly.

1.1 How to use SPI in JDK

You can see that SPI as a whole can be divided into three steps: service definition, service provision, and service invocation

  • Service definitions mainly provide the definition and abstraction of interfaces for services or functions. The following code is the Repository service definition.

    package example;
    public interface Repository {
      void save(String data);
    }
    Copy the code
  • The service provides the interface defined in the service definition phase, and creates a meta-INF /services directory in the classpath with a file named the same as the interface defined (java.sql.driver as seen above). The content of the file is the class name of the concrete implementation class of the interface. If there are more than one concrete, the line is broken, and one line is the class name of the concrete implementation class. The following code is a different implementation of the Repository service and exposes the service according to SPI rules.

    package example;
    public class MysqlRepository implements Repository {
      @Override
      public void save(String data) {
        System.out.println(data + " saved by Mysql Repository!"); }}Copy the code
    package example;
    public class MongoDBRepository implements Repository {
      @Override
      public void save(String data) {
        System.out.println(data + " saved by MongoDB repository!"); }}Copy the code

    Meta-inf/services/example. The Repository file content is as follows:

    example.MongoDBRepository example.MysqlRepository

  • The service invocation refers to the specific Jar implemented by the above service provider via the classpath (which can be used directly if the service provision is in the same project as the service invocation), and then loads the corresponding service through the ServiceLoader#load method. The ServiceLoader#iterator method iterates through the available services.

    // Use the load method to load corresponding services
    ServiceLoader<Repository> serviceLoader = ServiceLoader.load(Repository.class);
    // Iterate through the available services
    Iterator<Repository> iterator = serviceLoader.iterator();
    while (iterator.hasNext()) {
      Repository repository = iterator.next();
      // Invoke the actual service
      repository.save("Data");
    }
    Copy the code
1.2 ANALYSIS of SPI core source in JDK

The essence of the JDK’s SPI is to get the concrete implementation class of an interface from a configuration file, then load the class with its fully qualified name and create an object with a no-argument constructor reflection. The following is a brief analysis of SPI’s core source code with the above examples.

//ServiceLoader#load loads the service
public static <S> ServiceLoader<S> load(Class<S> service) {
  // Get the current thread's ClassLoader. This ClassLoader is AppClassLoader
  ClassLoader cl = Thread.currentThread().getContextClassLoader();
  return ServiceLoader.load(service, cl);
}
//ServiceLoader$LazyIterator#nextService gets the next available service implementation, omits irrelevant code
private S nextService(a) { Class<? > c =null;
  /** Using the SPI of the Repository service above as an example, Variable cn is actually stored in the meta-inf/services/example. The name of the class in the Repository file example. MongoDBRepository or example. Every time MysqlRepository traversal service to get a * * /
  String cn = nextName;
	try {
    	// The loader parameter is the contextClassLoader obtained by contextClassLoader
	    c = Class.forName(cn, false, loader);
	} catch (ClassNotFoundException x) {
	  // omit irrelevant code...
	}
  // omit irrelevant code...
	try {
    	// Create an object by calling the class's no-argument constructor through reflection
	    S p = service.cast(c.newInstance());
	    providers.put(cn, p);
	    return p;
	} catch (Throwable x) {
    // omit irrelevant code...
	}
	throw new Error();
};
Copy the code

Through the analysis of the core source code, you can see the JDK native SPI mechanism is by obtaining a configuration file interface implementation class’s fully qualified class name, and then through the reflection to invoke parameterless constructor to create objects to implement the service definition and concrete realization of isolation, but for like Dubbo like RPC framework JDK native SPI mechanism, Seems too simple and rough. Although the SPI mechanism implicitly creates objects for us using reflection, objects can only be created through a no-argument constructor. If SPI had dependency injection, AOP, and load on demand instances like Spring does, wouldn’t the Dubbo framework’s desire to separate core interface definition from implementation (” microkernel + plug-in “) be better served? The actual SPI mechanism that Dubbo has extended implements dependency injection, AOP, and load instances on demand. I can see Spring in Dubbo’s SPI mechanism.

Overview of SPI mechanism in Dubbo

With an overview of the JDK’s native SPI above, it should be relatively easy to understand the SPI mechanism in Dubbo. The core of Dubbo’s extended SPI mechanism is to solve three things: how to create instances on demand, how to implement dependency injection, and how to implement AOP. In addition to these three things, Dubbo’s SPI mechanism also provides a strong adaptive extension mechanism. Before we analyze the SPI mechanism in Dubbo, we can think about how to achieve these three things. This section takes a look at how to use the various capabilities provided by the SPI mechanism in Dubbo as a whole, followed by an in-depth analysis of the source code.

Note: The following analysis is based on Dubo-2.6.4

2.1 Basic usage of SPI in Dubbo

The SPI mechanism in Dubbo also parses the configuration file to get the concrete implementation class of the interface, then performs object creation and dependency injection through reflection, and implements AOP functions by detecting the presence of Wrapper classes during object creation (essentially SPI in Dubbo implements AOP through static proxies). Like the JDK’s SPI mechanism, you can also define the entire service to the end, breaking it down into three big steps.

1. Define services

Define the core interface of the service and annotate the service interface with @SPI annotations. If the service wants to implement Adaptive extension, it can annotate the interface method with @Adaptive annotations or the interface implementation class with @Adaptive annotations (this actual attribute provides the service part).

2. Provide services

Implement the service interface and create a configuration file named the service interface name under the meta-INF /dubbo/internal/ or meta-INF /dubbo/ or meta-INF /services/ directory. The contents of the configuration file are key-value pairs, key is the key of the extended implementation class, and value is the fully qualified class name of the extended implementation class. If the Adaptive expansion function is to be realized, @adaptive annotation can be added to the class. The following is the Dubbo com.alibaba.dubbo.common.compiler.Com piler interface implementation class corresponding to the specific content of the configuration file.

adaptive=com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler
jdk=com.alibaba.dubbo.common.compiler.support.JdkCompiler
javassist=com.alibaba.dubbo.common.compiler.support.JavassistCompiler
Copy the code

3. Invoke the service

Similar to the JDK’s SPI mechanism, service instances are created and invoked by a core class, called ExtensionLoader in Dubbo. Get the corresponding Instance of ExtensionLoader by using the ExtensionLoader#getExtensionLoader method, Then call the ExtensionLoader#getExtension method with the Extension name to get the specific interface implementation class, and finally call the method defined by the interface to complete the service call. The following shows how to get the JavassistCompiler implementation in Dubbo’s Compiler implementation Class via ExtensionLoader, and then call the Compiler#compile method to compile the source code into bytecode and load it into the JVM to generate the corresponding Class instance.

ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
/ / javassist for com.alibaba.dubbo.common.compiler.Com piler file JavassistCompiler implementation of the Key
Compiler compiler = loader.getExtension("javassist");
compiler.compile("resource code ...", Thread.currentThread().getContextClassLoader());
Copy the code

In fact, the above process of calling the service is implemented through the policy mode. When the Extension name is first passed, ExtensionLoader will find the corresponding restricted class name in the configuration file, load the class into the JVM, and then create the object through reflection. Once created, the created implementation is cached in the global ConcurrentMap, completing the creation of instances on demand.

2.2 SPI adaptive extended usage in Dubbo

Dependency injection and AOP in Spring are the core essence of the framework, as are dependency injection and AOP in Dubbo’s SPI. We’ll talk about adaptive extension first when we talk about how to use SPI adaptive extension in Dubbo, because Dubbo uses adaptive extension in dependency injection. What is adaptive extension? The simple idea is that when an extension interface method is called, a proxy class is called, and then the proxy class decides through policy mode which one dynamically creates the extension implementation and ultimately calls its methods based on the parameters passed in at run time. It looks a bit like a normal policy pattern, with different parameters and then locating to a specific implementation of the interface, but in fact Dubbo’s ADAPTIVE extension of SPI is a bit smarter. Dubbo first generates proxy-specific code for the extension interface based on the parameters passed in when the extension interface is called, then compiles this code via JavAssist or JDK and loads the corresponding classes into the JVM, and finally creates a proxy instance of the extension interface through reflection. Inside the proxy instance, a common policy pattern is used to determine which extension implementation to call according to the parameters passed in when the extension interface is called.

As mentioned above, Dubbo’s SPI mechanism determines which specific extension implementation class to use based on the parameters passed in when calling the extension interface. In order to form a unified contract mechanism, Dubbo defines the URL model to complete the transfer and acquisition of core parameters in the whole framework. Uniform Resource Locators: Uniform Resource Locators: Uniform Resource Locators: Uniform Resource Locators: Uniform Resource Locators: Uniform Resource Locators When using adaptive extension, methods to be declared by the interface must include parameters of type URL or getURL methods to get the URL, otherwise the runtime will throw an exception. The main reason to get the URL from the parameter list of the method declared by the interface is to use the URL to get the key of the extension class, which is to determine which extension implementation to call.

The difference between the use of adaptive extension and the basic use of SPI in Dubbo is that the @adapitve annotation is required on the method, and the method parameter list should have a URL type parameter or the URL can be obtained through the getURL method of the parameter. As the @adapitve annotation will explain in detail when analyzing the source, @adapitve can be annotated on a method or class. The annotation method means that Dubbo will automatically generate an adaptive class for it (in effect, a proxy class), and the annotation on a class means that the adaptive class is manually implemented. Only two classes within Dubbbo are Adaptive annotated, namely AdaptiveExtensionFactory and AdaptiveCompiler. To get a more intuitive understanding of adaptive expansion through an example.

package org.knight;
@SPI
public interface EngineMaker {
    @Adapitve
    Engine makeEngine(URL url);
}
Copy the code

Dubbo generates code for specific proxy functions for the EngineMaker interface. Here is the proxy class code generated by Dubbo for the EngineMaker interface:

package org.knight;
public class EngineMaker$Adaptive implements EngineMaker {
    public Wheel makeEngine(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
    	  // 1. Get the EngineMaker name from the URL
        String engineMakerName = url.getParameter("engine.maker");
        if (engineMakerName == null) {
            throw new IllegalArgumentException("engineMakerName == null");
        }
        // 2. Load specific EngineMaker via SPI
        EngineMaker engineMaker = ExtensionLoader
    		.getExtensionLoader(EngineMaker.class).getExtension(engineMakerName);
        // 3. Call the target method
        returnengineMaker.makeEngine(url); }}Copy the code

EngineMakerAdaptive is a proxy class, EngineMakerAdaptive is a proxy class, EngineMakerAdaptive will proxy the EngineMaker#makeEngine method, inside the makeEngine method, using the parameter URL to get the extension of the specific EngineMaker implementation (key in the SPI mechanism configuration file in Dubbo). The concrete EngineMaker implementation class is loaded through SPI, and the makeEngine method that implements the class instance is called.

2.3 SPI dependency injection and AOP usage in Dubbo

When the SPI mechanism in Dubbo is used to call a specific extension implementation, the SPI mechanism in Dubbo will check all the public setXXX methods that have only one parameter inside the specific extension implementation class after loading and creating the instance. Then try to get the corresponding extension implementation through ExtensionFactory and complete dependency injection. Let’s take a look at a detailed example. The extended class Electricdefiance internally relies on EngineMaker and exposes the setEngineMaker method so that Dubbo can inject the dependent EngineMaker.

/ / Engine interface
package org.knight;
public interface Engine {}
/ / FuelEngine implementation
package org.knight;
public class FuelEngine implements Engine {
  public FuelEngine(a) {System.out.println("FuelEngine implementation!");}
}
/ / ElectricEngine implementation
package org.knight;
public class ElectricEngine implements Engine {
  public ElectricEngine(a) { System.out.println("ElectricEngine implementation!");}
}
/ / Car interface
package org.knight;
public interface Car {
  void run(a);
}
/ / ElectricCar implementation
package org.knight;
public class ElectricCar implements Car {
  private final Engine engine;
  public ElectricCar(Engine engine) {
    this.engine = engine;
    System.out.println("ElectricCar implementation!");
  }
  public void run(a) {
    System.out.println("ElectricCar run with "+ engine.getClass()); }}/ / the CarMaker interface
package org.knight;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.SPI;
@SPI
public interface CarMaker {
  Car makeCar(URL url);
}
/ / ElectricCarMaker implementation
package org.knight;
import org.apache.dubbo.common.URL;
public class ElectricCarMaker implements CarMaker {
  public EngineMaker engineMaker;
  @Override
  public ElectricCar makeCar(URL url) {
    return new ElectricCar(engineMaker.makeEngine(url));
  }
  public void setEngineMaker(EngineMaker engineMaker) {
    System.out.println("Inject property engineMaker by Dubbo SPI!");
    this.engineMaker = engineMaker; }}/ / EngineMaker interface
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
@SPI
public interface EngineMaker {
  // Indicates that the URL model passes in a parameter named engineMakerType or engineType
  @Adaptive({"engineMakerType", "engineType"})
  Engine makeEngine(URL url);
}
/ / ElectricEngineMaker implementation
package org.knight;
import org.apache.dubbo.common.URL;
public class ElectricEngineMaker implements EngineMaker {
  @Override
  public ElectricEngine makeEngine(URL url) {return new ElectricEngine();}
}
/ / FuelEngineMaker implementation
package org.knight;
import org.apache.dubbo.common.URL;
public class FuelEngineMaker implements EngineMaker {
  @Override
  public FuelEngine makeEngine(URL url) {return new FuelEngine();}
}
Copy the code

In meta-INF /services there are two config files org.knight

electricCarMaker=org.knight.ElectricCarMaker

Org. Knight. EngineMaker for content

electricEngineMaker=org.knight.ElectricEngineMaker fuelEngineMaker=org.knight.FuelEngineMaker

The code for the main function that performs the operation is as follows

/ / the main function
package org.knight;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Main {
  public static void main(String args[]) {
    //1. Obtain the corresponding ExtensionLoader implementation
    ExtensionLoader<CarMaker> extensionLoader = ExtensionLoader.getExtensionLoader(CarMaker.class);
    //2. Load and create the corresponding ElectricEngineMaker with the name of the extension class (key in the configuration file).
    CarMaker carMaker = extensionLoader.getExtension("electricCarMaker");
    //3. Pass the engineMakerType parameter to electricEngineMaker through the URL
    URL url = new URL(""."".8080).addParameter("engineMakerType"."electricEngineMaker");
    // call the ElectricEngineMaker#makeCar method.
    Car car = carMaker.makeCar(url);
    // execute the Car#run methodcar.run(); }}Copy the code

The above code completes with the following result

Inject property engineMaker by Dubbo SPI! ElectricEngine implementation! ElectricCar implementation! ElectricCar run with class org.knight.ElectricEngine

It can be seen that the extended class Electricdefiance of Dubbo injected the dependent EngineMaker object, and the type of EngineMaker that implements the injection is ElectricEngineMaker instead of FuelEngineMaker. The key code snippet above is

@SPI
public interface EngineMaker {
  // Indicates that the URL model passes in a parameter named engineMakerType or engineType
  @Adaptive({"engineMakerType", "engineType"})
  Engine makeEngine(URL url);
}
Copy the code

with

//3. Pass the engineMakerType parameter through the URL. EngineMakerType = electricEngineMaker URL URL = new URL("", "", 8080). AddParameter ("engineMakerType", "electricEngineMaker");Copy the code

The @adaptive annotation of engineMakerType and engineType specifies that the parameters engineMakerType or engineType should be used to obtain EngineMaker self-adaption in the URL model The extension that should be extended. If the value of parameter engineMakerType in the URL model is not empty, Dubbo obtains this parameter value from the URL first as the extension. Otherwise, Dubbo will obtain the engineType parameter from the URL model. If the value of engineType parameter in the URL model is not empty, Dubbo will obtain the engineType parameter from the URL as the extension. Finally, Dubbo will take the default extension, which is the value of the @spi annotation, as the extension for the bottom of the pocket. Of course, the default extension has to exist. @spi (“electricEngineMaker”) stands for electricEngineMaker as the default extension. When all the circumstances do not match, Dubbo will not be able to get the extension and will not be able to complete the dependency injection. Dubbo get EngineMaker corresponding extension, will from the meta-inf/services/org. Knight. EngineMaker configuration file with extension to find specific fully qualified class name, and class is loaded into the JVM by reflection to create objects at the same time, Then inject the ElectricEngineMaker instance into Electrichum.

What if the @adaptive annotation on the EngineMaker#makeEngine method has a null value? Dubbo then converts EngineMaker’s scheduled internal rules to engine.maker as the parameter for the EXTENSION in the URL, and then uses that parameter, engine.maker, to get the value from the RUL model as the extension. Combine the annotation on @adaptive annotation value() to experience the above analysis.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};
}
Copy the code

String[] value() default {}; The comments above are as follows

Decide which target extension to be injected. The name of the target extension is decided by the parameter passed in the  URL, and the parameter names are given bythis method.
If the specified parameters are not found from URL, then the default extension will be used for dependency injection (specified in its interface's SPI).
For example, given String[] {"key1", "key2"}:
find parameter 'key1' in URL, use its value as the extension's name
try 'key2' for extension's name if 'key1' is not found (or its value is empty) in URL
use default extension if 'key2' doesn't exist either
otherwise, throw IllegalStateException
If the parameter names are empty, then a default parameter name is generated from interface's class name with the rule: divide classname from capital char into several parts, and separate the parts with dot '.', for example, for org.apache.dubbo.xxx.YyyInvokerWrapper, the generated name is String[] {"yyy.invoker.wrapper"}.
Returns:parameter names in URL
Copy the code

See the flow chart below to see how to get the extension from the URL model and then load the class with the extension. The core goal of the actual whole process is to determine the parameter name in the URL model to get the extension, that is, what parameter to get the extension from the URL model.

Having examined how the SPI mechanism in Dubbo can be used for dependency injection, let’s look at how AOP in Dubbo’s SPI mechanism can be used. AOP in Spring is essentially a proxy, and AOP in Dubbo’s SPI mechanism is essentially a proxy, except that the proxy is static, meaning that the proxy class is created first and then enhanced before and after the actual class is executed. Here’s how to create EngineMaker’s proxy class, or AOP execution class.

package org.knight;
import org.apache.dubbo.common.URL;
public class EngineMakerWrapper implements EngineMaker {
  private final EngineMaker engineMaker;
  // The public constructor argument declaration must have the enhanced interface as the only argument, which is the entry point for Dubbo implementing AOP
  public EngineMakerWrapper(EngineMaker engineMaker) {
    this.engineMaker = engineMaker;
  }
  @Override
  public Engine makeEngine(URL url) {
    System.out.println("Before Advice by EngineMakerWrapper!");
    Engine engine = engineMaker.makeEngine(url);
    System.out.println("After Advice by EngineMakerWrapper!");
    returnengine; }}Copy the code

The AOP class EngineMakerWrapper proxies EngineMaker and adds simple enhancements before and after executing the corresponding makeEngine methods. Now, as long as in the original meta-inf/services/org. Knight. EngineMaker configuration file new line of the following configuration can make effective AOP functionality.

engineMakerWrapper=org.knight.EngineMakerWrapper

Then execute main again to get the following output.

Inject property engineMaker by Dubbo SPI! Before Advice by EngineMakerWrapper! ElectricEngine implementation! After Advice by EngineMakerWrapper! ElectricCar implementation! ElectricCar run with class org.knight.ElectricEngine

The proxy class corresponding to the SPI mechanism of Dubbo must have a common constructor, and must have an enhanced interface as the only argument when building function parameter declarations. Otherwise, Dubbo cannot implement AOP, and the proxy class must be included in the configuration file. Although the AOP corresponding proxy class is configured in the configuration file through key-value pairs, the corresponding implementation cannot be obtained by extension as normal extension classes can.

ExtensionLoader<EngineMaker> loader = ExtensionLoader.getExtensionLoader(EngineMaker.class);
* * * / meta-inf/services/org. Knight. EngineMaker configuration files have corresponding * engineMakerWrapper = org. Knight. EngineMakerWrapper, EngineMaker is null **/
EngineMaker engineMaker = loader.getExtension("engineMakerWrapper");
Copy the code

The above code will throw an exception indicating No such extension org.knight.EngineMaker by name engineMakerWrapper

Note: It is actually possible to just write value in a configuration file instead of extensionName=extensionImpl, which might make more sense with AOP. So the configuration file just need to add the following line.

org.knight.EngineMakerWrapper

conclusion

This paper introduces the SPI mechanism in Dubbo step by step from the original SPI mechanism in JDK, and shows how to use the dependency injection, AOP, adaptive extension of SPI mechanism in Dubbo through examples. This paper focuses on the analysis of how to determine the extension class to be acquired from URL model and Adaptive extension annotation @adaptive. The next chapter will be combined with the source code in-depth analysis of dependency injection, AOP, adaptive expansion is how to achieve. Hope to finish reading this article you will gain, limited to my limited ability in the article is not correct hope correction. Finally, welcome to pay attention to personal public insight source code.

reference

Unified URL model in Dubbo

Dubbo SPI

SPI adaptive expansion

At the end

Original is not easy, praise, look, forwarding is a great encouragement to me, concern about the public number insight source code is my biggest support. At the same time believe that I will share more dry goods, I grow with you, I progress with you.