关于java:Dubbo拓展点加载机制

9次阅读

共计 22073 个字符,预计需要花费 56 分钟才能阅读完成。

Dubbo 拓展点加载机制

前言

本篇首先介绍现有 Dubbo 加载机制的详情,包含 Dubbo 所做的改良及局部个性。而后介绍加载机制曾经存在的一些要害注解,@SPI、@Adaptive、@Activate。而后介绍整个接在机制中最外围的 ExtensionLoader 的工作流程及实现原理。最初介绍拓展中应用的类动静编译的实现原理。通过本篇的浏览,咱们会对 Dubbo SPI 加载机制有深刻的理解,也会对这部分的源码有深刻的意识,后续胖友自行浏览源码也会更容易。

加载机制概述

Dubbo 良好的拓展性与两个方面是密不可分的,一是整个框架中针对不同场景,恰到好处的应用了各种设计模式,二就是咱们要说的加载机制。基于 Dubbo SPI 的加载机制,让整个框架的接口与具体实现齐全解耦,从而给整个框架良好拓展性奠定了根底。

Dubbo默认提供了很多能够间接应用的拓展点。Dubbo简直所有的性能组件都是基于拓展机制(SPI)实现的,这些外围拓展点前面会具体说。

Dubbo SPI没有间接应用 Java SPI, 而是在它的思维上又做了 - 定的改良,造成了一套本人的配置标准和个性。同时,Dubbo SPI 又兼容Java SPI。服务在启动的时候,Dubbo 就会查找这些扩大点的所有实现。

Java SPI

在说 Dubbo SPI 之前,咱们先说一下Java SPI。SPI 的全称是Service Provider Interface,起初是提供给厂商做插件开发的。对于 Java SPI 的具体定义和解释,能够参见此处。Java SPI 应用了策略模式,一个接口多种实现。咱们只申明接口,具体的实现并不在程序中间接确定,而是由程序之外的配置掌控,用于具体实现的拆卸。具体步骤如下:

  1. 定义一个接口及对应的办法。
  2. 编写该接口的一个实现类。
  3. META-INF/services/ 目录下,创立一个以接口全门路命名的文件, 如com.example.rpc.example.spi.HelloService
  4. 文件内容为具体实现类的全路径名,如果有多个,则用分行符分隔。
  5. 在代码中通过 java. util. ServiceLoader 来加载具体的实现类。如此一来,HelloService的具体实现就能够由文件 com.example.rpc.example.spi.HelloService 中配置的实现类来确定了,这里我配置的类为com.example.rpc.example.spi.HelloServiceImpl

我的项目构造如下:

Java SPI 示例代码如下:

public interface HelloService {void sayHello();
}
public class HelloServiceImpl implements HelloService{
    @Override
    public void sayHello() {System.out.printf("hello world!");
    }
}
public static void main(String[] args) {ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);
    // 获取所有的 SPI 实现,// 循环调用 sayHello()办法,// 会打印出 hello world 此处只有一个实现: HelloServiceImpl
    for (HelloService helloService : serviceLoader) {
        // 此处会输入 hello world
        helloService.sayHello();}
}

从下面的代码清单中能够看到,main 办法里通过 java.util.ServiceLoader 能够获取所有的接口实现,具体调用哪个实现,能够通过用户定制的规定来决定。

拓展点加载机制的改良

Dubbo 的扩大点加载从 JDK 规范的 SPI (Service Provider Interface) 扩大点发现机制增强而来。

Dubbo 对 JDK 规范的 SPI 做了一些改良:

  • JDK 规范的 SPI 会一次性实例化扩大点所有实现,如果有扩大实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩大点加载失败,连扩大点的名称都拿不到了。
  • 减少了对扩大点 IoC 和 AOP 的反对,一个扩大点能够间接 setter 注入其它扩大点。

HelloService 接口的 Dubbo SPI 革新

  1. 在目录META- INF/dubbo/ internal 下建设配置文件com.example.rpc.example.spi.HelloService,文件内容如下
impl=com.example.rpc.example.spi.HelloServiceImpl
  1. 为接口类增加 SPI 注解,设置默认实现为impl
@SPI("impl")
public interface HelloService {void sayHello();
}
  1. 实现类不变
public class HelloServiceImpl implements HelloService{
    @Override
    public void sayHello() {System.out.printf("hello world!");
    }
}
  1. 调用 Dubbo SPI
public static void main(String[] args) {
    // 通过 ExtensionLoader 获取接口 HelloService.class 的默认实现
    ExtensionLoader<HelloService> extensionLoader =                            ExtensionLoader.getExtensionLoader(HelloService.class);
    HelloService helloService = extensionLoader.getDefaultExtension();
    // 此处会打印出 hello world
    helloService.sayHello();}

拓展点注解

拓展点注解:@SPI

