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

摘要:在 Java 里, 为了规范开发,制定了大量的「规范」与「标准」,这些上层的内容,大多是以接口的形式提供出来。那这些接口最终实现是谁呢,在哪里呢?规范并不关心这个。所谓规范,是指定了一系列内容,来指导我们的开发实现。比方 Servlet规范对于 Servlet 的行为做了说明,具体实现时,可以是 T

在 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

这些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启动来看看具体的实现。

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

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

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

1//?synchronized?in?getExtensionClasses

2privateMap>?loadExtensionClasses()?{

3final?SPI?defaultAnnotation?=?type.getAnnotation(SPI.class);

4if(defaultAnnotation?!=null)?{

5Stringvalue?=?defaultAnnotation.value();

6if((value?=?value.trim()).length()?>0)?{

7String[]?names?=?NAME_SEPARATOR.split(value);

8if(names.length?>1)?{

9thrownewIllegalStateException("more?than?1?default?extension?name?on?extension?"+?type.getName()

10+":?"+?Arrays.toString(names));

11}

12if(names.length?==1)?cachedDefaultName?=?names[0];

13}

14}

15

16Map>?extensionClasses?=newHashMap>();

17loadDirectory(extensionClasses,?DUBBO_INTERNAL_DIRECTORY);

18loadDirectory(extensionClasses,?DUBBO_DIRECTORY);

19loadDirectory(extensionClasses,?SERVICES_DIRECTORY);

20returnextensionClasses;

21}

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

DUBBO_INTERNAL_DIRECTORY META-INF/dubbo/internal/

DUBBO_DIRECTORY META-INF/dubbo/

SERVICES_DIRECTORY META-INF/services/

