转载

Dubbo 的 SPI 实现以及与 JDK 实现的区别

Dubbo 的 SPI 实现以及与 JDK 实现的区别

在 Java 里, 为了规范开发,制定了大量的「 规范 」与「 标准 」,这些上层的内容,大多是以接口的形式提供出来。那这些接口最终实现是谁呢,在哪里呢?

规范并不关心这个。

所谓规范,是指定了一系列内容,来指导我们的开发实现。比如 Servlet规范对于 Servlet 的行为做了说明,具体实现时,可以是 Tomcat,可以是Jetty 等等。

再比如 Java 的 JDBC 规范,规定了 Driver 提供者需要实现的内容,但具体是 Oracle,或者MySQL 都可以支持。关于JDBC 可以看之前一篇文章( 没想到你是这样的 JDBC )。在之前我们可以通过 Class.forName来进行Driver 具体实现类的加载。从JDK1.6开始,官方提供了一个名为 「 SPI 」 的机制,来更方便快捷的进行对应实现类的加载,不需要我们关心。我们所需要做的,只需要将包含实现类的 JAR 文件放到 classpath中即可。

正好最近读了一些Dubbo的源码,其中有 Dubbo 的不同于JDK的另一种 SPI实现。所以这篇我们来看 Dubbo 的 「 SPI 」实现以及与 JDK 实现的区别。

首先,什么是 SPI 呢?

SPI(Service Provider Interfaces), 可以理解成一个交给第三方实现的API。JDK文档这样描述

A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service.

在Java 中使用到SPI的这些地方:

  • JDBC

  • JNDI

  • Java XML Processing API

  • Locael

  • NIO Channel Provider

  • ……

通过这种SPI 的实现形式,我们的应用仿佛有了可插拔的能力。

我们之前的文章 Tomcat 中 的可插拔以及 SCI 的实现原理 里,也分析了 容器 中是如何做到可插拔的。

JDK中的SPI 是怎样实现的呢?

在JDK中包含一个SPI最核心的类: ServiceLoader ,在需要加载Provider类的时候,我们所要做的是:

ServiceLoader.load(Provider.class);

在JDK中规范了 Service Provider的路径,所有 Provider必须在JAR文件的META-INF/services目录下包含一个文件,文件名就是我们要实现的Service的名称全路径。比如我们熟悉的JDBC 的MySQL实现, 在mysql-connector中,就有这样一个文件

META-INF/services/java.sql.Driver

Dubbo 的 SPI 实现以及与 JDK 实现的区别

这些provider是什么时候加载的呢?

由于Provider 的加载和初始化是 Lazy 的实现,所以需要的时候,可以遍历Provider 的 Iterator,按需要加载,已经加载的会存放到缓存中。

但有些实现不想Lazy,就直接在 ServiceLoader 的load执行之后直接把所有的实现都加载和初始化了,比如这次说的JDBC,所以这里在Tomcat里有个处理内存泄漏的,可以查看之前的文章(Tomcat与内存泄露处理)

继续说回具体的加载时机。我们一般在Spring 的配置中会增加一个datasource,这个数据源一般会在启动时做为一个Bean被初始化,此时数据源中配置的driver会被设置。

这些内容传入Bean中,会调用DriverManager的初始化

static {

loadInitialDrivers ();

println ( "JDBC DriverManager initialized" );

}

loadInitialDrivers 执行的的时候,除了ServiceLoader.load外,还进行了初始化

ServiceLoader<Driver> loadedDrivers = ServiceLoader. load (Driver. class );

Iterator<Driver> driversIterator = loadedDrivers.iterator();

try {

while (driversIterator.hasNext()) {

driversIterator.next();

}

} catch (Throwable t) {

// Do nothing

}

return null

;

我们再来看 Dubbo 的SPI实现方式。如果你能看下 Dubbo 的源码就会发现,实现时并没有使用 JDK 的SPI,而是自已设计了一种。

我们以Main class启动来看看具体的实现。

我们从使用的入口处来看,第一步传入一个 接口 , 然后再传入期待的实现的名称

1 SpringContainer container = (SpringContainer) ExtensionLoader.getExtensionLoader(Container.class).getExtension("spring");

这里传入的是 Container.class , 期待的实现是 spring

 1// synchronized in getExtensionClasses
2 private Map<String, Class<?>> loadExtensionClasses() {
3 final SPI defaultAnnotation = type.getAnnotation(SPI.class);
4 if (defaultAnnotation != null) {
5 String value = defaultAnnotation.value();
6 if ((value = value.trim()).length() > 0) {
7 String[] names = NAME_SEPARATOR.split(value);
8 if (names.length > 1) {
9 throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
10 + ": " + Arrays.toString(names));
11 }
12 if (names.length == 1) cachedDefaultName = names[0];
13 }
14 }
15
16 Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
17 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
18 loadDirectory(extensionClasses, DUBBO_DIRECTORY);
19 loadDirectory(extensionClasses, SERVICES_DIRECTORY);
20 return extensionClasses;
21 }

共从三个地方加载扩展的class

  • DUBBO_INTERNAL_DIRECTORY META-INF/dubbo/internal/

  • DUBBO_DIRECTORY META-INF/dubbo/

  • SERVICES_DIRECTORY META-INF/services/

Dubbo 的 SPI 实现以及与 JDK 实现的区别

 1private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
