ITPub博客

首页 > 应用开发 > Java > SPI 在 Dubbo中 的应用

SPI 在 Dubbo中 的应用

原创 Java 作者:vivo互联网技术 时间:2020-12-16 10:41:13 0 删除 编辑

通过本文的学习,可以了解 Dubbo SPI 的特性及实现原理,希望对大家的开发设计有一定的启发性。

一、概述

SPI 全称为 Service Provider Interface,是一种模块间组件相互引用的机制。其方案通常是提供方将接口实现类的全名配置在classPath下的指定文件中,由调用方读取并加载。这样需要替换某个组件时,只需要引入新的JAR包并在其中包含新的实现类和配置文件即可,调用方的代码无需任何调整。优秀的SPI框架能够提供单接口多实现类时的优先级选择,由用户指定选择哪个实现。

得益于这些能力,SPI对模块间的可插拔机制和动态扩展提供了非常好的支撑。

本文将简单介绍JDK自带的SPI,分析SPI和双亲委派的关系,进而重点分析DUBBO的SPI机制;比较两者有何不同,DUBBO的SPI带来了哪些额外的能力。

二、JDK自带SPI

提供者在classPath或者jar包的META-INF/services/目录创建以服务接口命名的文件,调用者通过java.util.ServiceLoader加载文件内容中指定的实现类。

1. 代码示例

  • 首先定义一个接口Search

search示例接口

package  com.example.studydemo.spi;
public  interface  Search {
     void  search();
}
  • 实现类FileSearchImpl实现该接口

文件搜索实现类

1
2
3
4
5
6
7
package  com.example.studydemo.spi;
public  class  FileSearchImpl  implements  Search {
     @Override
     public  void  search() {
         System.out.println( "文件搜索" );
     }
}
  • 实现类DataBaseSearchImpl实现该接口

数据库搜索实现类

1
2
3
4
5
6
7
package  com.example.studydemo.spi;
public  class  DataBaseSearchImpl  implements  Search {
     @Override
     public  void  search() {
         System.out.println( "数据库搜索" );
     }
}
  • 在项目的META-INF/services文件夹下,创建Search 文件

文件内容为:

1
2
com.example.studydemo.spi.DataBaseSearchImpl
com.example.studydemo.spi.FileSearchImpl

测试:

1
2
3
4
5
6
7
import  java.util.ServiceLoader;
public  class  JavaSpiTest {
     public  static  void  main(String[] args) {
         ServiceLoader<Search> searches = ServiceLoader.load(Search. class );
         searches.forEach(Search::search);
     }
}

结果为:

2. 简单分析

ServiceLoader作为JDK提供的一个服务实现查找工具类,调用自身load方法加载Search接口的所有实现类,然后可以使用for循环遍历实现类进行方法调用。

有一个疑问:META-INF/services/目录是硬编码的吗,其它路径行不行?答案是不行。

跟进到ServiceLoader类中,第一行代码就是private static final String PREFIX = “META-INF/services/”,所以SPI配置文件只能放在classPath或者jar包的这个指定目录下面。

ServiceLoader的文件载入路径

