阅读 345

Dubbo系列之-Dubbo SPI 机制

以下文章有参考https://book.douban.com/subject/34455777/。

1、加载机制概述

1.1 Java SPI

SPI全称是Service Provider Interface,服务提供接口。但是与分布式中的服务发现不是相同的概念。Java SPI使用了策略模式,一个接口可以有多个实现,但是具体的实现并不在程序中直接确定,而是通过外部配置掌控,使用的具体步骤如下:

  1. 定义一个接口及对应的方法
  2. 编写该接口的一个实现类
  3. 在META-INF/services目录下创建一个以接口全路径命名的文件
  4. 文件的内容是具体的实现类的全路径名
  5. 在代码中使用java.util.ServiceLoader来加载具体的实现类

1.2 扩展点加载机制的改进

Dubbo SPI做了一定的改进和优化。 JDK标准的SPI会一次性实例化扩展点所有的实现,如果有扩展实现,则初始化很慢,如果没有使用上的也加载,则浪费资源。Dubbo SPI只是加载配置文件中的类,并分成不同的种类缓存在内存中,而不会立即全部初始化。 JDK标准的SPI在初始化错误的时候,会吞掉错误信息,导致错误排查成本提高。Dubbo SPI在扩展加载失败的时候,会先抛出真实异常并打印日志。扩展点在被动加载失败的时候,如果加载失败,也不会影响其他扩展点和整个框架的使用。 Dubbo SPI 自己实现了IoC和AOP机制,一个扩展点可以通过setter方法直接注入其他扩展方法。另外Dubbo支持包装扩展类,推荐把通用的抽象逻辑放在包装类。,用于实现扩展点的AOP特性。

1.3 扩展点的配置规范

Dubbo SPI和Java SPI类似,需在要META-INF/dubbo/下放置对应的SPI配置文件,文件名称为接口名称的全路径名。配置文件的内容为key=扩展点实现类的全路径名,如果有多个实现,则使用换行符分割。其中key是Dubbo SPI 注解中传入的参数。在Dubbo启动的时候,会默认扫描META-INF/services/、MEAT-INF/dubbo/、META-INF/dubbo/internal/

1.4 扩展点的分类与缓存

Dubbo API可以分为Class缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类、包装扩展类(Wrapper类)、自适应扩展类(Adaptive类)。 Class缓存:Dubbo SPI 获取扩展类的时候,会先从缓存中读取。如果缓存中不存在,则加载配置文件,根据配置把Class缓存到内存中,并不会直接全部初始化。 实例化缓存:Dubbo SPI在实例化Class后,会将实例的对象缓存起来,每次在获取的时候,先去缓存中读取,如果缓存中读取不到,则重新加载并缓存。 普通扩展类:最基础的,配置在SPI配置文件中的扩展类实现。 包装扩展类:Wrapper没有具体的实现,只是做了通用逻辑的抽象,并且需要在构造方法中传入一个具体的扩展接口的实现。属于Dubbo的自动包装特性。 自适应扩展类:一个接口可能会有多重实现类,具体使用哪个实现类,可以在运行代码的时候,通过传入URL中的某些参数动态来确定。属于扩展点的自适应特性。 其他缓存:如扩展加载器缓存、扩展名缓存等。

1.5 扩展点的特性

扩展类一共包含四种特性:自动包装、自动加载、自适应和自动激活。

1.5.1 自动包装

ExtensionLoader在加载扩展类的时候,如果发现扩展类包含其他扩展点作为构造函数的参数,则认为这个扩展类是Wrapper类。

public class ProtocolFilterWrapper implements Protocol {
    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
}
复制代码

ProtocolFilterWrapper类虽然继承了Protocol接口,但是构造函数中有注入了一个Protocol类型的参数,则ProtocolFilterWrapper会被认定为Wrapper类。

1.5.2 自动加载

如果一个扩展类是另外一个扩展类的成员属性,并且拥有setter方法,那么框架也会自动注入对应的扩展点实例。ExtensionLoader在执行扩展点初始化的时候,会自动通过setter方法注入对应的实现类。如果有多个实现类,则会使用到自适应性。

1.5.3 自适应性