@SPI 注解能够应用在类、接口和枚举类上,Dubbo 框架中都是应用在接口上。它的次要作用就是标记这个接口是一个 Dubbo SPI 接口,即是一个扩大点,能够有多个不同的内置或用户定义的实现。运行时须要通过配置找到具体的实现类。@SPI 注解的源码如下所示。

@SPI 注解的源码

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    /**
     * 默认实现的 key 名称
     * default extension name
     */
    String value() default "";}

咱们能够看到 SPI 注解有一个 value 属性,通过这个属性,咱们能够传入不同的参数来设置这个接口的默认实现类。例如,咱们能够看到 Transporter 接口应用 Netty 作为默认实现,代码如下。

@SPI("netty")
public interface Transporter {...}

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

拓展点自适应注解:@Adaptive

@Adaptive注解能够标记在类、接口、枚举类和办法上,然而在整个 Dubbo 框架中,只有几个中央应用在类级别上,如 AdaptiveExtensionFactoryAdaptiveCompiler, 其余都标注在办法上。如果标注在接口的办法上,即办法级别注解,则能够通过参数动静取得实现类。办法级别注解在第一次 getExtension 时,会主动生成和编译一个动静的Adaptive 类,从而达到动静实现类的成果。

上面我举个例子说一下,拿 Protocol 接口举例,exportrefer 接口两个办法增加了 @Adaptive 注解,代码如下,Dubbo 在初始化扩大点时,会生成一个 Protocol$Adaptive 类,外面会实现这两个办法,办法里会有一些形象的通用逻辑,通过 @Adaptive 中传入的参数,找到并调用真正的实现类。相熟装璜器模式的读者会很容易了解这部分的逻辑。具体实现原理会在前面说。

上面是主动生成的 Protocol$Adaptive#export 实现代码,如代码如下,省略局部无关代码。

Protocol$Adaptive#export

public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws     org.apache.dubbo.rpc.RpcException {if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        org.apache.dubbo.common.URL url = arg0.getUrl();
        // 通过 protocol key 去寻找实现类的名称
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), org.apache.dubbo.rpc.Protocol.class);
        // 依据 url 中的参数 尝试获取真正的拓展点实现类
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) scopeModel.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        // 最终会调用具体拓展点的 bind 办法
        return extension.export(arg0);
    }
}

咱们从生成的源码中能够看出,主动生成的代码中实现了很多通用的性能,最终会调用真正的接口实现。

当该注解放在实现类上,则整个实现类会间接作为默认实现,不再主动生成上述代码。在扩大点接口的多个实现里,只能有一个实现上能够加 @Adaptive 注解。如果多个实现类都有该注解,则会抛出异样: More than 1 adaptive class found@Adaptive 注解的源代码如下。

Adaptive 注解的源代码

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
        // 数组 能够设置多个 key,会依照程序一次匹配
    String[] value() default {};}

该注解也能够传入 value 参数,是一个数组。咱们在代码清单 4.9 中能够看到,Adaptive 可 以传入多个 key 值,在初始化 Adaptive 注解的接口时,会先对传入的 URL 进行 key 值匹配,第一个 key 没匹配上则匹配第二个,以此类推。直到所有的 key 匹配结束,如果还没有匹配到,则会应用 ” 驼峰规定 ” 匹配,如果也没匹配到,则会抛出 IllegalStateException 异样。

什么是 ” 驼峰规定”呢? 如果包装类(Wrapper) 没有用 Adaptive 指定 key 值,则 Dubbo 会主动把接口名称依据驼峰大小写离开,并用符号连接起来,以此来作为默认实现类的名 称,如 org.apache.dubbo.xxx.YyylnvokerWpapper 中的 YyylnvokerWrapper 会被转换为 yyy.invoker.wrapper

为什么有些实现在实现类上会标注 @Adaptivene ?

放在实现类上,次要是为了间接固定对 应的实现而不须要动静生成代码实现,就像策略模式间接确定实现类。在代码中的实现形式是: ExtensionLoader中会缓存两个与 @Adaptive 无关的对象,一个缓存在 cachedAdaptiveClass 中,即 Adaptive 具体实现类的 Class 类型; 另外一个缓存在 cachedAdaptivelnstance 中,即 Class 的具体实例化对象。在扩大点初始化时,如果发现实现类有 @Adaptive 注解,则间接赋值给 cachedAdaptiveClass , 后续实例化类的时候,就不会再动静生成代码,间接实例化 cachedAdaptiveClass, 并把实例缓存到 cachedAdaptivelnstance 中。如果注解在接口办法上,则会依据参数,动静取得扩大点的实现,会生成 Adaptive 类, 再缓存到 cachedAdaptivelnstance 中。

拓展点主动激活注解:@Activate

