Dubbo之SPI原理详解
【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
SPI全称为Service Provider Interface,是一种服务提供机制,比如在现实中我们经常会有这种场景,就是对于一个规范定义方而言(可以理解为一个或多个接口),具体的服务实现方是不可知的(可以理解为对这些接口的实现类),那么在定义这些规范的时候,就需要规范定义方能够通过一定的方式来获取到这些服务提供方具体提供的是哪些服务,而SPI就是进行这种定义的。
1. jdk与Dubbo SPI对比
比较典型的SPI定义方式有jdk的SPI,其主要是在项目的META-INF/services
目录下以接口的全路径名为文件名,然后在该文件中声明该接口的具体实现类有哪些。这里说的META-INF/services
目录和其中的文件主要是在服务提供方的jar包中。这里的整体流程可以理解为,首先规范制定方会定义一个接口,比如com.jdk.spi.Fruit
,然后其通过读取项目目录下(包括所依赖的jar包中)的META-INF/service
名称为com.jdk.spi.Fruit
的文件,获取其中定义的子类,接着读取并实例化该类的对象,作为目标接口的实现类来使用。通过这种方式,服务提供方只需要在其提供的jar包的META-INF/services
目录中声明其提供的子类服务是什么即可。对于jdk的SPI,其主要存在两个问题:
- 为每个接口提供的服务一般尽量只提供一个,因为jdk的SPI默认会将所有目标文件中定义的所有子类都读取到返回使用;
- 当定义多个子类实现时,无法动态的根据配置来使用不同的配置;
基于上述两个问题,dubbo对jdk的SPI进行了扩展。从整体流程上而言,dubbo的SPI与jdk的SPI相似,也就是在规范制定方会定义一定的接口,然后读取项目的META-INF/dubbo/internal
目录下的以接口全路径名定义的文件,最后通过所需要的key值来获取不同的子类实现。
2. Dubbo SPI示例
这里假设对于规范提供方定义了一个com.dubbo.spi.Fruit
的接口,该接口的具体声明如下:
@SPI
public interface Fruit {
void eat();
}
这里的@SPI
注解的主要用于标识当前接口是一个dubbo的SPI接口。如下是规范制定方使用该接口的方式:
public class ExtensionFactoryApp {
public static void main(String[] args) {
Fruit apple = ExtensionLoader.getExtensionLoader(Fruit.class).getExtension("banana");
apple.eat();
}
}
这里ExtensionLoader.getExtensionLoader()
方法会读取META-INF/dubbo/internal
目录下的名称为com.dubbo.spi.Fruit
的文件,然后getExtension("banana")
方法会获取该文件中key值为banana的子类,并且进行实例化返回。通过这种方式,我们的规范定义方就不需要知道具体的Fruit
实现类是什么,而只需要按照一定的规则进行读取并使用即可。
对于服务提供方而言,其首先需要在META-INF/dubbo/internal
目录下定义一个名称为com.dubbo.spi.Fruit
的文件,如下是该文件的内容:
apple=com.dubbo.spi.impl.Apple
banana=com.dubbo.spi.impl.Banana
可以看到,这里为Fruit
接口定义了两个实现类Apple
和Banana
,对应的key分别为apple和banana。我们运行上述程序,可以看到如下输出:
eat banana
3. 实现原理
通过上面的示例可以看出,dubbo对于SPI的实现主要是在ExtensionLoader
这个类中,这个类主要有三个方法:
public T getExtension(String name);
public T getAdaptiveExtension();
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type);
- getExtension():主要用于获取名称为name的对应的子类的对象,这里如果子类对象如果有AOP相关的配置,这里也会对其进行封装;
- getAdaptiveExtension():使用定义的装饰类来封装目标子类,具体使用哪个子类可以在定义的装饰类中通过一定的条件进行配置;
- getExtensionLoader():加载当前接口的子类并且实例化一个ExtensionLoader对象。
3.1 getExtension()
这里getExtension()
方法的主要作用是获取name对应的子类对象返回。其实现方式是首先读取定义文件中的子类,然后根据不同的子类对象的功能的不同,比如使用@Adaptive
修饰的装饰类和用于AOP的Wrapper类,将其封装到不同的缓存中。最后根据传入的name获取其对应的子类对象,并且使用相应的Wrapper类对其进行封装。如下是getExtension()
方法的源码:
@SuppressWarnings("unchecked")
public T getExtension(String name) {
if (name == null || name.length() == 0) {
throw new IllegalArgumentException("Extension name == null");
}
// 如果名称为true,则返回默认的子类对象,这里默认的子类对象的name定义在目标接口的@SPI注解中
if ("true".equals(name)) {
return getDefaultExtension();
}
// 查看当前是否已经缓存有保存目标对象的实例的Holder对象,缓存了则直接返回,
// 没缓存则创建一个并缓存起来
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
// 如果无法从Holder中获取目标对象的实例,则使用双检查法为目标对象创建一个实例
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建name对应的子类对象的实例
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
可以看到,关于对于目标对象的获取,首先是从缓存里取,没取到才会进行创建。这里需要说明的是,如果传入的name为true,那么就会返回默认的子类实例,而默认的子类实例是通过其名称进行映射的,该名称存储在目标接口的@SPI
注解中。如下是createExtension()
方法的源码:
@SuppressWarnings("unchecked")
private T createExtension(String name) {
// 获取当前名称对应的子类类型,如果不存在,则抛出异常
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
// 获取当前class对应的实例,如果缓存中不存在,则实例化一个并缓存起来
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 为生成的实例通过其set方法注入对应的实例,这里实例的获取方式不仅可以通过SPI的方式
// 也可以通过Spring的bean工厂获取
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
// 实例化各个wrapper对象,并将目标对象通过wrapper的构造方法传入,
// 另外还会通过wrapper对象的set方法对其依赖的属性进行注入
instance = injectExtension((T) wrapperClass.getConstructor(type)
.newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: "
+ type + ") could not be instantiated: " + t.getMessage(), t);
}
}
在createExtension()
方法中,其主要做了三件事:a. 加载定义文件中的各个子类,然后将目标name对应的子类返回;b. 通过目标子类的set方法为其注入其所依赖的bean,这里既可以通过SPI,也可以通过Spring的BeanFactory获取所依赖的bean;c. 获取定义文件中定义的wrapper对象,然后使用该wrapper对象封装目标对象,并且还会调用其set方法为wrapper对象注入其所依赖的属性。
关于wrapper对象,这里需要说明的是,其主要作用是为目标对象实现AOP。wrapper对象有两个特点:a. 与目标对象实现了同一个接口;b. 有一个以目标接口为参数类型的构造函数。这也就是上述createExtension()
方法最后封装wrapper对象时传入的构造函数实例始终可以为instance实例的原因。这里我们首先看getExtensionClasses()
方法是如何实现的:
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加载定义文件,并且将定义的类按照功能缓存在不同的属性中,即:
// a. 目标class类型缓存在cachedClasses;
// b. wrapper的class类型缓存在cachedWrapperClasses;
// c. 用于装饰的class类型缓存在cachedAdaptiveClass;
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
上述代码中,主要是在缓存中获取目标class的map缓存,如果不存在则通过定义文件加载。这里我们继续看loadExtensionClasses()
的源码:
private Map<String, Class<?>> loadExtensionClasses() {
// 获取目标接口上通过@SPI注解定义的默认子类对应的名称,并将其缓存在cachedDefaultName中
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension "
+ "name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
// 分别在META-INF/dubbo/internal、META-INF/dubbo、META-INF/services目录下
// 获取定义文件,并且读取定义文件中的内容,这里主要是通过META-INF/dubbo/internal
// 获取目标定义文件
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY,
type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY,
type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY,
type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
这里loadExtensionClasses()
主要是分别从三个目录中读取定义文件,读取该文件,并且进行缓存。这里我们继续看loadDirectory()
方法的源码:
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir,
String type) {
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
// 加载定义文件
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
// 对定义文件进行遍历,依次加载定义文件的内容
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}
这里主要是对每个目录进行加载,然后依次加载定义文件的内容,而对定义文件内容的处理主要是在loadResource()
方法中,在对文件中每一行记录进行处理之后,其其最终是调用的loadClass()
方法加载目标class的。如下是loadClass()
方法的源码:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL,
Class<?> clazz, String name) throws NoSuchMethodException {
// 如果加载得到的子类不是目标接口的实现类,则抛出异常
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: "
+ type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
// 如果子类上标注有@Adaptive注解,说明其是一个装饰类,则将其缓存在cachedAdaptiveClass中,
// 需要注意的是,一个接口只能为其定义一个装饰类
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
// 这里判断子类是否是一个wrapper类,判断方式就是检查其是否有只含一个目标接口类型参数的构造函数,
// 有则说明其是一个AOP的wrapper类
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {
// 走到这里说明当前子类不是一个功能型的类,而是最终实现具体目标的子类
clazz.getConstructor();
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class "
+ clazz.getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
// 获取子类上的@Activate注解,该注解的主要作用是对子类进行分组的,
// 对于分组之后的子类,可以通过getActivateExtension()来获取
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
} else {
// 兼容alibaba版本的注解
com.alibaba.dubbo.common.extension.Activate oldActivate =
clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
if (oldActivate != null) {
cachedActivates.put(names[0], oldActivate);
}
}
// 将目标子类缓存到extensionClasses中
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension "
+ type.getName() + " name " + n + " on " + c.getName()
+ " and " + clazz.getName());
}
}
}
}
}
这里loadClass()
方法主要作用是对子类进行划分,这里主要划分成了三部分:a. 使用@Adaptive注解标注的装饰类;b. 包含有目标接口类型参数构造函数的wrapper类;c. 目标处理具体业务的子类。
总结而言,getExtension()
方法主要是获取指定名称对应的子类。在获取过程中,首先会从缓存中获取是否已经加载过该子类,如果没加载过,则通过定义文件加载,并且使用获取到的wrapper对象封装目标对象返回。
3.2 getAdaptiveExtension()
前面我们讲解了ExtensionLoader
在加载了定义文件之后会对子类进行一个划分,其中就涉及到使用@Adaptive
进行标注的子类,该子类的作用主要是用于对目标类进行装饰的,从而实现一定的目的。但是@Adaptive
也可以标注在方法上,其使用的方式主要是在目标接口的某个方法上进行标注,这个时候,dubbo就会通过javassist字节码生成工具来动态的生成目标接口的子类对象,该子类会对该接口中标注了@Adaptive
注解的方法进行重写,而其余的方法则默认抛出异常,通过这种方式可以达到对特定的方法进行修饰的目的。我们首先来阅读getAdaptiveExtension()
方法的源码:
@SuppressWarnings("unchecked")
public T getAdaptiveExtension() {
// 从缓存中获取装饰类的实例,存在则直接返回,不存在则创建一个缓存起来,然后返回
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 创建一个装饰类的实例
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive"
+ " instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("fail to create adaptive instance: "
+ createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
这里主要是从缓存中获取目标类的实例,不存在则创建一个该实例,这里我们直接阅读createAdaptiveExtension()
方法的源码:
@SuppressWarnings("unchecked")
private T createAdaptiveExtension() {
try {
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extension "
+ type + ", cause: " + e.getMessage(), e);
}
}
这里createAdaptiveExtension()
首先委托给getAdaptiveExtensionClass()
方法获取一个装饰类实例,然后通过injectExtension()
方法调用该实例的set方法来注入其所依赖的属性值。如下是getAdaptiveExtensionClass()
方法的源码:
private Class<?> getAdaptiveExtensionClass() {
// 获取目标extensionClasses,如果无法获取到,则在定义文件中进行加载
getExtensionClasses();
// 如果目标类型有使用@Adaptive标注的子类型,则直接使用该子类作为装饰类
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 如果目标类型没有使用@Adaptive标注的子类型,则尝试在目标接口中查找是否有使用@Adaptive标注的
// 方法,如果有,则为该方法动态生成子类装饰代码
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
这里可以看到,只有目标接口没有使用@Adaptive
标注的子类时,才会使用Javassist来为目标接口生成其子类的装饰方法。如下是createAdaptiveExtensionClass()
方法的源码:
private Class<?> createAdaptiveExtensionClass() {
// 创建子类代码的字符串对象
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
// 获取当前dubbo SPI中定义的Compiler接口的子类对象,默认是使用javassist,
// 然后通过该对象来编译生成的code,从而动态生成一个class对象
org.apache.dubbo.common.compiler.Compiler compiler =
ExtensionLoader.getExtensionLoader(
org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
上述代码中,首先动态生成目标接口的子类字符串,然后通过javassit来编译该子类字符串,从而动态生成目标class。关于如何编译目标代码字符串,这里主要有两种方式:jdk编译和javassist编译,读者可自行查阅其具体的编译原理,我们这里主要讲解dubbo SPI是如何生成子类代码的。由于createAdaptiveExtensionClassCode()
方法主要是通过一些字符串的拼接操作来得到子类字符串,我们这里就直接展示dubbo中一个非常重要的接口Protocol
,其生成的子类字符串的形式:
package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
private static final org.apache.dubbo.common.logger.Logger logger =
org.apache.dubbo.common.logger.LoggerFactory.getLogger(ExtensionLoader.class);
private java.util.concurrent.atomic.AtomicInteger count =
new java.util.concurrent.atomic.AtomicInteger(0);
// 对于未使用@Adaptive标注的方法,子类中直接抛出异常
public void destroy() {
throw new UnsupportedOperationException("method public abstract void"
+ " org.apache.dubbo.rpc.Protocol.destroy() of interface "
+ "org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
// 对于未使用@Adaptive标注的方法,子类中直接抛出异常
public int getDefaultPort() {
throw new UnsupportedOperationException("method public abstract int "
+ "org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface "
+ "org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
// 对于使用@Adaptive标注的类,则在子类中委托给其他的类进行处理
public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0)
throws org.apache.dubbo.rpc.RpcException {
if (arg0 == null) {
throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
}
if (arg0.getUrl() == null) {
throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument "
+ "getUrl() == null");
}
org.apache.dubbo.common.URL url = arg0.getUrl();
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) {
throw new IllegalStateException("Fail to get "
+ "extension(org.apache.dubbo.rpc.Protocol) name from url("
+ url.toString() + ") use keys([protocol])");
}
org.apache.dubbo.rpc.Protocol extension = null;
try {
extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
} catch (Exception e) {
if (count.incrementAndGet() == 1) {
logger.warn("Failed to find extension named " + extName
+ " for type org.apache.dubbo.rpc.Protocol, will use default extension"
+ " dubbo instead.", e);
}
extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
org.apache.dubbo.rpc.Protocol.class).getExtension("dubbo");
}
return extension.export(arg0);
}
public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0,
org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
if (arg1 == null) {
throw new IllegalArgumentException("url == null");
}
org.apache.dubbo.common.URL url = arg1;
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) {
throw new IllegalStateException("Fail to get "
+ "extension(org.apache.dubbo.rpc.Protocol) name from url("
+ url.toString() + ") use keys([protocol])");
}
org.apache.dubbo.rpc.Protocol extension = null;
try {
extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
} catch (Exception e) {
if (count.incrementAndGet() == 1) {
logger.warn("Failed to find extension named " + extName + " for type "
+ "org.apache.dubbo.rpc.Protocol, will use default extension dubbo "
+ "instead.", e);
}
extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
org.apache.dubbo.rpc.Protocol.class).getExtension("dubbo");
}
return extension.refer(arg0, arg1);
}
}
可以看到,子类中对于没有使用@Adaptive
标注的方法,其实现时直接抛出异常,对于使用@Adaptive
标注的方法,则对其进行实现,并且委托给其他的SPI提供者进行相关的处理。
3.3 getExtensionLoader()
这里getExtensionLoader()
的主要作用是为当前接口类型实例化一个ExtensionLoader对象,然后将其缓存起来。如下是getExtensionLoader()
方法的源码:
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
// 判断当前传入的如果不是接口,则抛出异常
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type
+ ") is not interface!");
}
// 这里主要是判断传入的接口上是否使用@SPI进行了标注,标注了该注解才表明当前
// 接口是一个dubbo的SPI接口
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type
+ ") is not extension, because WITHOUT @"
+ SPI.class.getSimpleName() + " Annotation!");
}
// 从缓存中读取当前类对应的ExtensionLoader对象,如果不存在,则实例化一个并且缓存起来
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
这里可以看到,对于ExtensionLoader的获取,其实现过程比较简单,主要是从缓存中获取,如果缓存不存在,则实例化一个并且缓存起来。
4. 小结
本文首先对jdk的SPI和dubbo的SPI进行了简单的对比,说明了dubbo相对于jdk的SPI所提供的额外的动态配置性能;然后通过一个示例来展示了dubbo的SPI的使用方式;最后着重讲解了用于实现dubbo SPI的ExtensionLoader的实现原理。
5. 广告
读者朋友如果觉得本文还不错,可以点击下面的广告链接,这可以为作者带来一定的收入,从而激励作者创作更好的文章,非常感谢!