1
2
3
4
5
6
7
8
9
10
11
public  final  class  ServiceLoader<S>
     implements  Iterable<S>
{
     //硬编码写死了文件路径
     private  static  final  String PREFIX =  "META-INF/services/" ;
  
     // The class or interface representing the service being loaded
     private  final  Class<S> service;
  
     // The class loader used to locate, load, and instantiate providers
     private  final  ClassLoader loader;

JDK SPI的使用比较简单,做到了基本的加载扩展组件的功能,但有以下几点不足:

  • 需要遍历所有的实现并实例化,想要找到某一个实现只能循环遍历,一个一个匹配;
  • 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名,导致在程序中很难去准确的引用它们;
  • 扩展之间彼此存在依赖,做不到自动注入和装配,不提供上下文内的IOC和AOP功能;
  • 扩展很难和其他的容器框架集成,比如扩展依赖了一个外部spring容器中的bean,原生的JDK SPI并不支持。

三、SPI与双亲委派

1.  SPI加载到何处

基于类加载的双亲委派原则,由JDK内部加载的class默认应该归属于bootstrap类加载器,那么SPI机制加载的class是否也属于bootstrap呢 ?

答案是否定的,原生SPI机制通过ServiceLoader.load方法由外部指定类加载器,或者默认取Thread.currentThread().getContextClassLoader()线程上下文的类加载器,从而避免了class被载入bootstrap加载器。

2.SPI是否破坏了双亲委派

双亲委派的本质涵义是在rt.jar包和外部class之间建立一道classLoader的鸿沟,即rt.jar内的class不应由外部classLoader加载,外部class不应由bootstrap加载。

SPI仅是提供了一种在JDK代码内部干预外部class文件加载的机制,并未强制指定加载到何处;外部的class还是由外部的classLoader加载,未跨越这道鸿沟,也就谈不上破坏双亲委派。

原生ServiceLoader的类加载器

1
2
3
4
//指定类加载器
public  static  <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)
//默认取前线程上下文的类加载器
public  static  <S> ServiceLoader<S> load(Class<S> service)

四、Dubbo SPI

Dubbo借鉴了Java SPI的思想,与JDK的ServiceLoader相对应的,Dubbo设计了ExtensionLoader类,其提供的功能比JDK更为强大。

1. 基本概念

首先介绍一些基本概念,让大家有一个初步的认知。

  • 扩展点(Extension Point):是一个Java的接口。
  • 扩展(Extension):扩展点的实现类
  • 扩展实例(Extension Instance):扩展点实现类的实例。
  • 自适应扩展实例(Extension Adaptive Instance)

自适应扩展实例其实就是一个扩展类的代理对象,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。

比如一个Search的扩展点,有一个search方法。有两个实现FileSearchImpl和DataBaseSearchImpl。Search的自适应实例在调用接口方法的时候,会根据search方法中的参数,来决定要调用哪个Search的实现。

如果方法参数中有name=FileSearchImpl,那么就调用FileSearchImpl的search方法。如果name=DataBaseSearchImpl,就调用DataBaseSearchImpl的search方法。 自适应扩展实例在Dubbo中的使用非常广泛。

在Dubbo中每一个扩展点都可以有自适应的实例,如果我们没有使用@Adaptive人工指定,Dubbo会使用字节码工具自动生成一个。

  • SPI Annotation

作用于扩展点的接口上,表明该接口是一个扩展点,可以被Dubbo的ExtentionLoader加载

  • Adaptive

@Adaptive注解可以使用在类或方法上。用在方法上表示这是一个自适应方法,Dubbo生成自适应实例时会在方法中植入动态代理的代码。方法内部会根据方法的参数来决定使用哪个扩展。

@Adaptive注解用在类上代表该实现类是一个自适应类,属于人为指定的场景,Dubbo就不会为该SPI接口生成代理类,最典型的应用如AdaptiveCompiler、AdaptiveExtensionFactory等。

@Adaptive注解的值为字符串数组,数组中的字符串是key值,代码中要根据key值来获取对应的Value值,进而加载相应的extension实例。比如new String[]{“key1”,”key2”},表示会先在URL中寻找key1的值,

如果找到则使用此值加载extension,如果key1没有,则寻找key2的值,如果key2也没有,则使用SPI注解的默认值,如果SPI注解没有默认值,则将接口名按照首字母大写分成多个部分,

然后以’.’分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名会变成yyy.invoker.wrapper,然后以此名称做为key到URL寻找,如果仍没有找到则抛出IllegalStateException异常。

  • ExtensionLoader

类似于Java SPI的ServiceLoader,负责扩展的加载和生命周期维护。ExtensionLoader的作用包括:解析配置文件加载extension类、生成extension实例并实现IOC和AOP、创建自适应的extension等,下文会重点分析。

  • 扩展名

