Dubbo: deep understanding of Dubbo's service discovery SPI mechanism

I. Preface

When using microservices, we have to talk about the topic of service discovery. Generally speaking, it means that the service provider registers the service in the registration center and tells the service consumer that the service already exists. Now let's look at the SPI mechanism in Dubbo

2, Introduction to SPI

The full name of SPI is Service Provider Interface, which is a service discovery mechanism. The essence of SPI is to configure the fully qualified name of the interface implementation class in the file, and the service loader reads the configuration file and loads the implementation class, so that the runtime can dynamically replace the implementation class for the interface

3, SPI in Dubbo

Dubbo is different from the above common Java way to implement SPI. In Dubbo, a set of more powerful SPI mechanism is re implemented, that is, through the key value pair to configure and cache. Concurrent HashMap and synchronize are also used to prevent concurrency problems. The main logic is encapsulated in the extension loader. Let's look at the source code.

4, Extension loader source code analysis

Because there are so many internal methods, we only select and implement the important logic part of SPI to explain.   

  1,getExtensionLoader(Class<T> type)

 1 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
 2         if (type == null) {
 3             throw new IllegalArgumentException("Extension type == null");
 4         } else if (!type.isInterface()) {
 5             throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
 6         } else if (!withExtensionAnnotation(type)) {
 7             throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
 8         } else {
 9             ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
10             if (loader == null) {
11                 EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
12                 loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
13             }
14 
15             return loader;
16         }
17     }

This is to convert the corresponding interface to an ExtensionLoader instance. It's equivalent to telling Dubbo that this is a service interface with corresponding service providers

First, the class passed in by logical judgment cannot be empty. It must be an interface and annotated by @ SPI annotation. If all three conditions are met, the ExtensionLoader instance will be created. Similarly, if the current class has been created an ExtensionLoader instance, take it directly. Otherwise, create a new one. The storage type of key value pair is used here, as shown below:

 

 

Use ConcurrentHashMap to prevent problems in concurrency, and there are many efficient hashtables, so we should also use ConcurrentHashMap for storage in our daily project concurrency scenarios.

 

  2,getExtension(String name)

 1 public T getExtension(String name) {
 2     if (name == null || name.length() == 0)
 3         throw new IllegalArgumentException("Extension name == null");
 4     if ("true".equals(name)) {
 5         // Get the default extension implementation class
 6         return getDefaultExtension();
 7     }
 8     // Holder,As the name implies, it is used to hold the target object
 9     Holder<Object> holder = cachedInstances.get(name);
10     if (holder == null) {
11         cachedInstances.putIfAbsent(name, new Holder<Object>());
12         holder = cachedInstances.get(name);
13     }
14     Object instance = holder.get();
15     // duplication check
16     if (instance == null) {
17         synchronized (holder) {
18             instance = holder.get();
19             if (instance == null) {
20                 // Create an extension instance
21                 instance = createExtension(name);
22                 // Set instance to holder in
23                 holder.set(instance);
24             }
25         }
26     }
27     return  instance;
28 }

This method is mainly equivalent to getting specific services. We have loaded the service interface above. Now we need to call a specific service implementation class under the service interface. Use this method. As can be seen from the above method, it will enter getOrCreateHolder, which means to get or create the Holder as the name implies. Go to the following methods:

 1 private Holder<Object> getOrCreateHolder(String name) {
 2         //Check for presence in cache
 3         Holder<Object> holder = (Holder)this.cachedInstances.get(name);
 4         if (holder == null) {
 5         //Create a new one if it doesn't exist in the cache Holder
 6             this.cachedInstances.putIfAbsent(name, new Holder());
 7             holder = (Holder)this.cachedInstances.get(name);
 8         }
 9 
10         return holder;
11     }

Similarly, the cache pool also uses ConcurrentHashMap as the storage structure

 

 

   3,createExtension(String name)

