三、Apache Dubbo学习整理---扩展点加载机制(1)

这篇比较枯燥,先记录下来。等着理解更深后,会使用通俗易懂的语言进行描述。

一、背景描述

dubbo是一个扩展性特别强的框架,基于JAVA的SPI思路改良后的Dubbo SPI,使接口和实现完全解耦。提供注册中心,通信协议,序列化等的扩展方式。而且自身在设计过程中针对不同的场景选择合理的设计模式。

二、Dubbo SPI描述

1、JAVA SPI

使用策略模式。只声明接口,具体的实现不在程序中直接确定,而且通过程序外的配置,用于具体实现的装配。

①定义一个接口以及方法。

②编写接口的实现类。

③创建

//定义SPI接口

public interface HelloService {

void sayHello();

}

//定义实现类

public class ChineseHello implements HelloService {

@Override

public void sayHello() {

System.out.println("你好!!!");

}

}

//使用ServiceLoader加载接口的所有实现类

public static void main(String[] args) {

ServiceLoader<HelloService> helloServices = ServiceLoader.load(HelloService.class);

for (HelloService helloService: helloServices){

helloService.sayHello();

}

}

//输出你好!!!!

2、Dubbo SPI

官方文档的解释:

1、JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展则初始化很耗时,如果没用上也加载,则浪费资源。

2、如果扩展加载失败,则连扩展的名称都获取不到了。如果JDK标准的ScriptEngine,通过getName()获取脚本类型的名称,如果RubyScriptEngine因为所引来的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被“吃掉”了,和Ruby对应不起来,当用户执行Ruby脚本时,会报不支持Ruby,而不是真正失败的原因。

3、增加了对扩展IOC和APO的支持,一个扩展可以直接setter注入其他扩展。在Java SPI中已经看到,java.util.ServiceLoader会一次把PrintService接口下的所有实现类全部初始化,用户直接调用接口。Dubbo SPI只是加载配置文件中的类,并分成不同的种类缓存在内存中,而不全立即全部初始化,在性能上有更好的表现。

//在目录META-INF/dubbo/internal下建立HelloService的默认实现类

impl=com.test.spi.ChineseHello

//定义SPI接口

public interface HelloService {

void sayHello();

}

//定义实现类

public class ChineseHello implements HelloService {

@Override

public void sayHello() {

System.out.println("你好!!!");

}

}

//调用Dubbo SPI加载配置文件的信息,并且加载默认实现类

public class SayHelloMain {

public static void main(String[] args) {

HelloService helloService = ExtensionLoader.getExtensionLoader(HelloService.class)

.getDefaultExtension();

helloService.sayHello();

}

}

//输出你好!!!!

JAVA SPI加载失败,可能会因为各种原因导致异常信息被“吞掉”,导致问题追踪比较困难。Dubbo SPI在扩展加载失败会先抛出真实异常并打印日志。扩展点在被动加载的时候,即使有部分失败,也不会影响其他扩展点和整个框架的使用。

  • ①Dubbo SPI自己实现了IOC和AOP机制。
  • ②一个扩展点可以通过setter方法直接注入其他扩展的方法。T injectExtension(T instace)方法实现了这个功能。
  • ③Dubbo支持包装扩展类,推荐把通用的抽象逻辑放在包装类中,用于实现扩展点的AOP特性。

3、扩展点的配置规范

Dubbo SPI和JAVA SPI类似,需要在META-INF/dubbo/下放置对应的SPI配置文件,文件名称需要命名为接口的全路径名。

配置文件的呢日用为key=扩展点实现类全路径名,有多个用换行符分割。

key为Dubbo SPI注解中的传入参数。

兼容JAVA SPI的配置路径和内容配置方式。在Dubbo启动时,会默认扫META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal/三个文件。

4、扩展点的分类与缓存

分为Class缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类,包装扩展类(Wrapper)和自适应扩展类(Adaptive)等。

Class缓存:Dubbo SPI获取扩展类时,会先从缓存中读取。如果缓存中不存在,则加载配置文件,根据配置把Class缓存在内存中,并不会直接全部初始化。

