Dubbo浅谈之SPI

前言

​ 突然发现,自己手动搭建一个项目,从头到尾自己来还是挺锻炼人的,今天尝试了自己在本地使用Dubbo+zookeeper搭建了两个项目,一个作为服务提供端,一个作为消费端,搭建的过程中,问题重重。简直不要太麻烦。

​ 首先在本地启动dubbo-admin就废了好长时间,服务端启动正常,但是消费端没有找到服务,然后又从头来弄服务端,总之,一番周折之后,一个简单的项目能正常跑起来了。在翻阅官方文档的时候,发现了挺有趣的东西,有对源码的分析,写的还挺详细的。

​ 在之前使用WCF的过程中,和这个类似,由于是微软的东西,没有开源,也没有仔细研究怎么实现的,今天就借着官网的文章,来仔细看一下dubbo是怎么一回事

Dubbo怎么用

​ 这个挺简单的,我个人建议先装dubbo-admin,这样,你才好判断问题在哪,比如是服务没有注册,或者是服务注册成功了,但是消费端出现问题。总之,装了dubbo-admin之后,你才好定位问题在哪。dubbo-admin怎么装就不写了,百度一大堆。

​ 然后就是使用了,先写服务,能注册成功了,在去写消费端,好像是废话。spring boot集成化挺高的,什么东西配置一下就好了,反正不难,出问题百度。啥都有

SPI

​ 至于dubbo怎么用就不说了,要用上这个东西,挺简单,跟着项目走就好了,无非写方法,调用方法,只不过这个是分布式的,可以跨项目调用,跨项目调用的好处就不言而喻了。中间的实现过程就不需要你管,都有dubbo来做

​ 来说说SPI吧,SPI(Service Provider Interface)服务发现机制,SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这是官网的话,我可以这么理解,就是将接口的实现类的路径放在一个文件里面,然后需要调用这个方法的时候,我们就去这个文件里面找这个配置的实现类。这样看来,就挺简单的,可配置的东西,理解起来就不难了。

Java SPI

​ 我在我项目里面写了Java SPI的demo源码如下:

​ 首先在resources的下面创建META-INF这个文件夹,然后在下级创建services这个文件夹,为什么叫这个名字,你去看看ServiceLoader的源码就能知道,它是固定使用这个去作为前缀查找配置的实现类

​ 在然后在下面创建一个文件 :这里是重点,需要创建一个以接口全限定名的配置文件,具体操作就是找到需要代理的接口,然后右键Copy Reference,就是文件名,文件里面的内容为实现类的全限定名,依旧找到实现类Copy Reference就好了。这是配置,在看看源码吧!

​ 从下面源码能看出来,主要是ServiceLoader.load去加载实现类,然后循环serviceLoader就能将Robot的所有实现方法全部调用

1
2
3
ServiceLoader<Robot> serviceLoader=ServiceLoader.load(Robot.class);
System.out.println("java SPI");
serviceLoader.forEach(Robot::sayHello);

这里就能看到Java SPI的缺点,它只能通过遍历全部获取,也就是说接口的实现类全部加载实例化了一遍,但是如果接口实现类多了,就很浪费了。换句话说,就是不能通过参数去获取指定的实现类。Dubbo就能。

Dubbo SPI

依旧说说源码:配置的话,还是在META-INF下面创建一个dubbo文件夹,然后dubbo文件夹里面依旧创建一个和Java SPI一样的全限定名的文件。

区别来了:在Dubbo SPI里面创建实现类是采用的键值对的形式创建的 key=com.XXX.XXX 这个key就是用来我们后续需要获取指定实现类的关键

下方先获取了接口的所有实现类,然后根据不同的key值,去取不同的实现类,来使用。对的,还需要在接口上面添加@SPI注解,这样ExtensionLoader才能识别到

1
2
3
4
5
ExtensionLoader<Robot> extensionLoader=ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimmusProme=extensionLoader.getExtension("OptimusProme");
optimmusProme.sayHello();
Robot bumblebee=extensionLoader.getExtension("Bumblebee");
bumblebee.sayHello();

demo就到这里,这篇文章的重点不是这个,咱们是来看源码的。

Dubbo SPI实现源码

源码的顺序就从上面的代码来看

getExtensionLoader

获取一个ExtensionLoader实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
} else if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
} else if (!withExtensionAnnotation(type)) { //如果接口上没有@SPI注解,抛异常
throw new IllegalArgumentException("Extension type(" + type + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
} else {
//先尝试获取,没有就实例化一个
ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
}

return loader;
}
}
getExtension

获取实现类的对象实例

看上去和上面的方法挺类似的,先从缓存里面获取实例,没有就new一个,然后从目标对象里面获取方法,如果在没有就去createExtension创建一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public T getExtension(String name) {
if (name != null && name.length() != 0) {
if ("true".equals(name)) {
//创建默认的实例
return this.getDefaultExtension();
} else {
//先从缓存获取目标对象
Holder<Object> holder = (Holder)this.cachedInstances.get(name);
if (holder == null) {
this.cachedInstances.putIfAbsent(name, new Holder());
holder = (Holder)this.cachedInstances.get(name);
}

Object instance = holder.get();
//双重检查
if (instance == null) {
synchronized(holder) {
instance = holder.get();
if (instance == null) {
//创建这个实例
instance = this.createExtension(name);
holder.set(instance);
}
}
}

return instance;
}
} else {
throw new IllegalArgumentException("Extension name == null");
}
}
createExtension

创建实例化对象

我直接在源码里面注释几个重点的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private T createExtension(String name) {
//获取所有扩展类的实例,然后根据名字获取当前需要获取的实例对象
Class<?> clazz = (Class)this.getExtensionClasses().get(name);
if (clazz == null) {
throw this.findException(name);
} else {
try {
T instance = EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//通过反射获取实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = EXTENSION_INSTANCES.get(clazz);
}
//注入,但是只支持前缀为set的方法实现注入
this.injectExtension(instance);
Set<Class<?>> wrapperClasses = this.cachedWrapperClasses;
Class wrapperClass;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for(Iterator i$ = wrapperClasses.iterator(); i$.hasNext(); instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance))) {
wrapperClass = (Class)i$.next();
}
}

return instance;
} catch (Throwable var7) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " + this.type + ") could not be instantiated: " + var7.getMessage(), var7);
}
}
}

这个里面的重点是getExtensionClasses()是获取所有扩展的实例

getExtensionClasses

这个获取所有扩展的实例,先从缓存里面获取,没有就去加载扩展的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

加载所有扩展实例对象,我们只用看下面的loadDirectory就好,上面的是在其他方法调用。

loadDirectory的第二个参数是三个文件夹,表示,他会从这三个文件夹去找扩展类的实例化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
loadDirectory

从文件夹里面找到接口全限定名的扩展实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
String fileName = dir + type.getName(); //获取文件夹全路径
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

其实这方法主要是读取文本,解析字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
String line;
while ((line = reader.readLine()) != null) {
//不要注释
final int ci = line.indexOf('#');
if (ci >= 0) line = line.substring(0, ci);
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
//按=分割,截取键值对
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
//加载扩展的实例,并且载入缓存
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}

好了,到这里没什么好讲的了,Dubbo SPI和Java SPI的区别最明显的就是 dubbo SPI是采用键值对的形式,需要哪个实例就去取相应的实例就好了,而Java SPI是遍历使用所有

总结

有一个全面的文档真好,而且是中文文档,太友好了,不懂的,看看文档,看看代码,调试一下程序,基本就能看个八九不离十,好了,不说了,划水去了。

-------------本文结束感谢您的阅读-------------
0%