In fact, the getExtension method is not always available. The current method is required when the service implementation class is loaded for the first time

 1 private T createExtension(String name) {
 2         Class<?> clazz = (Class)this.getExtensionClasses().get(name);
 3         if (clazz == null) {
 4             throw this.findException(name);
 5         } else {
 6             try {
 7                 T instance = EXTENSION_INSTANCES.get(clazz);
 8                 if (instance == null) {
 9                     EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
10                     instance = EXTENSION_INSTANCES.get(clazz);
11                 }
12 
13                 this.injectExtension(instance);
14                 Set<Class<?>> wrapperClasses = this.cachedWrapperClasses;
15                 Class wrapperClass;
16                 if (CollectionUtils.isNotEmpty(wrapperClasses)) {
17                     for(Iterator var5 = wrapperClasses.iterator(); var5.hasNext(); instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance))) {
18                         wrapperClass = (Class)var5.next();
19                     }
20                 }
21 
22                 return instance;
23             } catch (Throwable var7) {
24                 throw new IllegalStateException("Extension instance (name: " + name + ", class: " + this.type + ") couldn't be instantiated: " + var7.getMessage(), var7);
25             }
26         }
27     }

You can see that createExtension is actually a private method, which is automatically triggered by getExtension above. The internal logic is roughly as follows:

3.1. Get all extension classes through getExtensionClasses

3.2 creating extension objects through reflection

3.3. Inject dependency into extension objects (Dubbo has a separate IOC, which will be introduced later)

