1. 前言
Dubbo服务框架采用了「微内核+插件」的设计原则,Dubbo自身的核心功能点也是通过扩展点实现的,这意味着Dubbo几乎所有的功能点都可以由用户自定义的扩展和替换,这也大大的提高了Dubbo框架本身的高度可扩展性。举例来说,如果你觉得Dubbo内置的对象序列化方式不好用,你完全可以自定义一个;如果你觉得Netty网络传输不好用,你也完全可以自定义一个。
SPI的全称是「Service Provider Interface」,在介绍Dubbo SPI前,先来看看Java自带的SPI机制。
Java SPI起初是提供给厂商做插件开发用的,例如数据库驱动java.sql.Driver
,市面上各种各样的数据库,不同的数据库底层协议都不一样,为了方便开发者调用数据库而不用关心它们之间的差异,因此必须提供一个统一的接口来规范和约束这些数据库。有了统一的接口,数据库厂商就可以按照规范去开发自己的数据库驱动了。
厂商开发好数据库驱动了,应用如何使用呢?该使用哪个驱动呢?以MySQL为例,早期手写JDBC时,开发者需要手动注册驱动,现在已经不需要了,就是利用了SPI机制。
2. Java SPI
Java SPI使用了策略模式,一个接口多种实现,开发者面向接口编程,具体的实现并不在程序中直接硬编码,而是通过外部文件进行配置。
Java SPI约定了一个规范,使用步骤如下:
- 编写一个接口。
- 编写具体实现类。
- 在ClassPath下的
META-INF/services
目录创建以接口全限定名命名的文件,文件内容为实现类的全限定名,多个实现用换行符分割。 - 通过ServiceLoader类获取具体实现。
这里以MySQL为例,查看其Jar包,会发现配置文件如下: 获取接口具体实现,代码如下:
Iterator<Driver> iterator = ServiceLoader.load(Driver.class).iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().getClass());
}
输出:
class com.mysql.jdbc.Driver
class com.mysql.fabric.jdbc.FabricMySQLDriver
复制代码
Java SPI的缺点:
- 不支持按需加载,迭代器遍历会实例化所有的实现类,即使它没有被用到,太浪费资源了。
- 获取实现类的方式不灵活,只能通过迭代器遍历。
- 没有缓存,实现类会被多次创建。
- 扩展加载失败,失败原因丢失,不方便排查。
- 不支持AOP和IOC。
3. Dubbo SPI
Dubbo SPI定义了一套自己的规范,同时对Java SPI存在的问题进行了改进,优点如下:
- 扩展类按需加载,节约资源。
- SPI文件采用Key=Value形式,可以根据扩展名灵活获取实现类。
- 扩展类对象做了缓存,避免重复创建。
- 扩展类加载失败有详细日志,方便排查。
- 支持AOP和IOC。
Dubbo SPI使用规范:
- 编写接口,接口必须加@SPI注解,代表它是一个可扩展的接口。
- 编写实现类。
- 在ClassPath下的
META-INF/dubbo
目录创建以接口全限定名命名的文件,文件内容为Key=Value格式,Key是扩展点的名称,Value是扩展点实现类的全限定名。 - 通过ExtensionLoader类获取扩展点实现。
Dubbo默认会扫描META-INF/services
、META-INF/dubbo
、META-INF/dubbo/internal
三个目录下的配置,第一个是为了兼容Java SPI,第三个是Dubbo内部使用的扩展点。
Dubbo SPI支持四种特性:自动包装、自动注入、自适应、自动激活。
3.1 自动包装
Dubbo SPI的AOP就是利用「自动包装」来实现的。在扩展类的实现中,可能存在部分逻辑是通用的,应该把它们提取出来,而不是每个实现类都写一份重复的代码。此时,应该创建一个Wrapper包装类,编写通用逻辑,它内部应该持有一个原对象Origin,个性化的业务逻辑交给Origin自己处理,通用逻辑由Wrapper统一处理。
自动包装的规范是:Wrapper类应该提供一个构造函数,该函数只有一个参数:扩展点接口。
public class SayWrapper implements Say {
private final Say origin;
public SayWrapper(Say origin) {
this.origin = origin;
}
@Override
public void say() {
System.err.println("before...");
origin.say();
System.err.println("after...");
}
}
复制代码
SPI文件配置
impl=demo.spi.wrapper.SayImpl
wrapper=demo.spi.wrapper.SayWrapper
复制代码
获取扩展实现
// 默认获取的就是包装类
Say say = ExtensionLoader.getExtensionLoader(Say.class).getDefaultExtension();
say.say();
输出:
before...
say...
after...
复制代码
3.2 自动注入
Dubbo SPI是支持自动注入的,它类似于Spring的IOC,当扩展类的属性是另一个扩展点类型,且提供了Setter方法时,Dubbo会自动帮我们注入依赖依赖的扩展类成员对象。
假设现在有一个Eat扩展接口。
@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...");
}
}
复制代码
SayA依赖了Eat扩展。
public class SayA implements Say {
public Eat eat;
public void setEat(Eat eat) {
this.eat = eat;
}
}
复制代码
当我们获取SayA实现时,Dubbo会自动帮我们注入Eat扩展点对象。**Eat扩展点实现类可能有很多,该注入哪一个呢?**这就和下面要说的「自适应」有关了,其实Dubbo注入的始终是一个自适应扩展,它会根据参数中的URL去判断具体调用哪个实现。
3.3 自适应
SPI扩展点可能存在这种情况:扩展点实现类有很多,无法硬编码指定,需要运行时动态根据参数来确定具体实现类。为了实现该需求,Dubbo SPI实现了自适应调用。
自适应需要用到@Adaptive
注解,它可以加在类或方法上。加在类上,该类就是自适应类;加在方法上,会自动生成代理类,通过URL对象里的参数进行匹配,以确定具体实现。
自适应调用的实现原理并不复杂,Dubbo利用Javassist技术给扩展接口动态的生成了自适应代理类,类名的规则是XXX$Adaptive
,在代理类中,根据URL对象中的参数,去匹配具体的扩展点实现类。
@SPI
public interface Say {
// 匹配URL中的参数key
@Adaptive({"key"})
void say(URL url);
}
复制代码
假设有a、b两个实现,自适应调用如下:
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"));
输出:
sayA...
sayB...
复制代码
3.4 自动激活
场景:某个扩展点的多个实现类需要根据规则同时启用,例如Filter过滤器。
自动激活需要使用@Activate
注解,一旦加上该注解表示该实现类需要根据条件自动激活,注解属性含义如下:
属性 | 说明 |
---|---|
group | Group匹配成功则激活 |
value | URL中存在该Key则激活 |
order | 扩展点执行顺序 |
假设有Filter接口:
@SPI
public interface Filter {
void invoke();
}
复制代码
FilterA代表在consumer组、且URL中存在xxx参数时自动激活,顺序为1。
@Activate(group = {"consumer"}, value = {"xxx"}, order = 1)
public class FilterA implements Filter {
@Override
public void invoke() {
System.err.println("FilterA...");
}
}
复制代码
FilterB代表在provider组、且URL中存在ooo参数时自动激活,顺序为2。
@Activate(group = {"provider"}, value = {"ooo"}, order = 2)
public class FilterB implements Filter {
@Override
public void invoke() {
System.err.println("FilterB...");
}
}
复制代码
获取激活的扩展点实现类对象集合,如下仅会输出FilterA,FilterB的Group匹配失败了。
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);
输出:
demo.spi.activate.FilterA
复制代码
4. 源码分析
Dubbo SPI的核心类是ExtensionLoader
,它的主要职责就是加载扩展点实现类,以及根据各种条件获取扩展点实例。
属性说明如何:
public class ExtensionLoader<T> {
private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);
// 多个扩展点用逗号分割
private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
// 扩展点实例缓存
private final ConcurrentMap<Class<?>, Object> extensionInstances = new ConcurrentHashMap<>(64);
// 接口
private final Class<?> type;
// 扩展依赖注入器
private final ExtensionInjector injector;
// 扩展类名称缓存
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
// 扩展类缓存
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
// 自动激活扩展实例缓存
private final Map<String, Object> cachedActivates = Collections.synchronizedMap(new LinkedHashMap<>());
// 扩展类激活的Group缓存
private final Map<String, Set<String>> cachedActivateGroups = Collections.synchronizedMap(new LinkedHashMap<>());
// 扩展类激活的Value缓存
private final Map<String, String[]> cachedActivateValues = Collections.synchronizedMap(new LinkedHashMap<>());
// 扩展实例缓存
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// 动态生成的自适应实例缓存
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
// 动态生成的自适应类
private volatile Class<?> cachedAdaptiveClass = null;
// 默认扩展名称
private String cachedDefaultName;
// 动态创建自适应实例发生的异常
private volatile Throwable createAdaptiveInstanceError;
// 包装类缓存
private Set<Class<?>> cachedWrapperClasses;
// 异常缓存
private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<>();
/**
* 扩展类Class加载策略:默认从三个路径加载
* 1. META-INF/dubbo/internal/
* 2. META-INF/dubbo/
* 3. META-INF/services/
*/
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
/**
* Record all unacceptable exceptions when using SPI
* 记录加载扩展点时出现的异常
*/
private Set<String> unacceptableExceptions = new ConcurrentHashSet<>();
//
private ExtensionDirector extensionDirector;
// 扩展点后置处理
private List<ExtensionPostProcessor> extensionPostProcessors;
// 扩展类实例化策略
private InstantiationStrategy instantiationStrategy;
private Environment environment;
// 自动激活扩展点排序
private ActivateComparator activateComparator;
private ScopeModel scopeModel;
}
复制代码
ExtensionLoader是和接口绑定的,一个接口对应一个ExtensionLoader实例,获取接口对应的实例也很简单:
ExtensionLoader<Say> extensionLoader = ExtensionLoader.getExtensionLoader(Say.class);
复制代码
ExtensionLoader有三个常用方法,下面分别分析:
方法名 | 备注 |
---|---|
getDefaultExtension() | 获取默认扩展点实现类实例 |
getAdaptiveExtension() | 获取自适应实例 |
getActivateExtension() | 获取自动激活实例集合 |
4.1 默认扩展点
以getDefaultExtension()
方法为入口获取默认的扩展点实现类实例,默认情况下,如果有Wrapper类,会进行自动包装。
public T getDefaultExtension() {
// 加载实现类
getExtensionClasses();
if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
return null;
}
// 获取默认扩展点实现类实例
return getExtension(cachedDefaultName);
}
复制代码
getExtensionClasses()
方法会获取扩展点下的所有实现类,只会加载一次,加载完会进行缓存。
private Map<String, Class<?>> getExtensionClasses() {
// 优先从缓存中取
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 没有缓存,从指定路径下加载类
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
复制代码
loadExtensionClasses()
方法默认会从三个路径去加载Class,不同的加载路径被定义成一个加载策略,对应的类是LoadingStrategy。
private Map<String, Class<?>> loadExtensionClasses() {
// 缓存@SPI注解指定的默认扩展名
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
/**
* 依次从不同目录下加载
* 1. META-INF/dubbo/internal/
* 2. META-INF/dubbo/
* 3. META-INF/services/
*/
for (LoadingStrategy strategy : strategies) {
// 从指定目录加载Class
loadDirectory(extensionClasses, strategy, type.getName());
// compatible with old ExtensionFactory
if (this.type == ExtensionInjector.class) {
loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
}
}
return extensionClasses;
}
复制代码
扩展类加载完毕后,就可以根据默认的扩展名去创建实例了,默认是会进行自动包装的。
private T createExtension(String name, boolean wrap) {
// 获取扩展名对应的Class
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null || unacceptableExceptions.contains(name)) {
// Class实例创建失败过,抛出异常
throw findException(name);
}
try {
T instance = (T) extensionInstances.get(clazz);
if (instance == null) {
// 创建实例并缓存
extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));
instance = (T) extensionInstances.get(clazz);
// 前置处理
instance = postProcessBeforeInitialization(instance, name);
// Setter方法注入
injectExtension(instance);
// 后置处理
instance = postProcessAfterInitialization(instance, name);
}
if (wrap) {// 自动包装
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
// 包装类排序
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注解匹配,判断是否需要包装
if (wrapper == null
|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
// 反射创建包装类实例
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
// 包装类的后置处理
instance = postProcessAfterInitialization(instance, name);
}
}
}
}
initExtension(instance);
return instance;
}
}
复制代码
injectExtension()
方法会进行依赖注入,它会查找Class的Setter方法,然后判断它的参数是否也是扩展点,如果是就会从ExtensionAccessor中获取扩展点对应的自适应实例,然后反射赋值。 注:Dubbo SPI只能注入Adaptive实例,因此必须保证注入的扩展点是自适应的。
4.2 自适应
getAdaptiveExtension()
方法用来获取扩展点的自适应实例,它的原理并不复杂,无非就是生成扩展点的代理类,然后解析参数中URL的属性和@Adaptive注解的值做匹配,再去调用指定的扩展点实现。
自适应类会在程序运行时动态生成,可以用JDK的动态代理,也可以用类似CGLIB等字节码技术生成,Dubbo默认用的是Javassist。
自适应对象也有缓存,只会创建一次,对应的属性是cachedAdaptiveInstance
,创建自适应对象的方法是createAdaptiveExtension()
。
private T createAdaptiveExtension() {
try {
// 获取自适应类Class,创建实例
T instance = (T) getAdaptiveExtensionClass().newInstance();
// 前后置处理、Setter注入
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);
}
}
复制代码
创建自适应对象之前,首先得生成自适应Class,对应的方法是createAdaptiveExtensionClass()
。
private Class<?> createAdaptiveExtensionClass() {
ClassLoader classLoader = type.getClassLoader();
try {
if (NativeUtils.isNative()) {
return classLoader.loadClass(type.getName() + "$Adaptive");
}
} catch (Throwable ignore) {
}
// 根据Class和默认扩展名,生成Class源码
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
// 获取默认编译器:JavassistCompiler
org.apache.dubbo.common.compiler.Compiler compiler = extensionDirector.getExtensionLoader(
org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// 动态编译成Class
return compiler.compile(code, classLoader);
}
复制代码
这里给出一个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;
// 获取URL中的参数,key是@Adaptive注解指定的
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);
// 获取key指定的扩展名对应的实现类
demo.spi.adaptive.Say extension = (demo.spi.adaptive.Say) scopeModel.getExtensionLoader(demo.spi.adaptive.Say.class).getExtension(extName);
extension.say(arg0);
}
}
复制代码
4.3 自动激活
getActivateExtension()
方法用来获取自动激活的扩展点实例集合,如果希望某个扩展点实现自动激活,只需要在类上加@Activate即可,还可以配置Group和Value来设置自动激活的条件。例如某些扩展点只会在Provider端激活,而有些只会在Consumer端激活。
public @interface Activate {
// 自动激活时匹配的Group
String[] group() default {};
// 自动激活时匹配的Value
String[] value() default {};
// 扩展点顺序
int order() default 0;
}
复制代码
首先,需要从URL中解析出Key对应的Value,多个扩展点名称用逗号分割。
public List<T> getActivateExtension(URL url, String key, String group) {
// 获取Key对应的Value
String value = url.getParameter(key);
// Value使用,拆分
return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
}
复制代码
激活的扩展点实例使用TreeMap存储,Key会按照注解里的order属性进行排序。
Map<Class<?>, T> activateExtensionsMap = new TreeMap<>(activateComparator);
List<String> names = values == null ? new ArrayList<>(0) : asList(values);
Set<String> namesSet = new HashSet<>(names);
复制代码
Value如果配置了-default
,则会排除默认的自动激活类,反之会先加载默认的激活类,此时并不会加载Value指定的扩展类。
// 没有-default,先加载默认扩展点(Value和Group匹配成功的)
if (!namesSet.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
if (cachedActivateGroups.size() == 0) {
synchronized (cachedActivateGroups) {
// cache all extensions
if (cachedActivateGroups.size() == 0) {
// 加载配置的扩展类
getExtensionClasses();
// 遍历@Activate类,缓存类的Group和Value配置
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) {
// 兼容旧注解
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匹配
&& !namesSet.contains(name)// 被Key指定的扩展点后面会加载
&& !namesSet.contains(REMOVE_VALUE_PREFIX + name)
// Value匹配
&& isActive(cachedActivateValues.get(name), url)) {
// Group和Value均匹配成功,且没有被Key指定的默认扩展点,这里会加载。
activateExtensionsMap.put(getExtensionClass(name), getExtension(name));
}
});
}
复制代码
如果Value指定了default
,会影响扩展点的顺序,default内的扩展点依然是有序的,但是default前后的扩展点将不会根据order排序,例如:
`extA,default,extB`
extA的顺序将在所有默认扩展点之前,extB的顺序将在所有默认扩展点之后
复制代码
代码如下:
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;
}
复制代码
如果Value没有指定default
,那么所有扩展点实例将全部存储在TreeMap中,全部都是有序的。
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());
复制代码
5. 总结
SPI机制使用了策略模式,一个接口多种实现,开发者面向接口编程,具体实现并不在程序中硬编码指定,而是通过配置文件的方式在外部指定。Java内置了SPI机制,但是存在一些缺陷,例如:不支持按需加载,浪费资源,排查困难等等,因此Dubbo自己定义了一套规范,开发了自己的SPI功能。
Dubbo SPI进行了大量的优化和功能增强,它支持按需加载,并且对扩展对象做了缓存,不会重复创建对象。获取扩展对象的方式更加灵活,还增加了诸如自动包装、IOC和AOP、自动激活、自适应调用等多重高级特性。