和Java SPI不同,Dubbo中的扩展都有一个名称,用于在应用中引用它们。比如
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

  • 加载路径

Java SPI从/META-INF/services目录加载扩展配置,Dubbo从以下路径去加载扩展配置文件:
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services
其中META-INF/dubbo对开发者发放,META-INF/dubbo/internal 这个路径是用来加载Dubbo内部的拓展点的。

2.  代码示例

定义一个接口,标注上dubbo的SPI注解,赋予默认值,并提供两个extension实现类

1
2
3
4
5
package  com.example.studydemo.spi;
@SPI ( "dataBase" )
public  interface  Search {
     void  search();
}
1
2
3
4
5
6
public  class  FileSearchImpl  implements  Search {
     @Override
     public  void  search() {
         System.out.println( "文件搜索" );
     }
}
1
2
3
4
5
6
public  class  DataBaseSearchImpl  implements  Search {
     @Override
     public  void  search() {
         System.out.println( "数据库搜索" );
     }
}

在META-INF/dubbo 路径下创建Search 文件

文件内容如下:

1
2
dataBase=com.example.studydemo.spi.DataBaseSearchImpl
file=com.example.studydemo.spi.FileSearchImpl

编写测试类进行测试,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
public  class  DubboSpiTest {
     public  static  void  main(String[] args) {
         ExtensionLoader<Search> extensionLoader = ExtensionLoader.getExtensionLoader(Search. class );
         Search fileSearch = extensionLoader.getExtension( "file" );
         fileSearch.search();
         Search dataBaseSearch = extensionLoader.getExtension( "dataBase" );
         dataBaseSearch.search();
         System.out.println(extensionLoader.getDefaultExtensionName());
         Search defaultSearch = extensionLoader.getDefaultExtension();
         defaultSearch.search();
     }
}

结果为:

从代码示例上来看,Dubbo SPI与Java SPI在这几方面是类似的:

  • 接口及相应的实现
  • 配置文件
  • 加载类及加载具体实现

3源码分析

下面深入到源码看看SPI在Dubbo中是怎样工作的,以Protocol接口为例进行分析。

1
2
3
4
//1、得到Protocol的扩展加载对象extensionLoader,由这个加载对象获得对应的自适应扩展类
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol. class ).getAdaptiveExtension();
//2、根据扩展名获取对应的扩展类
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol. class ).getExtension( "dubbo" );

在获取扩展实例前要先获取Protocol接口的ExtensionLoader组件,通过ExtensionLoader来获取相应的Protocol实例Dubbo实际是为每个SPI接口都创建了一个对应的ExtensionLoader。

ExtensionLoader组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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!" );
     }
     //EXTENSION_LOADERS为ConcurrentMap,存储Class对应的ExtensionLoader
     ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
     if  (loader ==  null ) {
         EXTENSION_LOADERS.putIfAbsent(type,  new  ExtensionLoader<T>(type));
         loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
     }
     return  loader;
}

EXTENSION_LOADERS是一个 ConcurrentMap,以接口Protocol为key,以ExtensionLoader对象为value;保存的是Protocol扩展的加载类,第一次加载的时候Protocol还没有自己的接口加载类,需要实例化一个。

再看new ExtensionLoader<T>(type) 这个操作,下面为ExtensionLoader的构造方法:

1
2
3
4
rivate ExtensionLoader(Class<?> type) {
     this .type = type;
     objectFactory = (type == ExtensionFactory. class  null  : ExtensionLoader.getExtensionLoader(ExtensionFactory. class ).getAdaptiveExtension());
}

每一个ExtensionLoader都包含2个值:type和objectFactory,此例中type就是Protocol,objectFactory就是ExtensionFactory。

对于ExtensionFactory接口来说,它的加载类中objectFactory值为null。

