草庐IT

Dubbo SPI自适应扩展和IOC

pq217 2023-03-28 原文

前言

书接上回,本文主要研究DUBBO SPI机制中的IOC和自适应扩展

上文中我们定义了一个抽象的汽车接口 Car,并提供两个实现别克(Buick)和奥迪(Audi)

// 车
@SPI
public interface Car {
    void run();
}
// 奥迪车
public class Audi implements Car {
    @Override
    public void run() {
        System.out.println("Audi is running");
    }
}
// 别克车
public class Buick implements Car {
    @Override
    public void run() {
        System.out.println("Buick is running");
    }
}
// Car全限定名文件配置内容
Buick=com.xxx.Buick
Audi=com.xxx.Audi

通过SPI机制,可以轻松的在运行时根据配置选取某一个实现

// 初始化Car接口的扩展类加载器
ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
// 获取名为"Buick"的扩展实现
Car buick = extensionLoader.getExtension("Buick");
// 运行
buick.run();

IOC

IOC即控制翻转,依赖注入或者叫自动装配,即在某个容器中存在多个单例对象,当某个对象的属性是容器中另一个对象时,自动给属性赋值为这个依赖对象,大家都在用Spring,就不多做解释了

回到上例,假如我们要给车上加一个导航,而导航也有很多种实现:比如高德地图、百度地图等,所以导航我们也用SPI机制使其可以配置并扩展

// 抽象导航
@SPI
public interface Navigation {
    void start();
}
// 高德导航
public class AmapNavigation implements Navigation {
    @Override
    public void start(URL url) {
        System.out.println("高德地图开始导航...");
    }
}
// 谷歌导航
public class GoogleNavigation implements Navigation {
    @Override
    public void start(URL url) {
        System.out.println("谷歌地图开始导航...");
    }
}

META-INF/dubbo目录下,Navigation全限定名配置如下

Amap=com.xxx.AmapNavigation
Google=com.xxx.GoogleNavigation

到此我们也可以运行时选择某个实现执行导航了

ExtensionLoader<Navigation> navigationExtensionLoader = ExtensionLoader.getExtensionLoader(Navigation.class);
Navigation amap = navigationExtensionLoader.getExtension("Amap");
amap.start(); // 高德地图开始导航...

问题来了,如果现在想给奥迪汽车加上一个导航,并在汽车跑起来时自动开启导航,怎么办?

Dubbo SPI可以支持自动装配,官方文档:

加载扩展点时,自动注入依赖的扩展点。加载扩展点时,扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有 setter 方法来判定其成员。即 ExtensionLoader 会执行扩展点的拼装操作。

回到例子,所以只要给Car的实现Audi加上类型为Navigation的成员变量,并提供setter方法即可自动检测自动装配,如下

@Setter
public class Audi implements Car {
    // 导航
    private Navigation navigation;

    @Override
    public void run() {
        // 汽车启动
        System.out.println("Audi is running");
        // 开启内置导航
        this.navigation.start(url);
    }
}

此时再运行一下会发现红红的一篇报错。。。

问题出在,我们只是说要给奥迪车配导航,并没有指定具体是什么导航,再牛逼的框架也不知道该给你注入什么对象

问题在于缺少一个配置(指定使用什么实现),可问题在于dubbo再运行时才会知道具体的配置,不同的请求可能配置使用的实现不一样,比如负载均衡:

@DubboReference(loadbalance = "random")

每个服务可以配置不同的负载均衡策略,所以只有在实际调用时才能知道具体策略

URL配置

Dubbo每次请求发起,信息会封装在URL对象中,包含protocol(网络协议),ip,port(端口)等信息

除此之外,本次请求的配置信息,包含某个个扩展点具体使用哪个实现,也会以(Key-Value)的形式存在URL里,因此URL对象就包含了上面所需要的配置信息

URL

而这个URL对象,作为单次请求的配置,在每个扩展点的方法上以参数的形式传递,因此我们把扩展点代码加上URL参数