@Activate 能够标记在类、接口、枚举类和办法上。次要应用在有多个扩大点实现、须要根 据不同条件被激活的场景中,如 Filter 须要多个同时激活,因为每个 Filter 实现的是不同的性能。©Activate 可传入的参数很多,如下表:

参数名 成果
String[] group() URL 中的分组如果匹配则激活,则能够设置多个
String[] value() 查找 URL 中如果含有该 key 值,则会激活
String[] before() 填写扩大点列表,示意哪些扩大点要在本扩大点之前
String[] after() 同上,示意哪些须要在本扩大点之后
int order() 整型,间接的排序信息

ExtensionLoader 的工作原理

ExtensionLoader是整个扩大机制的次要逻辑类,在这个类外面卖现了配置的加载、扩大类 缓存、自适应对象生成等所有工作。上面就联合代码讲一下整个 ExtensionLoader 的工作流程。

工作流程

ExtensionLoader 的逻辑入口能够分为 getExtensiongetAdaptiveExtensiongetActivateExtension三个,别离是获取一般扩大类、获取自适应扩大类、获取主动激活的扩 展类。总体逻辑都是从调用这三个办法开始的,每个办法可能会有不同的重载的办法,依据不 同的传入参数进行调整,具体流程如图所示。

三个入口中,getActivateExtensiongetExtension 的依赖比拟getAdaptiveExtension 则绝对独立。

getActivateExtension办法只是依据不同的条件同时激活多个一般展类。因而,该办法中只会做一些通用的判断逻辑,如接口是否蕴含 @Activate 注解、匹配条件是否合乎等。最终还是通过调用 getExtension 办法取得具体扩大点实现类。

getExtension(String name)是整个扩大加载器中最外围的办法,实现了一个残缺的一般扩 展类加载过程。加载过程中的每一步,都会先查看缓存中是否己经存在所需的数据,如果存在 则间接从缓存中读取,没有则从新加载。这个办法每次只会依据名称返回一个扩大点实现类。初始化的过程能够分为 4 步:

  1. 框架读取 SPI 对应门路下的配置文件,并依据配置加载所有扩大类并缓存(不初始化)。
  2. 依据传入的名称初始化对应的扩大类。
  3. 尝试查找符合条件的包装类: 蕴含扩大点的 setter 办法,例如 setProtocol(Protocol protocol) 办法会主动注入 protocol 扩大点实现; 蕴含与扩大点类型雷同的构造函数,为其注入扩 展类实例,例如本次初始化了一个 ClassA, 初始化实现后,会寻找结构参数中须要 ClassA 的 包装类(Wrapper), 而后注入 ClassA 实例,并初始化这个包装类。
  4. 返回对应的扩大类实例。

getAdaptiveExtension也绝对独立,只有加载配置信息局部与 getExtension 共用了同一个 办法。和获取一般扩大类一样,框架会先查看缓存中是否有曾经初始化化好的 Adaptive 实例,没有则调用 createAdaptiveExtension 从新初始化。初始化过程分为 4 步:

  1. getExtension 一样先加载配置文件。
  2. 生成自适应类的代码字符串。
  3. 获取类加载器和编译器,并用编译器编译方才生成的代码字符串。Dubbo 一共有三种 类型的编译器实现,这些内容会在 4.4 节解说。
  4. 返回对应的自适应类实例。
    接下来,咱们就具体看一下 getExtensiongetAdaptiveExtensiongetActivateExtension 这三个流程的实现。

getExtension 的实现原理

getExtension的主流程下面曾经说过了,上面来具体说下每一步的实现原理。

当调用 getExtension(String name)办法时,会先查看缓存中是否有现成的数据,没有则 调用 createExtension 开始创立。这里有个非凡点,如果 getExtension 传入的 name 是 true, 则加载并返回默认扩大类。

在调用 createExtension 开始创立的过程中,也会先查看缓存中是否有配置信息,如果不 存在扩大类,则会从 META-INF/services/META-INF/dubbo/META-INF/dubbo/internal/ 这几个门路中读取所有的配置文件,通过 I /O 读取字符流,而后通过解析字符串,失去配置文件中 对应的扩大点实现类的全称(如 org.apache.dubbo.common.extensionloader.activate.impl. GroupActivateExtImpl)。扩大点配置信息加载过程的源码如下。