对于其他的接口来说,objectFactory都是通过ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()来获取;objectFactory的作用就是为dubbo的IOC提供依赖注入的对象,可以认为是进程内多个组件容器的一个上层引用,

随着这个方法的调用次数越来越多,EXTENSION_LOADERS 中存储的 loader 也会越来越多。

自适应扩展类与IOC

得到ExtensionLoader组件之后,再看如何获得自适应扩展实例。

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
public  T getAdaptiveExtension() {
     //cachedAdaptiveInstance为缓存的自适应对象,第一次调用时还没有创建自适应类,所以instance为null
     Object instance = cachedAdaptiveInstance.get();
     if  (instance ==  null ) {
         if (createAdaptiveInstanceError ==  null ) {
             synchronized  (cachedAdaptiveInstance) {
                 instance = cachedAdaptiveInstance.get();
                 if  (instance ==  null ) {
                     try  {
                         //创建自适应对象实例
                         instance = createAdaptiveExtension();
                         //将自适应对象放到缓存中
                         cachedAdaptiveInstance.set(instance);
                     catch  (Throwable t) {
                         createAdaptiveInstanceError = t;
                         throw  new  IllegalStateException( "fail to create adaptive instance: "  + t.toString(), t);
                     }
                 }
             }
         }
         else  {
             throw  new  IllegalStateException( "fail to create adaptive instance: "  + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
         }
     }
  
     return  (T) instance;
}

首先从cachedAdaptiveInstance缓存中获取,第一次调用时还没有相应的自适应扩展,需要创建自适应实例,创建后再将该实例放到cachedAdaptiveInstance缓存中。

创建自适应实例参考createAdaptiveExtension方法,该方法包含两部分内容:创建自适应扩展类并利用反射实例化、利用IOC机制为该实例注入属性。

1
2
3
4
5
6
7
8
private  T createAdaptiveExtension() {
     try  {
         //得到自适应扩展类并利用反射实例化,然后注入属性值
         return  injectExtension((T) getAdaptiveExtensionClass().newInstance());
     catch  (Exception e) {
         throw  new  IllegalStateException( "Can not create adaptive extenstion "  + type +  ", cause: "  + e.getMessage(), e);
     }
}

再来分析getAdaptiveExtensionClass方法,以Protocol接口为例,该方法会做以下事情:获取所有实现Protocol接口的扩展类、如果有自适应扩展类直接返回、如果没有则创建自适应扩展类。

1
2
3
4
5
6
7
8
9
10
11
//该动态代理生成的入口
private  Class<?> getAdaptiveExtensionClass() {
     //1.获取所有实现Protocol接口的扩展类
     getExtensionClasses();
     //2.如果有自适应扩展类,则返回
     if  (cachedAdaptiveClass !=  null ) {
         return  cachedAdaptiveClass;
     }
     //3.如果没有,则创建自适应扩展类
     return  cachedAdaptiveClass = createAdaptiveExtensionClass();
}

getExtensionClasses方法会加载所有实现Protocol接口的扩展类,首先从缓存中获取,缓存中没有则调用loadExtensionClasses方法进行加载并设置到缓存中,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private  Map<String, Class<?>> getExtensionClasses() {
     //从缓存中获取
     Map<String, Class<?>> classes = cachedClasses.get();
     if  (classes ==  null ) {
         synchronized  (cachedClasses) {
             classes = cachedClasses.get();
             if  (classes ==  null ) {
                 //从SPI配置文件中解析
                 classes = loadExtensionClasses();
                 cachedClasses.set(classes);
             }
         }
     }
     return  classes;
}

loadExtensionClasses方法如下:首先获取SPI注解中的value值,作为默认扩展名称,在Protocol接口中SPI注解的value为dubbo,因此DubboProtocol就是Protocol的默认实现扩展。其次加载三个配置路径下的所有的Protocol接口的扩展实现。

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
// 此方法已经getExtensionClasses方法同步过。
private  Map<String, Class<?>> loadExtensionClasses() {
     final  SPI defaultAnnotation = type.getAnnotation(SPI. class );
     if (defaultAnnotation !=  null ) {
         String value = defaultAnnotation.value();
         if (value !=  null  && (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<?>>();
     //分别从三个路径加载
     loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
     loadFile(extensionClasses, DUBBO_DIRECTORY);
     loadFile(extensionClasses, SERVICES_DIRECTORY);
     return  extensionClasses;
}
  
  
private  static  final  String SERVICES_DIRECTORY =  "META-INF/services/" ;
private  static  final  String DUBBO_DIRECTORY =  "META-INF/dubbo/" ;
private  static  final  String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY +  "internal/" ;

在加载配置路径下的实现中,其中有一个需要关注的点,如果其中某个实现类上有Adaptive注解,说明用户指定了自适应扩展类,那么该实现类就会被赋给cachedAdaptiveClass,在getAdaptiveExtensionClass方法中会被直接返回。

如果该变量为空,则需要通过字节码工具来创建自适应扩展类。

1
2
3
4
5
6
7
8
9
10
private  Class<?> createAdaptiveExtensionClass() {
     //生成类代码
     String code = createAdaptiveExtensionClassCode();
     //找到类加载器
     ClassLoader classLoader = findClassLoader();
     //获取编译器实现类,此处为AdaptiveCompiler,此类上有Adaptive注解
     com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler. class ).getAdaptiveExtension();
     //将类代码编译为Class
     return  compiler.compile(code, classLoader);
}

createAdaptiveExtensionClass方法生成的类代码如下:

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
package  com.alibaba.dubbo.rpc;
  
import  com.alibaba.dubbo.common.extension.ExtensionLoader;
  
public  class  Protocol$Adpative  implements  com.alibaba.dubbo.rpc.Protocol {
     public  void  destroy() {
         throw  new  UnsupportedOperationException( "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!" );
     }
  
     public  int  getDefaultPort() {
         throw  new  UnsupportedOperationException( "method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!" );
     }
  
     public  com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1)  throws  com.alibaba.dubbo.rpc.RpcException {
         if  (arg1 ==  null throw  new  IllegalArgumentException( "url == null" );
         com.alibaba.dubbo.common.URL url = arg1;
         String extName = (url.getProtocol() ==  null  "dubbo"  : url.getProtocol());
         if  (extName ==  null )
             throw  new  IllegalStateException( "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url("  + url.toString() +  ") use keys([protocol])" );
         com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol. class ).getExtension(extName);
         return  extension.refer(arg0, arg1);
     }
  
     public  com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0)  throws  com.alibaba.dubbo.rpc.RpcException {
         if  (arg0 ==  null throw  new  IllegalArgumentException( "com.alibaba.dubbo.rpc.Invoker argument == null" );
         if  (arg0.getUrl() ==  null )
             throw  new  IllegalArgumentException( "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null" );
         com.alibaba.dubbo.common.URL url = arg0.getUrl();
         String extName = (url.getProtocol() ==  null  "dubbo"  : url.getProtocol());
         if  (extName ==  null )
             throw  new  IllegalStateException( "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url("  + url.toString() +  ") use keys([protocol])" );
         com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol. class ).getExtension(extName);
         return  extension.export(arg0);
     }
} 

由字节码工具生成的类Protocol$Adpative在方法末尾调用了ExtensionLoader.getExtensionLoader(xxx).getExtension(extName)来满足adaptive的自适应动态特性。

传入的extName就是从url中获取的动态参数,用户只需要在代表DUBBO全局上下文信息的URL中指定protocol参数的取值,adaptiveExtentionClass就可以去动态适配不同的扩展实例。

再看属性注入方法injectExtension,针对public的只有一个参数的set方法进行处理,利用反射进行方法调用来实现属性注入,此方法是Dubbo SPI实现IOC功能的关键。

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
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())) {
                     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);
                         }
                     catch  (Exception e) {
                         logger.error(