3.4. Wrap the extension object in the corresponding Wrapper object

  4,getExtensionClasses()

  1 private Map<String, Class<?>> getExtensionClasses() {
  2     // Get loaded extension classes from cache
  3     Map<String, Class<?>> classes = cachedClasses.get();
  4     // duplication check
  5     if (classes == null) {
  6         synchronized (cachedClasses) {
  7             classes = cachedClasses.get();
  8             if (classes == null) {
  9                 // Load extension class
 10                 classes = loadExtensionClasses();
 11                 cachedClasses.set(classes);
 12             }
 13         }
 14     }
 15     return classes;
 16 }
 17 
 18 //Enter into loadExtensionClasses in
 19 
 20 private Map<String, Class<?>> loadExtensionClasses() {
 21     // Obtain SPI Note, here type Variable is calling getExtensionLoader Method passed in
 22     final SPI defaultAnnotation = type.getAnnotation(SPI.class);
 23     if (defaultAnnotation != null) {
 24         String value = defaultAnnotation.value();
 25         if ((value = value.trim()).length() > 0) {
 26             // Yes SPI Note content segmentation
 27             String[] names = NAME_SEPARATOR.split(value);
 28             // Testing SPI If the annotation content is legal, throw an exception if it is not in accordance with the law
 29             if (names.length > 1) {
 30                 throw new IllegalStateException("more than 1 default extension name on extension...");
 31             }
 32 
 33             // Set default name, reference getDefaultExtension Method
 34             if (names.length == 1) {
 35                 cachedDefaultName = names[0];
 36             }
 37         }
 38     }
 39 
 40     Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
 41     // Load the profile under the specified folder
 42     loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
 43     loadDirectory(extensionClasses, DUBBO_DIRECTORY);
 44     loadDirectory(extensionClasses, SERVICES_DIRECTORY);
 45     return extensionClasses;
 46 }
 47 
 48 //Enter into loadDirectory in
 49 
 50 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
 51     // fileName = Folder path + type Fully qualified name 
 52     String fileName = dir + type.getName();
 53     try {
 54         Enumeration<java.net.URL> urls;
 55         ClassLoader classLoader = findClassLoader();
 56         // Load all files with the same name according to the file name
 57         if (classLoader != null) {
 58             urls = classLoader.getResources(fileName);
 59         } else {
 60             urls = ClassLoader.getSystemResources(fileName);
 61         }
 62         if (urls != null) {
 63             while (urls.hasMoreElements()) {
 64                 java.net.URL resourceURL = urls.nextElement();
 65                 // load resources
 66                 loadResource(extensionClasses, classLoader, resourceURL);
 67             }
 68         }
 69     } catch (Throwable t) {
 70         logger.error("...");
 71     }
 72 }
 73 
 74 //Enter into loadResource in
 75 
 76 private void loadResource(Map<String, Class<?>> extensionClasses, 
 77     ClassLoader classLoader, java.net.URL resourceURL) {
 78     try {
 79         BufferedReader reader = new BufferedReader(
 80             new InputStreamReader(resourceURL.openStream(), "utf-8"));
 81         try {
 82             String line;
 83             // Read configuration content by line
 84             while ((line = reader.readLine()) != null) {
 85                 // Location # character
 86                 final int ci = line.indexOf('#');
 87                 if (ci >= 0) {
 88                     // Intercept # Previous string,# The following content is a comment, which needs to be ignored
 89                     line = line.substring(0, ci);
 90                 }
 91                 line = line.trim();
 92                 if (line.length() > 0) {
 93                     try {
 94                         String name = null;
 95                         int i = line.indexOf('=');
 96                         if (i > 0) {
 97                             // By equals sign = Bound, intercept key and value
 98                             name = line.substring(0, i).trim();
 99                             line = line.substring(i + 1).trim();
100                         }
101                         if (line.length() > 0) {
102                             // Load the class and pass the loadClass Method to cache the class
103                             loadClass(extensionClasses, resourceURL, 
104                                       Class.forName(line, true, classLoader), name);
105                         }
106                     } catch (Throwable t) {
107                         IllegalStateException e = new IllegalStateException("Failed to load extension class...");
108                     }
109                 }
110             }
111         } finally {
112             reader.close();
113         }
114     } catch (Throwable t) {
115         logger.error("Exception when load extension class...");
116     }
117 }
118 
119 //Enter into loadClass in
120 
121 private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, 
122     Class<?> clazz, String name) throws NoSuchMethodException {
123     
124     if (!type.isAssignableFrom(clazz)) {
125         throw new IllegalStateException("...");
126     }
127 
128     // Check whether there is any on the target class Adaptive annotation
129     if (clazz.isAnnotationPresent(Adaptive.class)) {
130         if (cachedAdaptiveClass == null) {
131             // Set up cachedAdaptiveClass cache
132             cachedAdaptiveClass = clazz;
133         } else if (!cachedAdaptiveClass.equals(clazz)) {
134             throw new IllegalStateException("...");
135         }
136         
137     // Testing clazz Whether it is Wrapper type
138     } else if (isWrapperClass(clazz)) {
139         Set<Class<?>> wrappers = cachedWrapperClasses;
140         if (wrappers == null) {
141             cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
142             wrappers = cachedWrapperClasses;
143         }
144         // storage clazz reach cachedWrapperClasses Cache
145         wrappers.add(clazz);
146         
147     // The program enters this branch, indicating clazz It's a common extension class
148     } else {
149         // Testing clazz Is there a default constructor? If not, an exception will be thrown
150         clazz.getConstructor();
151         if (name == null || name.length() == 0) {
152             // If name If it is empty, try to Extension Get in comments name,Or use a lowercase class name as the name
153             name = findAnnotationName(clazz);
154             if (name.length() == 0) {
155                 throw new IllegalStateException("...");
156             }
157         }
158         // segmentation name
159         String[] names = NAME_SEPARATOR.split(name);
160         if (names != null && names.length > 0) {
161             Activate activate = clazz.getAnnotation(Activate.class);
162             if (activate != null) {
163                 // If there is Activate Annotation, use names The first element of the array is the key,
164                 // storage name reach Activate Mapping of annotation objects
165                 cachedActivates.put(names[0], activate);
166             }
167             for (String n : names) {
168                 if (!cachedNames.containsKey(clazz)) {
169                     // storage Class Mapping to name
170                     cachedNames.put(clazz, n);
171                 }
172                 Class<?> c = extensionClasses.get(n);
173                 if (c == null) {
174                     // Store name to Class Mapping of
175                     extensionClasses.put(n, clazz);
176                 } else if (c != clazz) {
177                     throw new IllegalStateException("...");
178                 }
179             }
180         }
181     }
182 }