导航
//抽象导航
@SPI
public interface Navigation {
    void start(URL url);
}
// Google导航
public class GoogleNavigation implements Navigation {
    @Override
    public void start(URL url) {
        System.out.println("谷歌地图开始导航...");
    }
}
// 高德导航
public class AmapNavigation implements Navigation {
    @Override
    public void start(URL url) {
        System.out.println("高德地图开始导航...");
    }
}
汽车
// 车
@SPI
public interface Car {
    void run(URL url);
}
// 装配导航的奥迪车
@Setter
public class Audi implements Car {

    // 装配导航
    private Navigation navigation;

    @Override
    public void run(URL url) {
        // 汽车启动
        System.out.println("Audi is running");
        // 开启内置导航
        this.navigation.start(url);
    }
}
// 别克车
public class Buick implements Car {
    @Override
    public void run(URL url) {
        System.out.println("Buick is running");
    }
}

使用时传入配置,规定使用的导航

public static void main(String[] args) {
    // 配置使用高德导航
    Map<String, String> parameters = new HashMap<String, String>() {{
        put("navigation", "Amap");
    }};
    // 协议端口就随便写了
    URL url = new URL("protocol", "ip", 0, parameters);
    ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
    // 获取 奥迪 的实现
    Car buick = extensionLoader.getExtension("Audi");
    // 运行
    buick.run(url);
}

自适应机制

有了URL配置,我们Car运行时就知道要使用什么导航,但还是有问题

  • 只有运行时才能知道需要什么导航,难道车跑起来现场加载导航吗?
  • 如果真的是运行时加载导航,那就不是自动装配了,而且用户还得自己在run方法实现,很不友好

综上所述,自动装配是肯定要装配的,但问题是到底装配哪个导航,高德or谷歌?

答案是二者都不对,因为无论装配任何一个,运行时URL的配置不是该导航名都会出问题

而实际上装配的是一个新导航,这个导航有这样的特点,它可以获取到两种导航,并且运行时根据URL配置,内部使用不同的导航,相当于给两种导航包了壳子,也就是代理

有了这个新导航,即完成了自动装配,又可以在配置不同时切换不同的实际导航实现,这种可以根据运行时配置切换实际实现的机制即为自适应机制

我们可以代码实现一下这个新导航,即自适应扩展

// 标志是一个自适应扩展
@Adaptive
public class AdaptiveNavigation implements Navigation {
    @Override
    public void start(URL url) {
        // 获取配置中的名称
        String name = url.getParameter("navigation");
        // 导航实现加载器
        ExtensionLoader<Navigation> extensionLoader = ExtensionLoader.getExtensionLoader(Navigation.class);
        // 按配置名称获取实现
        Navigation impl = extensionLoader.getExtension(name);
        // 使用实际实现的start方法
        impl.start(url);
    }
}

代码使用了@Adaptive注解标志其为自适应扩展,代码中读取配置key "navigation"来获取实际扩展实现的名字,同时配置文件中也要加入该扩展

Adaptive=com.pq.pure.spi.navigation.AdaptiveNavigation

运行一下

public static void main(String[] args) {
    // 配置使用高德导航
    Map<String, String> parameters = new HashMap<String, String>() {{
        put("navigation", "Amap");
    }};
    // 协议端口就随便写了
    URL url = new URL("protocol", "ip", 0, parameters);
    ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
    // 获取 奥迪 的实现
    Car buick = extensionLoader.getExtension("Audi");
    // 运行
    buick.run(url);
}

输出如下

Audi is running
高德地图开始导航...

到此,真正的实现了自动装配~

自动生成的自适应扩展

上面这个自适应扩展如果自己写真的很麻烦,而且还得懂ExtensionLoader的使用才可以,实际上这个自适应扩展可以自动生成出来

这个仅凭抽象接口造出自适应扩展的技术也很简单:JDK动态代理,写法如下:

1.首先删除手写的自适应扩展,以免干扰测试

2.在扩展类接口Navigation方法上加上@Adaptive注解,并指明所参照的的参数key

@SPI
public interface Navigation {
    // 标记可以创建自适应扩展,配置key为navigation
    @Adaptive({"navigation"})
    void start(URL url);
}

这个@Adaptive是加在方法上而不是类上,估计是为了不同方法使用不同的配置key

再次测试一下,效果一样

到底,彻底实现了简单的自动装配~

自动装配源码