在Dubbo SPI中,使用@Adaptive注解,可以动态的通过URL中的参数来确定要使用的哪个具体的实现类。

@SPI("netty")
public interface Transporter {
    
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;

    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
复制代码

@Adaptive可以传入多个参数,当调用对应的方法的时候,会动态的从"URL"中提取key参数的value值,,如果能匹配上某个扩展实现类,则直接使用对应的实现类,如果未匹配上,则继续通过第二个key参数的value值继续进行查找,如果都未匹配到,则抛出异常。这种动态寻找实现类的方式比较灵活,但是只能激活一个实现类,如果需要实现多个实现类同时被激活,或者根据不同的条件,同时激活多个实现类,则需要使用到自动激活。

1.5.4 自动激活

使用@Activate注解,可以标记对应的扩展点默认被激活启用,该注解还可以传入不同的参数,设置扩展点在不同的条件下被自动激活。

2、扩展点注解

2.1 @SPI

首先定义出一个基本的接口com.impassive.SpiService

/** @author impassivey */
public interface SpiService {

  /** @return spi */
  String spi();
}
复制代码

然后实现两个继承类:com.impassive.service.SpiServiceImpl和com.impassive.service.JavaSpiServiceImpl。


/** @author impassivey */
public class SpiServiceImpl implements SpiService {

  public SpiServiceImpl() {
    System.out.println("Spi Service");
  }

  @Override
  public String spi() {
    System.out.println(SpiServiceImpl.class);
    return "spi";
  }
}


import com.impassive.SpiService;

/** @author impassivey */
public class JavaSpiServiceImpl implements SpiService {

  public JavaSpiServiceImpl() {
    System.out.println("Java Spi Service");
  }

  @Override
  public String spi() {
    System.out.println(JavaSpiServiceImpl.class);
    return "Java Spi";
  }
}
复制代码

编写配置文件,路径为META-INF/services/,名称与接口的全路径名一致com.impassive.SpiService,内容为实现类的全路径名。

com.impassive.service.SpiServiceImpl
com.impassive.service.JavaSpiServiceImpl
复制代码

编写测试方法(Java的SPI实现):

import java.util.ServiceLoader;
import org.junit.Test;

public class SpiTest {

  @Test
  public void testJavaSpi() {
    ServiceLoader<SpiService> spiServices = ServiceLoader.load(SpiService.class);
    for (SpiService spiService : spiServices) {
      spiService.spi();
    }
  }
}
复制代码

得到最终的输出结果为:

Spi Service
class com.impassive.service.SpiServiceImpl
Java Spi Service
class com.impassive.service.JavaSpiServiceImpl

Process finished with exit code 0
复制代码

由此可以看出,Java 的SPI会实例化全部的实现类。如果此时其中的一个是实现类是不需要使用,则会造成空间的浪费,初始化的速度也会变慢。 Dubbo的SPI实现,需要先在接口上加上@SPI的注解,表名这个接口是一个SPI接口,修改后的接口如下:

import org.apache.dubbo.common.extension.SPI;

/** @author impassivey */
@SPI
public interface SpiService {