拓展点配置信息加载过程的源码

        private Map<String, Class<?>> getExtensionClasses() {
        // 从缓存中获取已加载的拓展类
        Map<String, Class<?>> classes = cachedClasses.get();
        // 双重查看
        if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();
                if (classes == null) {
                    // 开始加载拓展类
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }


        /**
     * 在 GetExtensionClass 中同步
     * synchronized in getExtensionClasses
     */
    private Map<String, Class<?>> loadExtensionClasses() {
        // 查看是否有 SPI 注解。如果有,则获取注解中填写的名称,并缓存为默认实现名。// 如 @SPI("impl")会保留 impl 为默认实现
        cacheDefaultExtensionName();

        Map<String, Class<?>> extensionClasses = new HashMap<>();

        for (LoadingStrategy strategy : strategies) {
            // 加载指定文件夹下的配置文件
            loadDirectory(extensionClasses, strategy, type.getName());

            // 兼容旧的 ExtensionFactory
            if (this.type == ExtensionInjector.class) {loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
            }
        }

        return extensionClasses;
    }

        /**
     * 加载指定文件夹下的配置文件
     * @param extensionClasses
     * @param strategy
     * @param type
     */
    private void loadDirectory(Map<String, Class<?>> extensionClasses, LoadingStrategy strategy, String type) {
        // 加载 org.apache.xxxxx
        loadDirectory(extensionClasses, strategy.directory(), type, strategy.preferExtensionClassLoader(),
            strategy.overridden(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages());
        String oldType = type.replace("org.apache", "com.alibaba");
        // 加载 com.alibaba.xxxxx
        loadDirectory(extensionClasses, strategy.directory(), oldType, strategy.preferExtensionClassLoader(),
            strategy.overridden(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages());
    }

加载完扩大点配置后,再通过反射取得所有扩大实现类并缓存起来。留神,此处仅仅是把 Class 加载到 JVM 中,但并没有做 Class 初始化。在加载 Class 文件时,会依据 Class 上的注解来判断扩大点类型,再依据类型分类做缓存。扩大类的缓存分类代码如下:

拓展类的缓存分类

    private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                           boolean overridden) throws NoSuchMethodException {if (!type.isAssignableFrom(clazz)) {
            throw new IllegalStateException("Error occurred when loading extension class (interface:" +
                type + ", class line:" + clazz.getName() + "), class"
                + clazz.getName() + "is not subtype of interface.");
        }
        // 检测指标类上是否有 Adaptive 注解
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            // 如果发现是多个自适应类  设置 cachedAdaptiveClass 缓存
            cacheAdaptiveClass(clazz, overridden);
            // 检测 clazz 是否是 Wrapper 类型
        } else if (isWrapperClass(clazz)) {
            // 如果是包装类  存储 clazz 到 cachedWrapperClasses 缓存中
            cacheWrapperClass(clazz);
        } else {if (StringUtils.isEmpty(name)) {name = findAnnotationName(clazz);
                if (name.length() == 0) {throw new IllegalStateException("No such extension name for the class" + clazz.getName() + "in the config" + resourceURL);
                }
            }

            String[] names = NAME_SEPARATOR.split(name);
            if (ArrayUtils.isNotEmpty(names)) {cacheActivateClass(clazz, names[0]);
                for (String n : names) {
                    // 不是自适应类型, 也不是包装类型, 剩下的就是一般扩大类了,也会缓存起来
                    // 留神: 主动激活也是一般扩大类的 - 种,只是会依据不同条件同时激活罢了
                    cacheName(clazz, n);
                    saveInExtensionClass(extensionClasses, clazz, n, overridden);
                }
            }
        }
    }

最初,依据传入的 name 找到对应的类并通过 Class.forName 办法进行初始化,并为其注入 依赖的其余扩大类(主动加载个性)。当扩大类初始化后,会查看一次包装扩大类Set<Class<?> wrapperclasses, 查找蕴含与扩大点类型雷同的构造函数,为其注入刚初始化的扩大类。

依赖注入

injectExtension(instance); // 向拓展类实例中注入依赖
 List<Class<?>> wrapperClassesList = new ArrayList<>();
wrapperClassesList.addAll(cachedWrapperClasses);
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
  // 循环创立 Wrapper 实例
  for (Class<?> wrapperClass : wrapperClassesList) {Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
    if (wrapper == null
        || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
      // 将以后 instance 作为参数传给 Wrapper 的构造方法,并通过反射创立 Wrapper 实例。// 而后向 Wrapper 实例中注入依赖,最初将 Wrapper 实例再次赋值给 instance 变量
      instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
      instance = postProcessAfterInitialization(instance, name);
    }
  }
                }

injectExtension 办法中能够为类注入依赖属性, 它应用了 ExtensionFactory#getExtension (Class<T> type, String name)来获取对应的 bean 实例,这个工厂接口会在前面具体阐明。咱们先来理解一下注入的实现原理。

injectExtension办法总体实现了相似 Spring 的 IoC 机制,其实现原理比较简单: 首先通 过反射获取类的所有办法,而后遍历以字符串 set 结尾的办法,失去 set 办法的参数类型,再通 过 ExtensionFactory寻找参数类型雷同的扩大类实例,如果找到,就设值进去,代码如下。

注入依赖拓展类实现代码

