1. Introduction
Dubbo service framework adopts the design principle of “microkernel + plug-in”. The core function points of Dubbo itself are also realized through extension points, which means that almost all function points of Dubbo can be expanded and replaced by users’ customization, which greatly improves the high scalability of Dubbo framework itself. For example, if you don’t like Dubbo’s built-in object serialization method, you can always customize one. If Netty doesn’t work well for you, you can always customize one.
The full name of SPI is “Service Provider Interface”. Before introducing Dubbo SPI, let’s take a look at Java’s own SPI mechanism.
Java SPI was originally provided to manufacturers for plug-in development, such as database-driven java.sql.Driver. There are a variety of databases on the market, and the underlying protocols of different databases are different. In order to facilitate developers to call databases without caring about the differences between them, Therefore, a unified interface must be provided to regulate and constrain these databases. With a unified interface, database vendors can follow the specification to develop their own database drivers.
The manufacturer has developed a database driver, how to use the application? Which driver should I use? Take MySQL as an example. In the early days of writing JDBC by hand, developers had to register drivers manually.
2. Java SPI
Java SPI uses a policy pattern, in which multiple implementations of an interface are programmed to the interface by the developer. Implementations are not hardcoded directly in the program, but are configured through external files.
The Java SPI agrees on a specification that can be used as follows:
- Write an interface.
- Write concrete implementation classes.
- Under the background of the ClassPath
META-INF/services
Directory creates a file named with the fully qualified name of the interface. The content of the file is the fully qualified name of the implementation class. Multiple implementations are separated by a newline character. - Get the concrete implementation from the ServiceLoader class.
MySQL is used as an example to check its Jar package. The configuration file is as follows:To obtain the concrete implementation of the interface, the code is as follows:
Iterator<Driver> iterator = ServiceLoader.load(Driver.class).iterator();
while(iterator.hasNext()) { System.out.println(iterator.next().getClass()); } output:class com.mysql.jdbc.Driver
class com.mysql.fabric.jdbc.FabricMySQLDriver
Copy the code
Disadvantages of the Java SPI:
- Loading on demand is not supported, and iterator traversal instantiates all implementation classes even if they are not used, which is a waste of resources.
- The way to get implementation classes is inflexible and can only be traversed by iterators.
- Without caching, implementation classes are created multiple times.
- The extension fails to be loaded, and the cause of the failure is lost.
- AOP and IOC are not supported.
3. Dubbo SPI
Dubbo SPI defines its own set of specifications, while addressing some of the problems with Java SPI, with the following advantages:
- Extension classes are loaded on demand, saving resources.
- SPI files take the form of Key=Value, which gives you the flexibility to get implementation classes based on the extension.
- Extension class objects are cached to avoid repeated creation.
- Extended class loading failure logs are generated for easy troubleshooting.
- Support for AOP and IOC.
Dubbo SPI usage specification:
- Write an interface that must be annotated @spi to indicate that it is an extensible interface.
- Write the implementation class.
- Under the background of the ClassPath
META-INF/dubbo
Directory creates a file named with the fully qualified name of the interface in the format Key=Value, where Key is the name of the extension point and Value is the fully qualified name of the extension point implementation class. - The extension point implementation is obtained through the ExtensionLoader class.
By default, Dubbo scans the configurations of meta-INF /services, meta-INF/Dubbo, and meta-INF/Dubbo /internal. The first is for compatibility with Java SPI, and the third is for extension points used internally by Dubbo.
Dubbo SPI supports four features: automatic packaging, automatic injection, adaptive, and automatic activation.
3.1 Automatic Packaging
Dubbo SPI’s AOP takes advantage of “automatic wrapping.” In the implementation of an extended class, there may be parts of logic that are common and should be extracted rather than duplicated code for each implementation class. In this case, we should create a Wrapper class and write general logic, which should hold a original object Origin inside. The personalized business logic is handed to Origin itself, and the general logic is handled by Wrapper.
The specification for automatic wrapping is that the Wrapper class should provide a constructor that takes only one argument: the extension point interface.
public class SayWrapper implements Say {
private final Say origin;
public SayWrapper(Say origin) {
this.origin = origin;
}
@Override
public void say(a) {
System.err.println("before...");
origin.say();
System.err.println("after..."); }}Copy the code
SPI File Configuration
impl=demo.spi.wrapper.SayImpl
wrapper=demo.spi.wrapper.SayWrapper
Copy the code
Get extended implementation
// Get the wrapper class by defaultSay say = ExtensionLoader.getExtensionLoader(Say.class).getDefaultExtension(); say.say(); Output: before... say... after...Copy the code
3.2 Automatic Injection
Dubbo SPI supports automatic injection. Similar to Spring’s IOC, Dubbo automatically helps us inject dependent extension class member objects when the property of an extension class is of another extension point type and Setter methods are provided.
Suppose you now have an Eat extension interface.
@SPI
public interface Eat {
@Adaptive("key")
void eat(URL url);
}
public class EatImpl implements Eat {
@Override
public void eat(URL url) {
System.err.println("eat meat..."); }}Copy the code
SayA relies on the Eat extension.
public class SayA implements Say {
public Eat eat;
public void setEat(Eat eat) {
this.eat = eat; }}Copy the code
When we get the SayA implementation, Dubbo will automatically inject the Eat extension point object for us. **Eat extension point implementation classes may be many, which one should be injected? Dubbo’s injection is always an adaptive extension that determines which implementation to call based on the URL in the argument.
3.3 adaptive
SPI extension points can have situations where there are many extension point implementation classes that cannot be hard-coded, requiring the runtime to dynamically determine the implementation classes based on the parameters. To fulfill this requirement, Dubbo SPI implements adaptive invocation.
Adaptive requires the @adaptive annotation, which can be applied to a class or method. When added to a class, that class is an adaptive class; When applied to a method, the proxy class is automatically generated and matched against the parameters in the URL object to determine the implementation.
The implementation principle of Adaptive call is not complicated. Dubbo uses Javassist technology to dynamically generate an Adaptive proxy class for the extension interface, and the rule of the class name is XXX$Adaptive. In the proxy class, the specific extension point implementation class is matched according to the parameters in the URL object.
@SPI
public interface Say {
// Matches the key parameter in the URL
@Adaptive({"key"})
void say(URL url);
}
Copy the code
Assuming that there are two implementations a and B, the adaptive call is as follows:
Say say = ExtensionLoader.getExtensionLoader(Say.class).getAdaptiveExtension();
say.say(URL.valueOf("Http://127.0.0.1? key=a"));
say.say(URL.valueOf("Http://127.0.0.1? key=b")); Output: sayA... sayB...Copy the code
3.4 Automatic Activation
Scenario: Multiple implementation classes of an extension point need to be enabled at the same time based on rules, such as Filter filters.
Automatic activation requires the @Activate annotation. Once this annotation is added, the implementation class needs to be automatically activated according to the conditions. The annotation attributes are as follows:
attribute | instructions |
---|---|
group | The Group is activated if the match is successful |
value | The URL is activated if the Key exists |
order | Extension point execution order |
Suppose there is a Filter interface:
@SPI
public interface Filter {
void invoke(a);
}
Copy the code
FilterA indicates that the URL is automatically activated if XXX exists in the Consumer group. The sequence is 1.
@Activate(group = {"consumer"}, value = {"xxx"}, order = 1)
public class FilterA implements Filter {
@Override
public void invoke(a) {
System.err.println("FilterA..."); }}Copy the code
FilterB indicates that the provider group is automatically activated when ooO parameters exist in the URL in the second order.
@Activate(group = {"provider"}, value = {"ooo"}, order = 2)
public class FilterB implements Filter {
@Override
public void invoke(a) {
System.err.println("FilterB..."); }}Copy the code
Get the set of active extension point implementation class objects, the following output only FilterA, FilterB Group matching failed.
ExtensionLoader<Filter> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
URL url = URL.valueOf("Http://127.0.0.1? key=xxx,ooo");
List<Filter> filters = extensionLoader.getActivateExtension(url, "key"."consumer"); filters.stream().forEach(System.out::println); Output: demo. Spi. Activate. FilterACopy the code
4. Source code analysis
The core class of the Dubbo SPI is ExtensionLoader, whose primary responsibility is to load extension point implementation classes and obtain extension point instances based on various conditions.
Property description how to:
public class ExtensionLoader<T> {
private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);
// Multiple extension points are separated by commas
private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
// Extend the point instance cache
private finalConcurrentMap<Class<? >, Object> extensionInstances =new ConcurrentHashMap<>(64);
/ / interface
private finalClass<? > type;// Extend the dependency injector
private final ExtensionInjector injector;
// Extend class name caching
private finalConcurrentMap<Class<? >, String> cachedNames =new ConcurrentHashMap<>();
// Extend the class cache
private finalHolder<Map<String, Class<? >>> cachedClasses =new Holder<>();
// Automatically activate extended instance cache
private final Map<String, Object> cachedActivates = Collections.synchronizedMap(new LinkedHashMap<>());
// Extend the Group cache activated by the class
private final Map<String, Set<String>> cachedActivateGroups = Collections.synchronizedMap(new LinkedHashMap<>());
// Extend the Value cache activated by the class
private final Map<String, String[]> cachedActivateValues = Collections.synchronizedMap(new LinkedHashMap<>());
// Extend the instance cache
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// Dynamically generated adaptive instance cache
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
// Dynamically generated adaptive classes
private volatileClass<? > cachedAdaptiveClass =null;
// The default extension name
private String cachedDefaultName;
// Dynamically create exceptions for adaptive instances
private volatile Throwable createAdaptiveInstanceError;
// Wrap the class cache
privateSet<Class<? >> cachedWrapperClasses;// Exception cache
private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<>();
Meta-inf /dubbo/internal/ * 2. Meta-inf /dubbo/ * 3. Meta-inf /services/ */
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
/** * Record all exceptions when using SPI */
private Set<String> unacceptableExceptions = new ConcurrentHashSet<>();
//
private ExtensionDirector extensionDirector;
// Extension point postprocessing
private List<ExtensionPostProcessor> extensionPostProcessors;
// Extend the class instantiation policy
private InstantiationStrategy instantiationStrategy;
private Environment environment;
// Automatically enable extension point sorting
private ActivateComparator activateComparator;
private ScopeModel scopeModel;
}
Copy the code
ExtensionLoader is bound to an interface. An interface corresponds to an instance of ExtensionLoader.
ExtensionLoader<Say> extensionLoader = ExtensionLoader.getExtensionLoader(Say.class);
Copy the code
ExtensionLoader has three common methods, which are analyzed below:
The method name | note |
---|---|
getDefaultExtension() | Gets the default extension point implementation class instance |
getAdaptiveExtension() | Get an adaptive instance |
getActivateExtension() | Gets a collection of automatic activation instances |
4.1 Default extension points
Get the default extension point implementation class instance with the getDefaultExtension() method as the entry point. By default, the Wrapper class is automatically wrapped if there is one.
public T getDefaultExtension(a) {
// Load the implementation class
getExtensionClasses();
if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
return null;
}
// Get the default extension point implementation class instance
return getExtension(cachedDefaultName);
}
Copy the code
The getExtensionClasses() method gets all the implementation classes under the extension point, loads them once, and then caches them.
privateMap<String, Class<? >> getExtensionClasses() {// take priority from the cacheMap<String, Class<? >> classes = cachedClasses.get();if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// Load the class from the specified path without cachingclasses = loadExtensionClasses(); cachedClasses.set(classes); }}}return classes;
}
Copy the code
The loadExtensionClasses() method loads classes from three paths by default, each of which is defined as a load strategy corresponding to the Class LoadingStrategy.
privateMap<String, Class<? >> loadExtensionClasses() {// Cache the default extension specified by the @spi annotationcacheDefaultExtensionName(); Map<String, Class<? >> extensionClasses =new HashMap<>();
Meta-inf /dubbo/internal/ * 2. Meta-inf /dubbo/ * 3. Meta-inf /services/ */
for (LoadingStrategy strategy : strategies) {
// Load the Class from the specified directory
loadDirectory(extensionClasses, strategy, type.getName());
// compatible with old ExtensionFactory
if (this.type == ExtensionInjector.class) { loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName()); }}return extensionClasses;
}
Copy the code
Once the extension class is loaded, you can create instances with the default extension name, which is automatically wrapped by default.
private T createExtension(String name, boolean wrap) {
// Get the Class of the extensionClass<? > clazz = getExtensionClasses().get(name);if (clazz == null || unacceptableExceptions.contains(name)) {
// Failed to create a Class instance, throw an exception
throw findException(name);
}
try {
T instance = (T) extensionInstances.get(clazz);
if (instance == null) {
// Create an instance and cache it
extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));
instance = (T) extensionInstances.get(clazz);
// preprocessing
instance = postProcessBeforeInitialization(instance, name);
// Setter method injection
injectExtension(instance);
// post-processing
instance = postProcessAfterInitialization(instance, name);
}
if (wrap) {// Automatic packagingList<Class<? >> wrapperClassesList =new ArrayList<>();
if(cachedWrapperClasses ! =null) {
// Wrap class sort
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
for(Class<? > wrapperClass : wrapperClassesList) { Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);// @wrapper annotation matches to determine whether a Wrapper is needed
if (wrapper == null|| (ArrayUtils.contains(wrapper.matches(), name) && ! ArrayUtils.contains(wrapper.mismatches(), name))) {Reflection creates an instance of the wrapper class
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
// Post-processing of the packaging class
instance = postProcessAfterInitialization(instance, name);
}
}
}
}
initExtension(instance);
returninstance; }}Copy the code
The injectExtension() method does dependency injection, which looks for the Setter method of the Class and determines whether its arguments are also extension points. If so, it gets an adaptive instance of the extension point from ExtensionAccessor, and then reflects the assignment. Note: Dubbo SPI can only inject Adaptive instances, so you must ensure that the injected extension points are Adaptive.
4.2 adaptive
The getAdaptiveExtension() method is used to obtain the Adaptive instance of the extension point. Its principle is not complicated. It simply generates the proxy class of the extension point, and then parses the URL attribute in the parameter to match the value of the @Adaptive annotation, and then invokes the specified extension point implementation.
Adaptive classes are generated dynamically when the program runs, either using JDK dynamic proxies or bytecode technologies like CGLIB, which Dubbo uses by default, Javassist.
Adaptive objects also have a cache and are created only once, with the corresponding property cachedAdaptiveInstance and createAdaptiveExtension().
private T createAdaptiveExtension(a) {
try {
// Get the adaptive Class to create an instance
T instance = (T) getAdaptiveExtensionClass().newInstance();
// pre/post, Setter injection
instance = postProcessBeforeInitialization(instance, null);
instance = injectExtension(instance);
instance = postProcessAfterInitialization(instance, null);
initExtension(instance);
return instance;
} catch (Exception e) {
throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: "+ e.getMessage(), e); }}Copy the code
Create the adaptive object before, first of all have to generate the adaptive Class, corresponding method is createAdaptiveExtensionClass ().
privateClass<? > createAdaptiveExtensionClass() { ClassLoader classLoader = type.getClassLoader();try {
if (NativeUtils.isNative()) {
return classLoader.loadClass(type.getName() + "$Adaptive"); }}catch (Throwable ignore) {
}
// Generate the Class source code according to the Class and default extension
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
// Get the default compiler :JavassistCompiler
org.apache.dubbo.common.compiler.Compiler compiler = extensionDirector.getExtensionLoader(
org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// Dynamically compile to Class
return compiler.compile(code, classLoader);
}
Copy the code
Here is an example of adaptive class code generated by Dubbo:
public class Say$Adaptive implements demo.spi.adaptive.Say {
public void say(org.apache.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
// Get the parameter in the URL. Key is specified by the @adaptive annotation
String extName = url.getParameter("key");
if (extName == null)
throw new IllegalStateException("Failed to get extension (demo.spi.adaptive.Say) name from url (" + url.toString() + ") use keys([key])");
ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), demo.spi.adaptive.Say.class);
// Get the implementation class for the extension specified by keydemo.spi.adaptive.Say extension = (demo.spi.adaptive.Say) scopeModel.getExtensionLoader(demo.spi.adaptive.Say.class).getExtension(extName); extension.say(arg0); }}Copy the code
4.3 Automatic Activation
The getActivateExtension() method is used to get the set of automatically activated extension point instances. If you want an extension point to be automatically activated, just add @activate to the class. You can also configure Group and Value to set the conditions for automatic activation. For example, some extension points are only activated on the Provider side and some are only activated on the Consumer side.
public @interface Activate {
// Group matched for automatic activation
String[] group() default {};
// Value matched during automatic activation
String[] value() default {};
// Extend the point order
int order(a) default 0;
}
Copy the code
First, the Value corresponding to the Key needs to be resolved from the URL. Multiple extension point names are separated by commas.
public List<T> getActivateExtension(URL url, String key, String group) {
// Get the Value of the Key
String value = url.getParameter(key);
// Value use to split
return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
}
Copy the code
The active extension point instances are stored using TreeMap, and the Key is sorted by the order attribute in the annotation.
Map<Class<? >, T> activateExtensionsMap =new TreeMap<>(activateComparator);
List<String> names = values == null ? new ArrayList<>(0) : asList(values);
Set<String> namesSet = new HashSet<>(names);
Copy the code
Value If -default is configured, the default automatic activation class is excluded. Otherwise, the default activation class is loaded first. In this case, the extension class specified by Value is not loaded.
// There is no -default extension point (Value and Group match successfully)
if(! namesSet.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {if (cachedActivateGroups.size() == 0) {
synchronized (cachedActivateGroups) {
// cache all extensions
if (cachedActivateGroups.size() == 0) {
// Load the configured extension class
getExtensionClasses();
// Iterate through the @activate class and cache the Group and Value configurations of the class
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
activateGroup = ((Activate) activate).group();
activateValue = ((Activate) activate).value();
} else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
// Old annotations are compatible
activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
} else {
continue;
}
cachedActivateGroups.put(name, new HashSet<>(Arrays.asList(activateGroup)));
cachedActivateValues.put(name, activateValue);
}
}
}
}
cachedActivateGroups.forEach((name, activateGroup) -> {
if (isMatchGroup(group, activateGroup)/ / Group match
&& !namesSet.contains(name)// The extension point specified by Key is loaded later
&& !namesSet.contains(REMOVE_VALUE_PREFIX + name)
/ / Value matching
&& isActive(cachedActivateValues.get(name), url)) {
// Group and Value match successfully, and there is no default extension point specified by Key.activateExtensionsMap.put(getExtensionClass(name), getExtension(name)); }}); }Copy the code
If Value specifies default, the order of extension points will be affected. Extension points in default are still ordered, but extension points before and after default will not be sorted according to order. For example:
`extA,defaultExtB 'extA will come before all default extension points, and extB will come after all default extension pointsCopy the code
The code is as follows:
if (namesSet.contains(DEFAULT_KEY)) {
ArrayList<T> extensionsResult = new ArrayList<>(activateExtensionsMap.size() + names.size());
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if(! name.startsWith(REMOVE_VALUE_PREFIX) && ! namesSet.contains(REMOVE_VALUE_PREFIX + name)) {if(! DEFAULT_KEY.equals(name)) {if(containsExtension(name)) { extensionsResult.add(getExtension(name)); }}else{ extensionsResult.addAll(activateExtensionsMap.values()); }}}return extensionsResult;
}
Copy the code
If Value does not specify default, then all extension point instances are stored in TreeMap, all in order.
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if(! name.startsWith(REMOVE_VALUE_PREFIX) && ! namesSet.contains(REMOVE_VALUE_PREFIX + name)) {if(! DEFAULT_KEY.equals(name)) {if(containsExtension(name)) { activateExtensionsMap.put(getExtensionClass(name), getExtension(name)); }}}}return new ArrayList<>(activateExtensionsMap.values());
Copy the code
5. To summarize
The SPI mechanism uses a policy pattern, in which multiple implementations of an interface are programmed to the interface by the developer, and the implementation is not hard-coded in the program, but specified externally through configuration files. Java has SPI built in, but it has some drawbacks, such as not supporting load on demand, wasting resources, difficult troubleshooting, etc., so Dubbo defined his own set of specifications and developed his own SPI functionality.
Dubbo SPI has a number of optimizations and enhancements. It supports loading on demand and caches extended objects without creating them repeatedly. The way to get extended objects is more flexible, and advanced features such as automatic wrapping, IOC and AOP, automatic activation, and adaptive invocation are added.