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 的实现形式,我们的应用仿佛有了可插拔的能力。
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
这些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 loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
我们再来看 Dubbo 的SPI实现方式。如果你能看下 Dubbo 的源码就会发现,实现时并没有使用 JDK 的SPI,而是自已设计了一种。
我们以Main class启动来看看具体的实现。
我们从使用的入口处来看,第一步传入一个接口, 然后再传入期待的实现的名称
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/
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的形式提供等等。
推荐内容:https://www.roncoo.com/course/list.html?courseName=Dubbo
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
五:利用java使用redis
一:下载驱动包 http://static.runoob.com/download/jedis-2.9.0.jar 二:连接测试使用 string: 1 package string; 2 3 import redis.clients.jedis.Jedis; 4 5 public class StringTest { 6 public static void main(String[] args) { 7 Jedis redis=new Jedis("localhost");//连接本都库 8 System.out.println("ping:"+redis.ping());//ping一下看看是不是已连接 9 redis.set("stringone", "helloword");//set 10 System.out.println(redis.get("stringone"));//get 11 } 12 } list: 1 package list; 2 3 import java.util.List; 4 5 import redis.clients.jedis.Jedis...
- 下一篇
dpkg:错误:正在解析文件 '/var/lib/dpkg/updates/0014' 第 0 行附近:在字段名 #padding 中有换行...
解决方案如下: sudo rm /var/lib/dpkg/updates/* sudo apt-get update python@ubuntu:~/Desktop/_Welcome_.jpg.extracted$ sudo rm /var/lib/dpkg/updates/* python@ubuntu:~/Desktop/_Welcome_.jpg.extracted$ sudo apt-get update 问题解决!!! 您可以考虑给博主来个小小的打赏以资鼓励,您的肯定将是我最大的动力。thx. 微信打赏 支付宝打赏 作者: Angel_Kitty 出处:http://www.cnblogs.com/ECJTUACM-873284962/ 关于作者:潜心机器学习以及信息安全的综合研究。如有问题或建议,请多多赐教! 版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。 特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我 声援博主:如果您觉得文章对您有帮助,可以点击右下角【...
相关文章
文章评论
共有0条评论来说两句吧...