for (Method method : instance.getClass().getMethods()) {
        // 找到以 set 结尾的办法,要求只能有一个参数,并且是 public
    if (!isSetter(method)) {continue;}
    /**
     * 查看是否须要主动注入这个属性
     * Check {@link DisableInject} to see if we need auto injection for this property
     */
    if (method.getAnnotation(DisableInject.class) != null) {continue;}
    // 获取 setter 办法参数类型
    Class<?> pt = method.getParameterTypes()[0];
    if (ReflectUtils.isPrimitives(pt)) {continue;}

    try {
        // 获取属性名,比方 setName 办法对应属性名 name
        String property = getSetterProperty(method);
        // 从 ObjectFactory 中获取依赖对象
        Object object = injector.getInstance(pt, property);
        if (object != null) {
            // 通过反射调用 setter 办法设置依赖
            method.invoke(instance, object);
        }
    } catch (Exception e) {logger.error("Failed to inject via method" + method.getName()
            + "of interface" + type.getName() + ":" + e.getMessage(), e);
    }

}

从源码中能够晓得,包装类的结构参数注入也是通过 injectExtension 办法实现的。

getAdaptiveExtension 的实现原理

getAdaptiveExtension() 办法中,会为扩大点接口主动生 成实现类字符串,实现类次要蕴含以下逻辑: 为接口中每个有 Adaptive 注解的办法生成默认实现 (没有注解的办法则生成空实现),每个默认实现都会从URL 中提取 Adaptive 参数值,并 以此为根据动静加载扩大点。而后,框架会应用不同的编译器,把实现类字符串编译为自适应 类并返回。本节次要解说字符串代码生成的实现原理。

生成代码的逻辑次要分为 7 步,具体步骤如下:

  1. 生成 package、import、类名称等头部信息。此处只会引入一个类 ExtensionLoader。为了不写其余类的 import 办法,其余办法调用时全副应用全门路。类名称会变为“接口名称 + $Adaptive”的格局。例如:Protocol 接口会生成 Protocol$Adpative
  2. 遍历接口所有办法,获取办法的返回类型、参数类型、异样类型等。为第 (3) 步判 断是否为空值做筹备。
  3. 生成参数为空校验代码,如参数是否为空的校验。如果有近程调用,还会增加Invocation 参数为空的校验。
  4. 生成默认实现类名称。如果©Adaptive 注解中没有设定默认值,则依据类名称生成,如 YyylnvokerWrapper 会被转换为yyy.invoker.wrapper。生成的规定是一直找大写字母,并把它 们用连接起来。失去默认实现类名称后,还须要晓得这个实现是哪个扩大点的。
  5. 生成获取扩大点名称的代码。依据 @Adaptive 注解中配置的 key 值生成不同的获取代 码,例如: 如果是@Adaptive(“protocol”), 则会生成url.getProtocol()
  6. 生成获取具体扩大实现类代码。最终还是通过 getExtension(extName)办法获取自适 应扩大类的真正实现。如果依据 URL 中配置的 key 没有找到对应的实现类,则会应用第(4) 步中生成的默认实现类名称去找。
  7. 生成调用后果代码。

上面咱们用 Dubbo。源码中自带的一个单元测试来演示代码生成过程,代码如下。

自适应类生成的代码

//SPI 配置文件中的配置
impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1
@SPI
public interface HasAdaptiveExt {
        // 自适应接口、echo 办法上有 @Adaptive 注解
    @Adaptive
    String echo(URL url, String s);
}

// 在测试方法中调用这个自适应类
SimpleExt ext = ExtensionLoader.getExtensionLoader(HasAdaptiveExt.class).getAdaptiveExtension(); 

生成一下自适应代码

package org.apache.dubbo.common.extension.adaptive;
import org.apache.dubbo.rpc.model.ScopeModel;
import org.apache.dubbo.rpc.model.ScopeModelUtil;
public class HasAdaptiveExt$Adaptive implements org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt {public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1)  {if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        String extName = url.getParameter("has.adaptive.ext", "adaptive");
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt) name from url (" + url.toString() + ") use keys([has.adaptive.ext])");
        ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt.class);
      // 实现类变为配置文件中的 org.apache.dubbo.common.extension.adaptive.impl.HasAdaptiveExt_ManualAdaptive
        org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt extension = (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt)scopeModel.getExtensionLoader(org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt.class).getExtension(extName);    
        return extension.echo(arg0, arg1);
    }
}

生成完代码之后就要对代码进行编译,生成一个新的 Classo Dubbo 中的编译器也是一个自 适应接口,但 @Adaptive 注解是加在实现类 AdaptiveCompiler 上的。这样一来AdaptiveCompiler 就会作为该自适应类的默认实现,不须要再做代码生成和编译就能够应用了。