  /** @return spi */
  String spi();
}
复制代码

注意,需要使用该接口,需要引入apache下的dubbo-commen包。然后在META-INF/dubbo下以接口的全路径名为名称的文件中添加配置信息。他属于一种key-value的形式,等号左边是Key,右边是value。

dubbo-spi=com.impassive.service.SpiServiceImpl
java-spi=com.impassive.service.JavaSpiServiceImpl
复制代码

编写测试方法:

@Test
public void testDubboSpi() {
  ExtensionLoader<SpiService> spiService = ExtensionLoader.getExtensionLoader(SpiService.class);
  SpiService extension = spiService.getExtension("dubbo-spi");
  extension.spi();
}
复制代码

输出结果如下:

Spi Service
class com.impassive.service.SpiServiceImpl

Process finished with exit code 0
复制代码

根据返回值可知,JavaSpiServiceImpl并未加载。所以Dubbo实现了扩展点的按需加载,可以减少空间和性能的浪费。@SPI注解支持参数,用于设置一个默认的实现子类。如果找不到设置的默认子类,则会抛出异常。如果没有设置,则默认的实现为null。

2.2 @Adaptive

@Adaptive注解可以标注在类、接口、枚举类和方法上。 如果标注在方法上,则是方法级别的注解,则可以通过参数动态获得实现类。方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。在org.apache.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClass方法中会生成一个Adaptive类,具体是实现是通过调用URL的org.apache.dubbo.common.URL#getParameter(java.lang.String, java.lang.String)方法获得对应的一个扩展点。如果获取失败,再使用一个默认的实现。例如org.apache.dubbo.remoting.Transporter#connect使用的默认实现是netty。 如果该注解标注在实现类上面,则该实现类就是默认的扩展点,如果一个接口有多个实现类,并且有多个实现类均标注了@Adaptive注解,则会抛出异常。 该注解也可以传入一个数组,会根据数组的顺序先后进行比较,如果第一个key未找到,则匹配第二个key。如果所有的key都未匹配到,则使用驼峰规则进行匹配,如果也未匹配到,则抛出异常。驼峰规则:如果@Adaptive的value为空,则会根据该接口的名称,将驼峰式命名的接口名称转换为以"."分割的名称。例如全路径名下的com.impassiev.ImpassiveService会转换成impassive.service,具体实现见方法:org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#getMethodAdaptiveValue。ExtensionLoader会缓存两个对象:cachedAdaptiveClass,缓存Adaptive具体实现类的Class类型;cachedAdaptiveInstance,缓存Class的具体实例,具体见代码org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtensionorg.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension。

2.3 @Activate

@Activate可以标注在类、接口、枚举类和方法,主要使用在有多个扩展点实现、需要根据不同的条件被激活的场景中。具体的使用方法见后续章节。

3、ExtensionLoader的工作原理

ExtensionLoader是整个扩展机制的主要逻辑类。在这个类里面实现了配置的加载、扩展类缓存、自适应对象生成等所有工作。

3.1 工作流程

ExtensionLoader的逻辑入口可以分为getExtension、getAdaptiveExtension、getActivateExtension三个,分别是获取普通扩展类、获取自适应扩展类、获取自动激活的扩展类。总体逻辑都是从调用这三个方法开始,每个方法可能会有不同的重载方法,根据不同的传入方法进行调整。

3.2 getExtension的实现原理

1、传入扩展点的名字,如果扩展点的名字为true,则获取默认扩展点 2、扩展的是实例由一个Holder把控,会先从Holder中回去,如果Holder获取失败,则会去创建一个Holder 3、如果Holder中的实例为空,则会去加载扩展点的配置文件,并加载扩展点 4、加载扩展点的时候会先从扩展点Class缓存中获取,如果获取不到,则去加载配置文件,并加入到缓存中 5、加载配置文件的时候会加载META-INF/dubbo/、META-INF/dubbo/internal/、META-INF/services/(兼容JavaSPI)三个目录,这三个目录的加载使用的是Java的SPI机制 6、加载配置文件后,会去获取一个类加载器,用于加载配置文件中的扩展类。类加载器的获取机制是:先获取当前线程上下文的类加载,如果获取失败,则获取ExtensionLoader的类加载,如果获取失败,则获取应用类加载器 7、读取配置文件的每一行,根据等号的位置进行分割,左边为扩展点的名字,右边为扩展的实例类。扩展点的名字可以是多个,用","分割,然后将名字缓存起来。但是只能缓存一个,当一个class被缓存的时候,后续就不会再缓存。 8、如果扩展点的名字有重复的,则会抛出异常 9、最后将class信息缓存起来 10、使用缓存起来的class信息,实例化一个对象,并缓存实例化的对象 11、如果扩展点内部有其他属性,则使用set方法注入 12、如果扩展点是一个包装类对象,则初始化一个包装类对象,并注入该扩展点 13、如果类有继承Lifecycle接口,则调用该接口的init方法进行初始化

3.3 getAdaptiveExtension的实现原理

1、会先从自适应类的缓存中去获取,如果获取不到,则去加载资源。如果没有@Adaptive的注解,则会直接抛出异常 2、使用加载后的类去实例化一个对象 3、将加载的类加入到缓存中

3.4 getActivateExtension的实现原理

根据案例进行解说:

ExtensionLoader<SpiService> spiService = ExtensionLoader.getExtensionLoader(SpiService.class);
URL url = new URL("test", "name", 1123);
URL we = url.addParameter("we", "java1");
List<SpiService> extension = spiService.getActivateExtension(we, "we");
System.out.println(extension);
复制代码
/** @author impassivey */
@Activate(value = "we")
public class JavaSpiServiceImpl implements SpiService {

