Dubbo SPI自适应扩展和IOC

作者: pq217 | 来源:发表于2022-08-01 09:58 被阅读0次

    前言

    书接上回,本文主要研究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

        本文链接:https://www.haomeiwen.com/subject/gazlwrtx.html