回到Dubbo源码层面证实一下上面所说的,setter注入/自适应扩展/JDK动态代理等逻辑

setter注入

来到核心类ExtensionLoader(扩展加载器)的createExtension(创建扩展)中执行的injectExtension方法,该方法专门处理依赖注入

private T injectExtension(T instance) {
    ...
    // 循环所有方法
    for (Method method : instance.getClass().getMethods()) {
        // 是否是setter方法
        if (!isSetter(method)) {
            continue;
        }
        ...
        // 获取setter属性类型
        Class<?> pt = method.getParameterTypes()[0];
        try {
            String property = getSetterProperty(method);
            // 获取依赖对象
            Object object = objectFactory.getExtension(pt, property);
            // 反射调用setter方法注入对象
            if (object != null) {
                method.invoke(instance, object);
            }
        } catch (Exception e) {
            ...
        }
    }
    return instance;
}

可以看到Dubbo检查setter方法的逻辑,如果有则依赖注入

ExtensionFactory

其中获取依赖对象的方法为objectFactory.getExtension(pt, property),而objectFactory的类型是ExtensionFactory

ExtensionFactory即是一个加载依赖对象的工厂,并且支持扩展,它只有一个方法,即根据类型和名称获取依赖对象

@SPI
public interface ExtensionFactory {
    <T> T getExtension(Class<T> type, String name);
}

objectFactory在构造方法中被初始化

private ExtensionLoader(Class<?> type) {
    this.type = type;
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

实例化的结果是一个ExtensionFactory(可扩展接口)的自适应扩展,其中getAdaptiveExtension为获取自适应扩展的方法,看一下其扩展配置

spring=org.apache.dubbo.config.spring.extension.SpringExtensionFactory
adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory

其中有一个adaptive实现,打开看一下

AdaptiveExtensionFactory

首先该扩展有@Adaptive注解,标识这是一个自适应扩展,这种情况下getAdaptiveExtension方法获取的扩展就是这个类,而不是jdk自动生成,而这个自适应扩展的获取逻辑简单粗暴: 从所有普通扩展中获取依赖,哪个能加载到就返回

而普通扩展有两个SpiExtensionFactory和SpringExtensionFactory

SpiExtensionFactory

SpiExtensionFactory就是从其它扩展实现加载,实现方法就是根据type获取自适应扩展(没有就通过动态代理生成)

public class SpiExtensionFactory implements ExtensionFactory {

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        // 是接口且携带@SPI注解
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            // 获取该类型扩展类加载器
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if (!loader.getSupportedExtensions().isEmpty()) {
                // 获取自适应扩展
                return loader.getAdaptiveExtension();
            }
        }
        return null;
    }

}
SpringExtensionFactory

SpringExtensionFactory从spring容器加载bean,这个很实用

public <T> T getExtension(Class<T> type, String name) {

    //SPI should be get from SpiExtensionFactory
    if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
        return null;
    }

    //从ApplicationContext中获取bean
    for (ApplicationContext context : CONTEXTS) {
        T bean = BeanFactoryUtils.getOptionalBean(context, name, type);
        if (bean != null) {
            return bean;
        }
    }

    return null;
}

比如当我们自定义一些扩展,扩展的一些参数有依赖于数据库配置,这种注入就非常方便了