实例缓存:基于性能考虑,Dubbo框架不仅缓存Class,也会缓存Class实例化后的对象。每次获取的时候,会先从缓存中读取,如果缓存中读不到,则重新加载并缓存起来。因为缓存的Class并不会全部实例化,而是根据需求实例化并缓存,因此性能更好。


  • 普通扩展类:

    最基础的,配置在SPI配置文件中的扩展类实现。

  • 包装扩展类:

    Wrapper类没有具体的实现,只是做了通用逻辑的抽象,并且需要在构造方法中传入一个具体的扩展接口的实现。属于Dubbo的自动包装特性。

  • 自适应扩展类:

    一个扩展接口会有多种实现类,具体使用哪个可以不写死在配种或者代码中,在运行时,通过传入URL中的某些参数动态来确定。属于扩展点的自适应特性。

  • 其他缓存:

    如扩展类加载器缓存、扩展名缓存等。

5、扩展点的特性

自动包装、自动加载、自适应和自动激活。

  • ①自动包装:

    自动包装是上面提到的包装扩展类,ExtensionLoader在加载扩展时,如果发现这个扩展类包含其他扩展点作为构造函数的参数,则这个扩展类就会被认为是Wrapper类。

    public class ProteocolFilterWrapper implements Protocol{

    private final Protocol protocol;

    //实现了Protocol,但在构造函数中又传入了一个Protocol类型的参数,框架会自动注入

    public ProteocolFilterWrapper (Protocol protocol){

    if(protocol == null){

    throw new IllegalArgumentException("protocol == null");

    }

    this.protocol = protocol

    }

}

ProteocolFilterWrapper 实现了Protocol接口,但是构造函数中又传入了Protocol类型的参数。因此ProteocolFilterWrapper会被认定为Wrapper类。这是一种装饰器模式,把通用的抽象逻辑封装或者对子类进行增强,让子类可以更加专注具体的实现。

  • ②自动加载:

    除了在构造函数中传入其他扩展实例,我们还经常使用setter方法设置属性值。如果某个扩展类是另外一个扩展点类的成员属性,并且拥有setter方法,那么框架也会自动注入对应的扩展点实例。ExtensionLoader在执行扩展点出初始化的时候,会自动通过setter方法注入对应的实现类。如果扩展类属性是一个接口,有多种实现,那么具体注入哪一个呢?涉及第三特性---自适应。

  • ③自适应:

    在Dubbo SPI中,我们使用@Adaptive注解,可以动态通过URL中的参数来确定要使用哪个具体的实现类。从而解决自动加载中的实例注入问题。

    @SPI("netty")

    public interface Transporter{

    @Adaptive({Constants.SERVER_KEY,Constants.TRANSPORTER_KEY}})

    Server bind(URL url,ChannelHandler handler);

    @Adaptive({Constants.CLIENT_KEY,Constants.TRANSPORTER_KEY}})

    Client connect(URL url,ChannelHandler handler);

    }

@Adaptive传入两个参数,外部调用Transporter#bin方法的时候,会动态从传入参数“URL”中提取key参数“server”的value值,如果能匹配上某个扩展实现类则直接使用对应的实现类;如果未匹配上,则继续通过第二个key。如果都没匹配上,则抛出异常。也就是说@Adaptive传入多个参数,依次进行实现类的匹配,直到最后抛出异常。

如果一个类的多个实现类都要加载怎么办?涉及最后一个特性---自动激活。

  • ④自动激活:

    使用@Activate注解,可以标记对应的扩展点默认被激活启用。该注解还可以通过传入不同的参数,设置扩展点在不同的条件下被自动激活。主要的使用场景是某个扩展点的多个实现类需要同时启动(如Filter扩展点)

    二、扩展点注解

    1、扩展点注解:@SPI

    可以作用在类,接口和枚举上,Dubbo框架都使用在接口上。作用为标记这个接口是Dubbo SPI接,即是一个扩展点,可以有多个不同的实现。运行时需要通过配置找到具体的实现类。

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.TYPE})

public @interface SPI {

String value() default "";

}

SPI有个value属性,通过这个属性,可以传入不同的参数来设置这个接口的默认实现类。

