一文带你彻底了解Dubbo的SPI机制
文章目录
Java SPI使用
Dubbo为何自己实现一套SPI
Dubbo SPI初体验
SPI
IOC
Aop
什么是包装类?
AOP增强
@Adaptive
一些需要注意的地方
@Activate
结尾
在分布式系统中服务的调用,就要涉及到RPC。而提起RPC,想到最多的就是dubbo。了解dubbo的工作原理,有助于我们更好的使用它。
打开下载的dubbo工程,我发现在dubbo的各个子模块,有很多这样的类似代码:ExtensionLoader.getExtensionLoader(xxx.class),经过我一番百度后发现他们指向一个名次:SPI。官网中表示Dubbo采用微内核+SPI,使得有特殊需求的接入方可以自定义扩展,做定制的二次开发。好像解释了什么,但是又好像还不清楚它是干什么的,不过不要急,接下来我们就从例子使用到dubbo的源码来剖析spi的神秘面纱。
Java SPI使用
在了解dubbo spi之前,我们就不得不提一下Java SPI。SPI的全称是service provider interface,起初是提供给厂商做插件开发的 ,它使用了策略模式, 一个接口多种实现。我们只声明接口, 具体的实现并不在程序中直接确定, 而是由程序之外的配置掌控 。啥意思呢?我们搞个例子就明白了:
//定义一个接口
public interface DoWork {
void doWork();
}
//两个实现类
public class WriteBug implements DoWork {
@Override
public void doWork() {
System.out.println("写bug");
}
}
public class WriteCode implements DoWork {
@Override
public void doWork() {
System.out.println("写程序");
}
}
在META-INF.services目录下创建一个文件,文件名即为接口的全路径名:com.alibaba.dubbo.demo.provider.JavaSPI.DoWork:这个样子;
文件中的内容:
com.alibaba.dubbo.demo.provider.JavaSPI.WriteBug
com.alibaba.dubbo.demo.provider.JavaSPI.WriteCode
准备工作已经做完了,接下来测试一下效果如何:
public static void main(String[] s){
//获取到DoWork的所有实现
ServiceLoader<DoWork> serviceServiceLoader = ServiceLoader.load(DoWork.class);
for (DoWork doWork : serviceServiceLoader){
doWork.doWork();
}
}
运行结果:
写bug
写程序
可以看到执行了全部实现类的方法,因此我们可以理解为spi实际上就是在文件中记录接口都有哪些实现类,然后根据接口名来实例化它的实现类。
Dubbo为何自己实现一套SPI
但是实际上dubbo并没有采用Java的SPI,而是自己实现了一套SPI机制。既然有Java的SPI,为什么dubbo不用呢?
我们观察上面的例子,可以发现其实Java SPI实际上是将接口所有的实现类都加载出来了,如果项目过大,那么会加载全部的实现类,那肯定会有一些实现类实现类没有用上,这样就造成了浪费。
另一方面,Java的SPI功能也比较单一,dubbo的spi在此基础上还实现了ioc、aop等功能,这些我会在下面从源码的角度来分析这些功能。
Dubbo SPI初体验
既然我们上面看了Java的SPI如何使用,那这个dubbo的spi我们也是需要看看的。
在dubbo中,约定文件是放在以下三个目录中:
META-INF/services/
META-INF/dubbo/
META-INF/dubbo/internal/
大部分还是比较类似的,先定义一个接口、两个实现类,要注意接口上面需要加@SPI注解的。
//注意加上@SPI注解
@SPI
public interface DoWork {
void doWork();
}
public class WriteBug implements DoWork {
@Override
public void doWork() {
System.out.println("写bug");
}
}
public class WriteCode implements DoWork{
@Override
public void doWork() {
System.out.println("写程序");
}
}
dubbo spi文件内容与java spi文件内容略微不太相同,可以理解为key-value的形式,value就是实现类的全路径名,这个key呢可以理解为这个类名的简称,这个要记得,下面会用得到:
bug=com.alibaba.dubbo.demo.provider.DubboSPI.WriteBug
code=com.alibaba.dubbo.demo.provider.DubboSPI.WriteCode
public static void main(String[] args) {
ExtensionLoader<DoWork> extensionLoader = ExtensionLoader.getExtensionLoader(DoWork.class);
//这个code就是上面说的"简称"
DoWork code = extensionLoader.getExtension("code");
code.doWork();
DoWork bug = extensionLoader.getExtension("bug");
bug.doWork();
}
ExtensionLoader类包含了整个SPI的核心方法,包括像下面的获取@Adaptive、@Active注解的信息等,都是在这个类中实现的。
getExtensionLoader方法可以获得对应接口的Extension加载器,这个方法也是比较简单的;
@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!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);//从缓存中拿
if (loader == null) {
//缓存中没有的话,就去new一个放到这个map中,然后再获取返回
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
其实就是从缓存中拿,如果缓存中有,那么就取缓存中的,缓存中没有,那么就new一个放进去。
ok,现在获取到了扩展实现类加载器,接下来就看如何通过执行getExtension(很重要,可以标记为五星重要程度,后面还会用到的)来获取到对应的实例。
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
return getDefaultExtension();
}
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {//双检锁
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
代码行数虽然比较多,实际上都是从缓存中取,看是否有值,没有值的话就去创建。因为我们现在是首次加载,那肯定是没值的,所以就要到这个createExtension方法中:
private T createExtension(String name) {
Class<?> clazz = getExtensionClasses().get(name);//获取名称对应的class
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//通过反射来创建实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);//依赖注入
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {//对于那些wrapper类型的,进行包装一下
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);
}
}
这里的核心方法就两个:getExtensionClasses、injectExtension。这两个方法在后面的@Adaptive、@Active注解中都会用到。
首先是这个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方法。
private Map<String, Class<?>> loadExtensionClasses() {
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];
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
还记得最开始的时候说的dubbo会扫描三个目录下的文件么?没错就是这三个loadDirectory方法里面的目录。
最终调用的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.");
}
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());
}
} else if (isWrapperClass(clazz)) {//包装类的话,可以放到set中,这个是可以有多个的。
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {//说明既不是@Adaptive,也不是装饰类,他只是普通的或者是active。
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 activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);//设置class与名字的映射
}
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());
}
}
}
}
}
在这里我给大家总结一下这个方法:先看这个类上面有没有@Adaptive注解。如果有@Adaptive注解,那么就将它缓存起来,原则上一个接口只允许有一个@Adaptive,这是为啥呢?我们可以看上面的代码,如果有多个@Adaptive注解的话,他是会抛出异常的。
然后再看这个接口的实现类是否有以Wrapper结尾的实现类,比如Protocol的其中一个实现类:ProtocolFilterWrapper,如果是的话,就会放到一个set集合中。如果这两种都不是的话,那么就是普通的扩展类,将他们存到一个map中返回就可以了。在存到这个map的时候,是以类对应的简写为key,类的路径为value。
IOC
我们可以先看下这个injectExtension中有这么一段方法:
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
/
可以看到,它是先获取到这个实例对应类中的全部方法,然后呢,依次循环遍历,对于那些以set开头、只有一个参数、并且为public的方法,我们就认为这是需要对这个实例进行属性注入的部分。
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3,4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
}
这个其实就是要获取到需要注入的属性,就是将第四个字符设置为小写,后面的不动进行分割。比如setUserName,分割完毕后就是userName,这样有属性,又有类路径,就可以通过反射对原先的实例进行属性注入,也就完成了依赖注入。
Aop
这里分为两部分:判断是否为包装类以及对目标对象进行增强。
什么是包装类?
举个简单的栗子:
public class Son implements Parent {
private Parent parent;
public Son(Parent parent) {
this.parent = parent;
}
}
在dubbo中是这个样子的:
也就是实现类的构造方法的参数,为他自己所实现的接口。而且在dubbo中。包装类都是以Wrapper结尾的。
缓存包装类
缓存包装类是在ExtensionLoader.loadClass中:
else if (isWrapperClass(clazz)) {//包装类的话,可以放到set中,这个是可以有多个的。
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
}
private boolean isWrapperClass(Class<?> clazz) {
try {
Constructor<?> constructor = clazz.getConstructor(type);//看类的构造参数中,有没有对应接口的参数,如果有,就说明是包装类
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
通过遍历所有的实现类,找出这些包装类,然后放入缓存中。
AOP增强
@SuppressWarnings("unchecked")
private T createExtension(String name) {
Map<String, Class<?>> extensionClasses = getExtensionClasses();
Class<?> clazz = extensionClasses.get(name);//获取名称对应的class
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//通过反射来创建实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);//依赖注入
//获取包装类集合
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
//遍历
for (Class<?> wrapperClass : wrapperClasses) {
//对返回的对象进行包装增强
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);
}
}
@Adaptive
@SPI注解那里存的是默认值,这个会在动态生成编译的类中,会设置默认值
从上面的例子中可以看到,通过SPi,我们可以每次只加载对应接口的实现类,这样可以减少加载全部实现类带来的开销。但是我又有一个问题了,我不想在启动项目的时候加载这些扩展类,而是希望在运行时根据参数,来动态加载扩展类,这个该怎么做呢?
此时@Adaptive注解就派上用场了。
@Adaptive有两种用法:一种是添加到类上,另一种则是添加到方法上。
如果添加到类上,表示该类是接口的适配器。但是实际上,注解添加到类上的是很少的,大部分都是添加到方法上的。目前只有AdaptiveExtensionFactory和AdaptiveCompiler是添加到类上的。
在这里我们就以这个AdaptiveExtensionFactory为例,来看下这个:
AdaptiveExtensionFactory的类上有这个@Adaptive注解,在执行构造方法的时候,就会将ExtensionLoader对应的实现类加载到一个List中缓存起来,这样的话在getExtension的时候,传入要取的class,key,就可以直接从这个list中取出对应的值。
2.事实上大部分情况下都是注解到方法上的,他会根据接口的信息,来动态拼接成一个代理类。为了更清楚了解@Adaptive的实现,我们就从源码的角度,以RegistryFactory为例子来看下这个过程到底是啥样的:
方法起始于ExtensionLoader.getAdaptiveExtension:
public T getAdaptiveExtension() {
//从缓存中拿,没有的话就创建
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
return (T) instance;
}
private T createAdaptiveExtension() {
//主要方法为getAdaptiveExtensionClass
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
}
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();//通过这个方法来获取到Adaptive注解的类。
if (cachedAdaptiveClass != null) {//如果有,那么就说明有@Adaptive注解添加到类上了。
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();//没有的话就创建。注解在方法上的话,那么就需要进行编译。
}
这里我省略了一些无关紧要的代码,保留了核心代码,getExtensionClasses这个方法是不是感觉很熟悉呢?没错,在上面我们也介绍过他的源码了,这里就不多说了,直接ctrl+f搜索就可以了。
因为这个接口上的@Adaptive是在接口上的,所以cachedAdaptiveClass是为空的,这里就需要执行createAdaptiveExtensionClass去创建实现类了。
private Class<?> createAdaptiveExtensionClass() {
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
这个code里面就是包含了代理实现类的所有信息,他长这个样子:
看起来不太直接,而且也没办法调试,我们可以将他复制下来,ctrl+alt+l进行格式化一下,这样在下次启动程序的时候,断点就可以进入这个类中了。
进入这个方法中,根据传入的URL中的参数值,来调用getExtension方法,最终得到需要的扩展类。
PS:在dubbo3.0的时候项目中就已经放了这些$Adaptive的类了。
一些需要注意的地方
当@Adaptive添加到方法上的时候,要保证方法中的参数至少有一个为URL。
但是我们发现有的方法中的参数,只有一个invoker,比如这样的:
这个方法中就没有URL,那这是咋回事?
通过查看拼接生成的代理类,我们发现,通过Invoker是可以得到URL的:
所以从某种意义上来说的话,填入了Invoker也相当于填入了URL。
关于SPI注解与@Adaptive注解中的值。
SPI注解后面的值是默认的值,当URL中的值为空的时候,就使用默认的值,比如上面图片中的:
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
这个dubbo就是默认值,也是@SPI中的值。
而@Adaptive添加到方法上时,后面的参数表示在加载时优先使用的值。
@Activate
这个注解是扩展点自动激活加载,在这个注解上可以设置group、value、order等值。
这些值主要表示注解在的类、方法上所属的分组、执行顺序等,当传入的参数符合直接中的值的时候,可以进行激活。
这个注解对应的源码,其实大部分在上面都已经说过了,总的来说,并不是特别的难,源码在这里,大家可以先看一下:
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
//1
getExtensionClasses();
for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Activate activate = entry.getValue();
if (isMatchGroup(group, activate.group())) {
if (!names.contains(name)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
&& isActive(activate, url)) {
//2
T ext = getExtension(name);
exts.add(ext);
}
}
}
//排序
Collections.sort(exts, ActivateComparator.COMPARATOR);
}
List<T> usrs = new ArrayList<T>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
if (Constants.DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
//3
T ext = c(name);
usrs.add(ext);
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
他的核心方法就是我在里面标记的这几个,其余的呢,都是循环啦,取值之类的操作,没有什么太多的复杂逻辑。
结尾
以上就是dubbo spi的核心内容了,因为spi贯穿了dubbo的所有核心功能,所以要想看懂dubbo的源码,那么就需要先了解SPI这个前置任务。大家可以一边对照着文章,一边打断点,这样对SPI也会了解更深入。
作者:java知路
欢迎关注微信公众号 :java知路