1privatevoidloadDirectory(Map>?extensionClasses,Stringdir)?{

2StringfileName?=?dir?+?type.getName();

3try{

4Enumeration?urls;

5ClassLoader?classLoader?=?findClassLoader();

6if(classLoader?!=null)?{

7urls?=?classLoader.getResources(fileName);

8}else{

9urls?=?ClassLoader.getSystemResources(fileName);

10}

11if(urls?!=null)?{

12while(urls.hasMoreElements())?{

13java.net.URL?resourceURL?=?urls.nextElement();

14loadResource(extensionClasses,?classLoader,?resourceURL);

15}

16}

17}catch(Throwable?t)?{

18logger.error("Exception?when?load?extension?class(interface:?"+

19type?+",?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等形式加载,而是下面这个样子:

1privatevoidloadClass(Map>?extensionClasses,?java.net.URL?resourceURL,?Class?clazz,Stringname)?throws?NoSuchMethodException?{

2if(!type.isAssignableFrom(clazz))?{

3thrownewIllegalStateException("Error?when?load?extension?class(interface:?"+

4type?+",?class?line:?"+?clazz.getName()?+"),?class?"

5+?clazz.getName()?+"is?not?subtype?of?interface.");

6}

7if(clazz.isAnnotationPresent(Adaptive.class))?{

8if(cachedAdaptiveClass?==null)?{

9cachedAdaptiveClass?=?clazz;

10}elseif(!cachedAdaptiveClass.equals(clazz))?{

11thrownewIllegalStateException("More?than?1?adaptive?class?found:?"

12+?cachedAdaptiveClass.getClass().getName()

13+",?"+?clazz.getClass().getName());

14}

15}elseif(isWrapperClass(clazz))?{

16Set>?wrappers?=?cachedWrapperClasses;

17if(wrappers?==null)?{

18cachedWrapperClasses?=newConcurrentHashSet>();

19wrappers?=?cachedWrapperClasses;

20}

21wrappers.add(clazz);

22}else{

23clazz.getConstructor();

24if(name?==null||?name.length()?==0)?{

25name?=?findAnnotationName(clazz);

26if(name.length()?==0)?{

27thrownewIllegalStateException("No?such?extension?name?for?the?class?"+?clazz.getName()?+"?in?the?config?"+?resourceURL);

28}

29}

30String[]?names?=?NAME_SEPARATOR.split(name);

31if(names?!=null&&?names.length?>0)?{

32Activate?activate?=?clazz.getAnnotation(Activate.class);

33if(activate?!=null)?{

34cachedActivates.put(names[0],?activate);

35}

36for(Stringn?:?names)?{

37if(!cachedNames.containsKey(clazz))?{

38cachedNames.put(clazz,?n);

39}

40Class?c?=?extensionClasses.get(n);

41if(c?==null)?{

42extensionClasses.put(n,?clazz);

43}elseif(c?!=?clazz)?{

44thrownewIllegalStateException("Duplicate?extension?"+?type.getName()?+"?name?"+?n?+"?on?"+?c.getName()?+"?and?"+?clazz.getName());

45}

46}

47}

48}

49}

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

1privateTcreateExtension(String?name){

2Class?clazz?=?getExtensionClasses().get(name);

3if(clazz?==null)?{

4throwfindException(name);

5}

6try{

7T?instance?=?(T)?EXTENSION_INSTANCES.get(clazz);

8if(instance?==null)?{

9EXTENSION_INSTANCES.putIfAbsent(clazz,?clazz.newInstance());

10instance?=?(T)?EXTENSION_INSTANCES.get(clazz);

11}

12injectExtension(instance);

13Set>?wrapperClasses?=?cachedWrapperClasses;

14if(wrapperClasses?!=null&&?!wrapperClasses.isEmpty())?{

15for(Class?wrapperClass?:?wrapperClasses)?{

16instance?=?injectExtension((T)?wrapperClass.getConstructor(type).newInstance(instance));

17}

18}

19returninstance;

20}catch(Throwable?t)?{

21thrownewIllegalStateException("Extension?instance(name:?"+?name?+",?class:?"+

22type?+")??could?not?be?instantiated:?"+?t.getMessage(),?t);

23}

24}

1privateTinjectExtension(T?instance){

2try{

3if(objectFactory?!=null)?{

4for(Method?method?:?instance.getClass().getMethods())?{

5if(method.getName().startsWith("set")

6&&?method.getParameterTypes().length?==1

7&&?Modifier.isPublic(method.getModifiers()))?{

8Class?pt?=?method.getParameterTypes()[0];

9try{

10String?property?=?method.getName().length()?>3??method.getName().substring(3,4).toLowerCase()?+?method.getName().substring(4)?:"";

11Objectobject=?objectFactory.getExtension(pt,?property);

12if(object!=null)?{

13method.invoke(instance,object);

14}

15}catch(Exception?e)?{

16logger.error("fail?to?inject?via?method?"+?method.getName()

17+"?of?interface?"+?type.getName()?+":?"+?e.getMessage(),?e);

18}

19}

20}

21}

22}catch(Exception?e)?{

23logger.error(e.getMessage(),?e);

24}

25returninstance;

26}

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

欢迎工作一到五年的Java工程师朋友们加入Java架构开发:744677563

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

  • 全部评论(0)
最新发布的资讯信息
【系统环境|服务器应用】不花钱推广小程序的方法(2018-12-18 23:20)
【系统环境|服务器应用】MySQL基本使用(2018-12-18 23:20)
【系统环境|服务器应用】iOS混编Flutter优化&注意(2018-12-18 23:19)
【系统环境|服务器应用】iOS - 最易用的数据库工具类 `XWDatabase` 开源(2018-12-18 23:19)
【系统环境|服务器应用】《iOS设计模式解析》总结(2018-12-18 23:19)
【系统环境|服务器应用】#程序员入职一月就离任,领导竟直接撕破脸皮:招你进来真是瞎了眼(2018-12-18 23:19)
【系统环境|服务器应用】前台埋点统计方案思考(2018-12-18 23:19)
【系统环境|服务器应用】Java 哪些事最困扰你?(2018-12-18 23:19)
【系统环境|服务器应用】vue项目中api接口管理总结(2018-12-18 23:18)
【系统环境|服务器应用】Hive详情与核心知识点(2018-12-18 23:18)
手机二维码手机访问领取大礼包
返回顶部