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 use
ExtensionLoader
Rather thanServiceLoader
The main logic is encapsulated in this class - The configuration files are stored in different directories
META-INF/services
Dubbo 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