There are many ways to do this. Make a logic:

1. getExtensionClasses(): check the cache first. If the cache fails to hit, lock it through synchronized. After locking, check the cache again and judge whether it is empty. At this time, if the classes are still null, the extension class is loaded through loadExtensionClasses.

2, loadExtensionClasses(): parse the interface of SPI annotation, then call the loadDirectory method to load the specified folder configuration file.

3. loadDirectory(): method first obtains all resource links through classLoader, and then loads resources through loadResource method.

4, loadResource(): used to read and parse configuration files, and through reflection loading class, and finally call loadClass method for other operations. The loadclass method is used primarily for operation caching.

5. Summary:

Let's take a look at how Dubbo performs SPI, that is, the implementation class of the discovery interface. First, you need to instantiate the extension class loader. In order to better fit with microservices, we call it service loader. The cache structure of ConcurrentHashMap is used in the service loader. In the process of looking for services, Dubbo first loads classes through reflection, and then configures the corresponding folders and files with the implementation classes (i.e. service providers) of interfaces (i.e. service interfaces) represented by @ SPI. Save the configuration file in the cache in the form of key value pairs. Key is the name of the class under the current service interface, and value is the corresponding class configuration file generated by Dubbo. Convenient for our next call. In order to prevent concurrency problems, concurrent HashMap and synchronize keyword are used to double check nodes with concurrency problems.

5, IOC in Dubbo

It was mentioned in the createExtension to inject extended objects into dependencies. Here is the inject extension (t instance):

 1 private T injectExtension(T instance) {
 2     try {
 3         if (objectFactory != null) {
 4             // All methods of traversing the target class
 5             for (Method method : instance.getClass().getMethods()) {
 6                 // Whether the detection method is set At the beginning, the method has only one parameter, and the access level of the method is public
 7                 if (method.getName().startsWith("set")
 8                     && method.getParameterTypes().length == 1
 9                     && Modifier.isPublic(method.getModifiers())) {
10                     // Obtain setter Method parameter type
11                     Class<?> pt = method.getParameterTypes()[0];
12                     try {
13                         // Get the property name, such as setName Method corresponding property name name
14                         String property = method.getName().length() > 3 ? 
15                             method.getName().substring(3, 4).toLowerCase() + 
16                                 method.getName().substring(4) : "";
17                         // from ObjectFactory Get dependent objects in
18                         Object object = objectFactory.getExtension(pt, property);
19                         if (object != null) {
20                             // Call by reflection setter Method set dependency
21                             method.invoke(instance, object);
22                         }
23                     } catch (Exception e) {
24                         logger.error("fail to inject via method...");
25                     }
26                 }
27             }
28         }
29     } catch (Exception e) {
30         logger.error(e.getMessage(), e);
31     }
32     return instance;
33 }

In the above code, the objectFactory variable is of type adaptive ExtensionFactory, which maintains a list of extensionfactories internally for storing other types of extensionfactories. Dubbo currently provides two kinds of ExtensionFactory, namely SpiExtensionFactory and SpringExtensionFactory. The former is used to create adaptive extensions, and the latter is used to obtain the required extensions from Spring's IOC container. This is why we often say that Dubbo can be seamlessly connected with Spring, because the underlying layer of Dubbo depends on Spring, and the IOC container of Spring can be used directly.

Six, summary

If you want to continue to dig deep into the source code of the framework, you can think more about where to use synchronize, why to use it, and what concurrency problems will occur if you don't use it. Dubbo's service discovery just lays the foundation for us to learn the Dubbo framework later, at least let us know how Dubbo conducts service discovery.

Tags: Java Dubbo Spring

Posted on Mon, 10 Feb 2020 06:45:43 -0500 by Grant Cooper