有关Dubbo SPI自适应扩展和IOC的更多相关文章

  1. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  2. c - mkmf 在编译 C 扩展时忽略子文件夹中的文件 - 2

    我想这样组织C源代码:+/||___+ext||||___+native_extension||||___+lib||||||___(Sourcefilesarekeptinhere-maycontainsub-folders)||||___native_extension.c||___native_extension.h||___extconf.rb||___+lib||||___(Rubysourcecode)||___Rakefile我无法使此设置与mkmf一起正常工作。native_extension/lib中的文件(包含在native_extension.c中)将被完全忽略。

  3. ruby-on-rails - 向 Rails 3 添加 Ruby 扩展方法的最佳实践? - 2

    我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion

  4. ruby - 如何在 ruby​​ 中复制目录结构,不包括某些文件扩展名 - 2

    我想编写一个ruby​​脚本来递归复制目录结构,但排除某些文件类型。因此,给定以下目录结构:folder1folder2file1.txtfile2.txtfile3.csfile4.htmlfolder2folder3file4.dll我想复制这个结构,但不包含.txt和.cs文件。因此,生成的目录结构应如下所示:folder1folder2file4.htmlfolder2folder3file4.dll 最佳答案 您可以使用查找模块。这是一个代码片段:require"find"ignored_extensions=[".cs"

  5. ruby - 扩展类和实例 - 2

    这个问题有两个部分。在RubyProgrammingLanguage一书中,有一个使用模块扩展字符串对象和类的示例(第8.1.1节)。第一个问题。为什么如果您使用新方法扩展类,然后创建该类的对象/实例,则无法访问该方法?irb(main):001:0>moduleGreeter;defciao;"Ciao!";end;end=>nilirb(main):002:0>String.extend(Greeter)=>Stringirb(main):003:0>String.ciao=>"Ciao!"irb(main):004:0>x="foobar"=>"foobar"irb(main):

  6. ruby - 动态扩展现有方法或覆盖 ruby​​ 中的发送方法 - 2

    假设我们有A、B、C类。Adefself.inherited(sub)#metaprogramminggoeshere#takeclassthathasjustinheritedclassA#andforfooclassesinjectprepare_foo()as#firstlineofmethodthenrunrestofthecodeenddefprepare_foo#=>prepare_foo()neededhere#somecodeendendBprepare_foo()neededhere#somecodeendend如您所见,我正在尝试将foo_prepare()调用注入

  7. ruby-on-rails - 如何扩展 Ruby Test::Unit 断言以包含 assert_false? - 2

    显然在Test::Unit中没有assert_false。您将如何通过扩展断言并添加文件config/initializers/assertions_helper.rb来添加它?这是最好的方法吗?我不想修改test/unit/assertions.rb。顺便说一句,我不认为这是多余的。我使用的是assert_equalfalse,something_to_evaluate。这种方法的问题是很容易意外使用assertfalse,something_to_evaluate。这将始终失败,不会引发错误或警告,并且会在测试中引入错误。 最佳答案

  8. ruby-on-rails - 无法构建 gem native 扩展 (mkmf (LoadError)) - Ubuntu 12.04 - 2

    这个问题在这里已经有了答案:Unabletoinstallgem-Failedtobuildgemnativeextension-cannotloadsuchfile--mkmf(LoadError)(17个答案)关闭9年前。嘿,我正在尝试在一台新的ubuntu机器上安装rails。我安装了ruby​​和rvm,但出现“无法构建gemnative扩展”错误。这是什么意思?$sudogeminstallrails-v3.2.9(没有sudo表示我没有权限)然后它会输出很多“获取”命令,最终会出现这个错误:Buildingnativeextensions.Thiscouldtakeawhi

  9. ruby-on-rails - 使用模块扩展带有 "has_many"的插件中的模型 - 2

    我在引擎样式插件中有一些代码,其中包含一些模型。在我的应用程序中,我想扩展其中一个模型。通过在初始值设定项中包含一个模块,我已经设法将实例和类方法添加到相关模型中。但是我似乎无法添加关联、回调等。我收到“找不到方法”错误。/libs/qwerty/core.rbmoduleQwertymoduleCoremoduleExtensionsmoduleUser#InstanceMethodsGoHere#ClassMethodsmoduleClassMethodshas_many:hits,:uniq=>true#nomethodfoundbefore_validation_on_crea

  10. ruby-on-rails - Ruby 1.9.3 -> 2.0 别名方法和扩展 - 2

    我正在尝试将Ruby1.9.3应用程序升级到2.0,除了一个小问题外,一切似乎都很顺利。我写了一个模块,我将其包含在我的模型中以覆盖activerecorddestroy。它将现有的destroy方法别名为destroy!,然后覆盖destroy以更改记录上的deleted_at时间戳。仅当我升级到ruby​​2.0时,destroy!不再破坏记录,但其行为就像我的新覆盖方法一样。知道为什么会这样吗?下面是更相关的代码部分。完整要点here.defself.included(base)base.class_evaldoalias_method:destroy!,:destroyalia

随机推荐