如果一个接口上既有 @SPI(”impl”) 注解,办法上又有 @Adaptive(”impl2”) 注解,那么会以哪个 key 作为默认实现呢? 由下面动静生成的 SAdaptive 类能够得悉,最终动静生成的实现办法会 是 url.getParameter("impl2", "impl”), 即优先通过@Adaptive 注解传入的 key 去查找扩大实现类; 如果没找到,则通过 @SPI 注解中的 key 去查找; 如果 @SPI 注解中没有默认值,则把类名转化 为 key, 再去查找。

getActivateExtension 的实现原理

接下来,@Activate 的 实现原理,先从它的入口办法说起。getActivateExtension(URL url, String key, String group)办法能够获取所有主动激活扩大点。参数别离是 URL、URL 中指定的 key(多个则用逗号隔开)和 URL 中指定的组信息(group)0 其实现逻辑非常简单,当调用该办法时,主线流程分为 4 步:

(1) 查看缓存,如果缓存中没有,则初始化所有扩大类实现的汇合。

(2) 遍历整个 @Activate 注解汇合,依据传入 URL 匹配条件(匹配 group、name 等),得 到所有合乎激活条件的扩大类实现。而后依据@Activate。中配置的 before、after、order 等参数进 行排序。

(3) 遍历所有用户自定义扩大类名称,依据用户 URL 配置的程序,调整扩大点激活程序,遵循用户在 URL 中配置的程序。

(4) 返回所有主动激活类汇合。

获取 Activate 扩大类实现,也是通过 getExtension 失去的。因而,能够认为 getExtension 是其余两种 Extension 的基石。

此处有一点须要留神,如果 URL 的参数中传入了 -default, 则所有的默认 @Activate 都不会被激活,只有 URL 参数中指定的扩大点会被激活。如果传入了符号结尾的扩大点名,则该扩大点也不会被主动激活。例如:-xxxx, 示意名字为 xxxx 的扩大点不会被激活。

ExtensionInjector 的实现原理

通过后面的介绍,咱们能够晓得 ExtensionLoader 类是整个 SPI 的外围。然而,ExtensionLoader类自身又是如何被创立的呢?

咱们晓得 RegistryFactory 工厂类通过 @Adaptive(("protocol"}) 注解动静查找注册核心实 现,依据 URL 中的 protocol 参数动静抉择对应的注册核心工厂,并初始化具体的注册核心客户端。而实现这个个性的 ExtensionLoader 类,自身又是通过工厂办法 ExtensionInjector#getInstance 创立的,并且这个注射接口上也有 SPI 注解,还有多个实现。

ExtensionInjector 接口

@SPI
public interface ExtensionInjector {
    /**
     * Get instance of specify type and name.
     *
     * @param type object type.
     * @param name object name.
     * @return object instance.
     */
    <T> T getInstance(Class<T> type, String name);

    @Override
    default void setExtensionAccessor(ExtensionAccessor extensionAccessor) {}}

既然注射接口有多个实现,那么是怎么确定应用哪个注射类实现的呢? 咱们能够看到 AdaptiveExtensionInjector 这个实现类工厂上有@Adaptive 注解。因而,AdaptiveExtensionInjector 会作为一开始的默认实现。注射类之间的关系如图

<img src=”https://qiniu-cdn.janker.top/oneblog/20220108200711372.png” style=”zoom:50%;” />

能够看到,除了 AdaptiveExtensionInjector, 还有SpiExtensionInjectorSpringExtensionInjector 两个实现类。也就是说,咱们除了能够从Dubbo SPI 治理的容器中获取扩大点实例,还能够从Spring 容器中获取。

那么 DubboSpring容器之间是如何买通的呢? 咱们先来看 SpringExtensionInjector 的 实现,该注射类提供了初始化 Spring 上下文的办法,在应用之前会调用 init 办法,初始化 SpringExtensionInjector 中的 spring 上下文。当调用 getInstance 获取扩大类时,上下文曾经筹备好,如果没有返回 null,如果有就继续执行getOptionalBean(获取 Bean), 代码如下

     private ApplicationContext context;
    public ApplicationContext getContext() {return context;}

    public void init(ApplicationContext context) {this.context = context;}
    
        
    public <T> T getInstance(Class<T> type, String name) {if (context == null) {
            // ignore if spring context is not bound
            return null;
        }

        //check @SPI annotation
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {return null;}

        T bean = getOptionalBean(context, name, type);
        if (bean != null) {return bean;}

        //logger.warn("No spring extension (bean) named:" + name + ", try to find an extension (bean) of type" + type.getName());
        return null;
    }

那么 Spring 的上下文又是在什么时候被保存起来的呢? 咱们能够通过代码搜寻得悉,在 ReferenceBean 和 ServiceBean 中会调用静态方法保留 Spring 上下文,即一个服务被公布或被 援用的时候,对应的 Spring 上下文会被保留下来。

咱们再看一下 SpiExtensionInjector, 次要就是获取扩大点接口对应的Adaptive 实现类。例 如: 某个扩大点实现类 ClassA 上有 @Adaptive 注解,则调用 SpiExtensionInjector#getInstance 会间接返回 ClassA 实例,代码如下。

SpiExtensionInjector源码

public <T> T getInstance(Class<T> type, String name) {if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {ExtensionLoader<T> loader = extensionAccessor.getExtensionLoader(type);
        if (loader == null) {return null;}
          // 依据类型获取所有的扩大点加载器
        if (!loader.getSupportedExtensions().isEmpty()) {return loader.getAdaptiveExtension();
        }
    }
    return null;
}

通过一番流转,最终还是回到了默认实现 AdaptiveExtensionnInjector 是哪个, 因为该注射类上有 @Adaptive注解。这个默认注射器在初始化办法中就获取了所有扩大注射类并缓存起来,包含 SpiExtensionnInjectorSpringExtensionnInjectorAdaptiveExtensionInjector初始化放法如下。

// 拓展注射器汇合
    private List<ExtensionInjector> injectors = Collections.emptyList();

public void initialize() throws IllegalStateException {
        // 获取拓展注射器的 ExtensionLoader
        ExtensionLoader<ExtensionInjector> loader = extensionAccessor.getExtensionLoader(ExtensionInjector.class);
        List<ExtensionInjector> list = new ArrayList<ExtensionInjector>();
        // 获取曾经反对的拓展类
        for (String name : loader.getSupportedExtensions()) {
            // 通过拓展类 name 获取拓展类注射器 并增加到汇合中 injectors
            list.add(loader.getExtension(name));
        }
        injectors = Collections.unmodifiableList(list);
    }

AdaptiveExtensionnInjector 缓存的工厂会通过 TreeSet 进行排序,SPI 排在后面,Spring 排在前面。当调用 getInstance 办法时,会遍历所有的工厂,先从 SPI 容器中获取扩大类; 如 果没找到,则再从 Spring 容器中查找。咱们能够了解为,AdaptiveExtensionnInjector持有了所 有的具体工厂实现,它的 getInstance 办法中只是遍历了它持有的所有工厂,最终还是调用 SPI 或 Spring 工厂实现的 getInstance 办法。getInstance办法代码如下:

public <T> T getInstance(Class<T> type, String name) {
        // 遍历所有工厂进行查找,程序是 SPI ->Spring
    for (ExtensionInjector injector : injectors) {T extension = injector.getInstance(type, name);
        if (extension != null) {return extension;}
    }
    return null;
}

拓展点动静编译的实现原理

Dubbo SPI的自适应个性让整个框架非常灵活,而动静编译又是自适应个性的根底,因为 动静生成的自适应类只是字符串,须要通过编译能力失去真正的 Class。尽管咱们能够应用反射 来动静代理一个类,然而在性能上和间接编译好的 Class 会有肯定的差距。Dubbo SPI 通过代码 的动静生成,并配合动静编译器,灵便地在原始类根底上创立新的自适应类。上面介绍 Dubbo SPI 动静编译器的品种及对应的实现原理。

总体构造

Dubbo 中有三种代码编译器,别离是 JDK 编译器、Javassist编译器和 AdaptiveCompiler 编译器。这几种编译器都实现了 Compiler接口,编译器类之间的关系如图

Compiler接口上含有一个 SPI 注解,注解的默认值是 @SPI(”javassist”), 很显著,Javassist 编译器将作为默认编译器。如果用户想扭转默认编译器,则能够通过 <dubbo:application compiler=”jdk” /> 标签进行配置。

AdaptiveCompiler下面有 @Adaptive 注解,阐明 AdaptiveCompiler 会固定为默认实现,这 个 Compiler 的次要作用和 AdaptiveExtensionInjector 类似,就是为了治理其余Compiler, 代码如下:

AdaptiveCompiler 的逻辑

    public static void setDefaultCompiler(String compiler) {
            // 设置默认的编译器名称
        DEFAULT_COMPILER = compiler;
    }

    @Override
    public Class<?> compile(String code, ClassLoader classLoader) {
        Compiler compiler;
        ExtensionLoader<Compiler> loader = frameworkModel.getExtensionLoader(Compiler.class);
        String name = DEFAULT_COMPILER; // copy reference
        if (name != null && name.length() > 0) {compiler = loader.getExtension(name);
        } else {compiler = loader.getDefaultExtension();
        }
          // 通过 ExtensionLoader 获取对应的译器扩大类实现,调用真正的 compile 做编译
        return compiler.compile(code, classLoader);
    }

AdaptiveCompiler#setDefaultCompiler 办法会在 ApplicationConfig 中被调用,也就是 Dubbo 在启动时,会解析配置中的 <dubbo:application compiler="jdk" /> 标签,获取设置的值,初始化对应的编译器。如果没有标签设置,则应用 @SPI(“javassist”) 中的设置,即 JavassistCompiler。而后看一下AbstpactCompiler, 它是一个抽象类,无奈实例化,但在外面封装了通用的模 板逻辑。还定义了一个形象办法doCompile , 留给子类来实现具体的编译逻辑。JavassistCompilerJdkCompiler都实现了这个形象办法。

AbstractCompiler的次要形象逻辑如下:

  1. 通过正则匹配出包门路、类名,再依据包门路、类名拼接出全门路类名。
  2. 尝试通过 Class.forName 加载该类并返回,避免反复编译。如果类加载器中没有这个类,则进入第 3 步。
  3. 调用 doCompile 办法进行编译。这个形象办法由子类实现。上面将介绍两种编译器的具体实现。

上面将介绍两种编译器的具体实现。

Javassist 动静代码编译

Java 中动静生成 Class 的形式有很多,能够间接基于字节码的形式生成,常见的工具库有 CGLIBASMJavassist 等。而自适应扩大点应用了生成字符串代码再编译为 Class 的形式。

Dubbo 中 JavassistCompiler 的实现原理也很清晰了。因为 咱们之前曾经生成了代码字符串,因而在 JavassistCompiler 中,就是一直通过正则表达式匹 配不同部位的代码,而后调用 Javassist 库中的 API 生成不同部位的代码,最初失去一个残缺的 Class 对象。具体步骤如下:

(1) 初始化Javassist, 设置默认参数,如设置以后的classpath

(2) 通过正则匹配出所有 import 的包,并应用 Javassist 增加import

(3) 通过正则匹配出所有 extends 的包,创立 Class 对象,并应用 Javassist 增加extends

(4) 通过正则匹配出所有 implements 包,并应用 Javassist 增加implements

(5) 通过正则匹配出类外面所有内容,即失去 {} 中的内容,再通过正则匹配出所有办法, 并应用 Javassist 增加类办法。

(6) 生成 Class 对象。

JavassistCompiler继承了抽象类 Abstractcompiler, 须要实现父类定义的一个形象办法doCompileo 以上步骤就是整个 doCompile 办法在 JavassistCompiler 中的实现。

JDK 动静代码编译

JdkCompilerDubbo 编译器的另一种实现,应用了 JDK自带的编译器,原生 JDK 编译器 包位于 javax.tools下。次要应用了三个货色:JavaFileObject 接口、ForwardingJavaFileManager 接口、JavaCompiler.CompilationTask 办法。整个动静编译过程 能够简略地总结为: 首先初始化一个 JavaFileObject 对象,并把代码字符串作为参数传入结构 办法,而后调用 JavaCompiler.CompilationTask 办法编译出具体的类。JavaFileManager负责 治理类文件的输出 / 输入地位。以下是每个接口 / 办法的简要介绍:

  1. JavaFileObject接口。字符串代码会被包装成一个文件对象,并提供获取二进制流 的接口。Dubbo 框架中的 JavaFileObjectlmpl 类能够看作该接口一种扩大实现,构造方法中需 要传入生成好的字符串代码,此文件对象的输出和输入都是 ByteArray 流。因为 SimpleJavaFileObjectJavaFileObject之间的关系属于 JDK 中的常识,因而在本篇不深刻解说,有趣味的读者能够自行查看 JDK 源码。
  2. JavaFileManager接口。次要管理文件的读取和输入地位。JDK 中没有能够间接应用 的实现类,惟一的实现类 ForwardingJavaFileManager 结构器又是 protected 类型。因而 Dubbo 中 定制化实现了一个 JavaFileManagerlmpl 类,并通过一个自定义类加载器 ClassLoaderlmpl 完 成资源的加载。
  3. JavaCompiler.CompilationTaskJavaFileObject 对象编译成具体的类。

小结

本章的内容比拟多,首先介绍了 Dubbo SPI的一些概要信息, 包含与 Java SPI 的区别、Dubbo SPI的新个性、配置标准和外部缓存等。其次介绍了 Dubbo SPI中最重要的三个注解:@SPI@Adaptive@Activate, 解说了这几个注解的作用及实现原理。而后联合 ExtensionLoader 类 的源码介绍了整个 Dubbo SPI 中最要害的三个入口: getExtensiongetAdaptiveExtensiongetActivateExtension, 并解说了创立 ExtensionLoader 的工厂 (ExtensionInjector) 的工作原 理。最初还解说了自适应机制中动静编译的实现原理。

微信搜寻:爪哇干货分享,交个敌人,进技术交换群

正文完
 0