2 String fileName = dir + type.getName();
3 try {
4 Enumeration<java.net.URL> urls;
5 ClassLoader classLoader = findClassLoader();
6 if (classLoader != null) {
7 urls = classLoader.getResources(fileName);
8 } else {
9 urls = ClassLoader.getSystemResources(fileName);
10 }
11 if (urls != null) {
12 while (urls.hasMoreElements()) {
13 java.net.URL resourceURL = urls.nextElement();
14 loadResource(extensionClasses, classLoader, resourceURL);
15 }
16 }
17 } catch (Throwable t) {
18 logger.error("Exception when load extension class(interface: " +
19 type + ", description file: " + fileName + ").", t);
20 }
21 }
这里通过classLoader,寻找符合传入的特定名称的文件, java.net.URL resourceURL = urls.nextElement();

此时会得到一个包含该文件的URLPath, 再通过 loadResource ,将资源加载

此时得到的文件内容是

spring=com.alibaba.dubbo.container.spring.SpringContainer

再进一步,将等号后面的 class 加载,即可完成。

loadClass时,并不是直接通过类似Class.forName等形式加载,而是下面这个样子:

 1private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
2 if (!type.isAssignableFrom(clazz)) {
3 throw new IllegalStateException("Error when load extension class(interface: " +
4 type + ", class line: " + clazz.getName() + "), class "
5 + clazz.getName() + "is not subtype of interface.");
6 }
7 if (clazz.isAnnotationPresent(Adaptive.class)) {
8 if (cachedAdaptiveClass == null) {
9 cachedAdaptiveClass = clazz;
10 } else if (!cachedAdaptiveClass.equals(clazz)) {
11 throw new IllegalStateException("More than 1 adaptive class found: "
12 + cachedAdaptiveClass.getClass().getName()
13 + ", " + clazz.getClass().getName());
14 }
15 } else if (isWrapperClass(clazz)) {
16 Set<Class<?>> wrappers = cachedWrapperClasses;
17 if (wrappers == null) {
18 cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
19 wrappers = cachedWrapperClasses;
20 }
21 wrappers.add(clazz);
22 } else {
23 clazz.getConstructor();
24 if (name == null || name.length() == 0) {
25 name = findAnnotationName(clazz);
26 if (name.length() == 0) {
27 throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
28 }
29 }
30 String[] names = NAME_SEPARATOR.split(name);
31 if (names != null && names.length > 0) {
32 Activate activate = clazz.getAnnotation(Activate.class);
33 if (activate != null) {
34 cachedActivates.put(names[0], activate);
35 }
36 for (String n : names) {
37 if (!cachedNames.containsKey(clazz)) {
38 cachedNames.put(clazz, n);
39 }
40 Class<?> c = extensionClasses.get(n);
41 if (c == null) {
42 extensionClasses.put(n, clazz);
43 } else if (c != clazz) {
44 throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
45 }
46 }
47 }
48 }
49 }

加载之后,需要对class进行初始化,此时直接newInstance一个,再通过反射注入的方式将对应的属性设置进去。

 1private T createExtension(String name) {
2 Class<?> clazz = getExtensionClasses().get(name);
3 if (clazz == null) {
4 throw findException(name);
5 }
6 try {
7 T instance = (T) EXTENSION_INSTANCES.get(clazz);
8 if (instance == null) {
9 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
10 instance = (T) EXTENSION_INSTANCES.get(clazz);
11 }
12 injectExtension(instance);
13 Set<Class<?>> wrapperClasses = cachedWrapperClasses;
14 if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
15 for (Class<?> wrapperClass : wrapperClasses) {
16 instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
17 }
18 }
19 return instance;
20 } catch (Throwable t) {
21 throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
22 type + ") could not be instantiated: " + t.getMessage(), t);
23 }
24 }
 1private T injectExtension(T instance) {
2 try {
3 if (objectFactory != null) {
4 for (Method method : instance.getClass().getMethods()) {
5 if (method.getName().startsWith("set")
6 && method.getParameterTypes().length == 1
7 && Modifier.isPublic(method.getModifiers())) {
8 Class<?> pt = method.getParameterTypes()[0];
9 try {
10 String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
11 Object object = objectFactory.getExtension(pt, property);
12 if (object != null) {
13 method.invoke(instance, object);
14 }
15 } catch (Exception e) {
16 logger.error("fail to inject via method " + method.getName()
17 + " of interface " + type.getName() + ": " + e.getMessage(), e);
18 }
19 }
20 }
21 }
22 } catch (Exception e) {
23 logger.error(e.getMessage(), e);
24 }
25 return instance;
26 }

通过上面的描述我们看到,JDK 与 Dubbo的 SPI 实现上,虽然都是从JAR中加载对应的扩展,但还是有些明显的区别,比如:Dubbo 支持更多的加载路径,同时,并不是通过Iterator的形式,而是直接通过名称来定位具体的Provider,按需要加载,效率更高,同时支持Provider以类似IOC的形式提供等等。

关注『  Tomcat那些事儿    ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

Dubbo 的 SPI 实现以及与 JDK 实现的区别

                        转发是最大的支持 ,谢谢

更多精彩内容:

一台机器上安装多个Tomcat 的原理(回复001)

监控Tomcat中的各种数据 (回复002)

启动Tomcat的安全机制(回复003)

乱码问题的原理及解决方式(回复007)

Tomcat 日志工作原理及配置(回复011)

web.xml 解析实现(回复 012)

线程池的原理( 回复 014)

Tomcat 的集群搭建原理与实现 (回复 015)

类加载器的原理 (回复 016)

类找不到等问题 (回复 017)

代码的热替换实现(回复 018)

Tomcat 进程自动退出问题 (回复 019)

为什么总是返回404? (回复 020)

...

PS: 对于一些 Tomcat常见问 题,在公众号的【 常见问题 】菜单中,有需要的朋友欢迎关注查看。

原文  https://mp.weixin.qq.com/s/YzJzx9fwM5YwiWQzuY_9Gw
正文到此结束
Loading...