  public JavaSpiServiceImpl() {
    System.out.println("Java Spi Service");
  }

  @Override
  public String spi() {
    System.out.println(JavaSpiServiceImpl.class);
    return "Java Spi";
  }

  @Override
  public void setName(String name) {

  }

  @Override
  public void test(URL url) {

  }
}
复制代码

在上面的案例中,会根据key->we去激活JavaSpiServiimpl,因为他的配置中有一个value的值为key,也可以写成key:value的形式,但是当值为key:value的形式的时候,URL中配置的参数的key:value的形式必须一致才可以可以成功激活,不然激活无效。因为上述的方式是获取一个Activate的方法,所以,配置文件中也必须得配置一个名叫java1的扩展点,不然会抛出异常,表示找不到名字为java1的扩展点。激活的流程如下: 1、先根据key的值,获取到对应的value值。如果value的值为"-default",则不会去加载这些扩展点。 2、如果名字是default,则会加载所有的扩展点 3、如果扩展点的名字是"-"开头的,则不会被激活

3.5 ExtensionFactory的实现原理

ExtensionFactory工厂类的关系图: 在这里插入图片描述

从类图可以看出,ExtensionFactory的实现主要有三个:SpringExtensionFactory、SpiExtensionFactory和AdaptiveExtensionFactory三个。SpringExtensionFactory在进行服务引用和服务暴露的时候,会将Spring的上下文保存起来,用以连通Dubbo和Spring容器。在Spring中,会去遍历所有的上下文,然后根据名字和bean的类型进行查找。如果接口标注了SPI注解,则SpringExtensionFactory不会进行查找,会使用SpiExtensionFactory进行查找。 AdaptiveExtensionFactory是ExtensionFactory的默认实现类。在初始化的时候,会找出所有的ExtensionFactory,并实例化,然后在获取一个实例的时候。如果是Spring中的实例,则使用SpringExtensionFactory进行查找实例化,如果接口标注了SPI注解,则使用SpiExtensionFactory进行实例化。ExtensionFactory的激活时间是在初始化ExtensionLoader的时候,会自动去获取一个ExtensionFactory。 下面展示整个SPI机制的流程 SPI 流程

4、扩展点动态编译的实现

在自适应类处生成的只是一个字符串的,需要编译之后,才能成为一个Class。

4.1 总体结构

Duubo中有三种代码编译器,分别是JDK编译器、Javassist编译器和AdaptiveCompiler编译器。 在这里插入图片描述

Compiler上的SPI注解有一个默认值javassist,即JavassistCompier会作为默认编译器,进行代码的编译。AdaptiveCompiler的实现原理和AdaptiveExtensionFactory一致,是作为两个编译器的选择,如果用户有指定对应的编译器,则使用指定的编译器,否则使用默认的编译器。 AbstractCompiler默认实现了compiler方法,实现逻辑主要包括了获取包的名称、类的名称,拼接出全路径,然后使用路径去判断,是否有加载这个类,如果加载了就返回,否则调用doCompier方法去加载。doCompiler方法由子类实现。

4.2 Javassist动态代码编译

1、初始化Javassist的ClassBuilder 2、设置ClassName 3、通过正则取出import、extends、implements等信息,并调用Javassist的API,设置对应的参数 4、通过正则取出各个方法、字段,并使用对应的Api设置 5、指定ClassLoader编译生成Class信息

4.3 JDK动态代码编译

主要分为了三个步骤: 1、初始化一个JavaFileObject对象;字符串代码会包装成一个文件对象。 2、设置JavaFileManage对象的值;主要管理文件的读取和输出位置 3、使用JavaCompiler编译

5 关键类入口注解详解

ExtensionLoader