例如Protocol的默认为dubbo。

@SPI("dubbo")

public interface Protocol {

int getDefaultPort();

@Adaptive

<T> Exporter<T> export(Invoker<T> var1) throws RpcException;

@Adaptive

<T> Invoker<T> refer(Class<T> var1, URL var2) throws RpcException;

void destroy();

}

Dubbo中很多地方通过getExtension(Class type,String name)来获取扩展点接口的具体实现。此时会对class做校验,判断是否是接口,以及是否有@SPI注解,两者缺一不可。

2、扩展点自适应注解:@Adaptive

@Adaptive注解可以表示在类,接口,枚举和方法上,但是在整个Dubbo框架中,只有几个地方使用到了类级别上。其他都标注在方法上。如果标注在方法上,为方法级别注解,则可以通过参数动态获取实现类,这一点在自适应特性中已经说明。方法级别注解,在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。

例如:Protocol接口在export和refer两个接口上添加了@Adaptive注解。Dubbo在初始化扩展点时,会生成Protocol$Adaptive类,里面会实现两个方法,方法里会有一些抽象的通用逻辑,通过@Adaptive中传入的参数,找到并调用真正的实现类。和装饰器模式比较类似。

@Adaptive注解的代码

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.TYPE, ElementType.METHOD})

public @interface Adaptive {

String[] value() default {};

}

该注解也可以传入value值,并且是一个数组。

  • 1、在初始化Adaptive注解的接口时,会对先传入的URL进行key值匹配,
  • 2、第一个key没匹配上继续匹配后续的key,直到所有的key匹配完毕,
  • 3、如果还没有匹配到,则会使用“驼峰规则”匹配,
  • 4、如果还没匹配到,则抛出IllegalStateException异常。

驼峰规则:

如果包装类(wrapper)没有用Adaptive匹配到key值,则Dubbo会自动把接口名称根据驼峰大小写分开,并且用“.”符号连接起来,以此来作为默认实现类的名称,如org.apache.dubbo.xxx.HelloInovkerWrapper中HelloInvokerWrapper会被转移为hello.invoker.wrapper。

为什么有些实现类会标注@Adaptive注解?

  • 1、放在实现类上,主要是为了直接固定对应的实现而不需要动态生成代码实现,就像策略模式直接确定实现类。
  • 2、在代码中的实现方式是:ExtensionLoader中会缓存两个与@Adaptive有关的对象,一个缓存在cachedAdaptiveClass中,即Adaptive具体的实现类的Class类型;
  • 3、另一个缓存在cachedAdaptiveInstance中,Class的具体实例化对象。
  • 4、在扩展点初始化时,如果发现实现类中有@Adaptive注解,则直接赋值给cachedAdaptiveClass,后续实例化类的时候,就不会在动态生成代码,直接实例化cachedAdaptiveClass,并把实力缓存到cachedAdaptiveInstance中。
  • 5、 如果注解在接口方法上,会根据参数,动态获得扩展点的实现,会生成Adaptive类,在缓存到cachedAdaptiveInstance中。

public class ExtensionLoader<T> {

//实现类类型cachedAdaptiveClass

private volatile Class<?> cachedAdaptiveClass = null;

private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap();

private String cachedDefaultName;

//实例化对象cachedAdaptiveInstance

private final Holder<Object> cachedAdaptiveInstance = new Holder();

3、扩展点自动激活注解:@Activate

@Activate可以标识在类、接口、枚举类和方法上。主要使用在多个扩展点实现、需要根据不同条件被激活的场景上。如Filter需要多个同时激活,因为每个Filter的实现是不同的功能。

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.TYPE, ElementType.METHOD})

public @interface Activate {

//URL中的分组如果匹配则激活,可以设置多个

String[] group() default {};

//查找URL中如果含有该key值,则激活

String[] value() default {};

//标识哪些扩展点要在本扩展点之前

String[] before() default {};

//标识哪些扩展点要在本扩展点之前

String[] after() default {};

//排序信息

int order() default 0;

}

以上是 三、Apache Dubbo学习整理---扩展点加载机制(1) 的全部内容, 来源链接: utcz.com/a/34203.html

回到顶部