文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

dubbo的SPI应用与原理是什么

2023-06-05 03:37

关注

dubbo的SPI应用与原理是什么,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

dubbo

SPI(Service Provider Interface)

想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们了解下JAVA SPI与dubbo SPI的用法,再分析DUBBO SPI的源码,本文的dubbo源码是基于2.7.5版本。

JAVA 原生SPI 示例

public interface Car {    String getBrand();}
public class BM implements Car {    public String getBrand() {        System.out.println("BM car");        return "BM";    }}public class Benz implements Car {    public String getBrand() {        System.out.println("benz car");        return "Benz";    }}

举例:JAVA的java.sql包中就定义一个接口Driver,各个服务提供商实现该接口。当我们需要使用某个数据库时就导入相应的jar包。

缺点

dubbo SPI示例

Dubbo的SPI接口都会使用@SPI注解标识,该注解的主要作用就是标记这个接口是一个SPI接口。源码如下:

@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface SPI {        String value() default "";}

该注解只作用在接口上,value用来设置默认拓展类

public class JavaSPITest {    @Test    public void sayHello() throws Exception {        ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);        //按需获取实现类对象        Car car = carExtensionLoader.getExtension("benz");        System.out.println(car.getBrand());    }}

输出结果为

benz carBenz

Dubbo SPI 源码分析

在dubbo SPI示例方法中,我们首先通过ExtensionLoader的 getExtensionLoader 方法获取一个接口的 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象,源码如下,首先是getExtensionLoader 方法:

        private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {        //校验传进的type类是否为空        if (type == null) {            throw new IllegalArgumentException("Extension type == null");        }        //校验传进的type类是否为接口        if (!type.isInterface()) {            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");        }        //校验传进的type类是否有@SPI注解        if (!withExtensionAnnotation(type)) {            throw new IllegalArgumentException("Extension type (" + type +                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");        }        //从ExtensionLoader缓存中查询是否已经存在对应类型的ExtensionLoader实例        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);        if (loader == null) {            //没有就new一个ExtensionLoader实例,并存入本地缓存            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);        }        return loader;    }

getExtensionLoader会对传进的接口进行校验,其中包括是否有@SPI注解校验,这也是在接口上需加@SPI的原因。然后从EXTENSION_LOADERS缓存中获取该接口类型的ExtensionLoader,如果获取不到,则创建一个该接口类型的ExtensionLoader放入到缓存中,并返回该ExtensionLoader

注意这里创建ExtensionLoader对象的构造方法如下:ExtensionLoader.getExtensionLoader获取ExtensionFactory接口的拓展类,再通过getAdaptiveExtension从拓展类中获取目标拓展类。它会设置该接口对应的 objectFactory常量为AdaptiveExtensionFactory。因为AdaptiveExtensionFactory类上加了@Adaptive注解,为什么是AdaptiveExtensionFactory原因在之后的文章会解释,且objectFactory也会在后面用到。

    private ExtensionLoader(Class<?> type) {        this.type = type;        //type通常不为ExtensionFactory类,则objectFactory为ExtensionFactory接口的默认扩展类AdaptiveExtensionFactory        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());    }

当通过ExtensionLoader.getExtensionLoader取到接口的加载器Loader之后,在通过getExtension方法获取需要拓展类对象。该方法的整个执行流程如下图所示
dubbo的SPI应用与原理是什么

参照执行流程图,拓展类对象的获取源码如下:

        private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();     public T getExtension(String name) {        if (StringUtils.isEmpty(name)) {            throw new IllegalArgumentException("Extension name == null");        }        if ("true".equals(name)) {            // 获取默认的拓展实现类,即@SPI注解上的默认实现类, 如@SPI("benz")            return getDefaultExtension();        }        // Holder,顾名思义,用于持有目标对象,从缓存中拿,没有则创建        final Holder<Object> holder = getOrCreateHolder(name);        Object instance = holder.get();        // 双重检查        if (instance == null) {            synchronized (holder) {                instance = holder.get();                if (instance == null) {                    // 创建拓展实例                    instance = createExtension(name);                    // 设置实例到 holder 中                    holder.set(instance);                }            }        }        return (T) instance;    }        private Holder<Object> getOrCreateHolder(String name) {        // 首先通过扩展名从扩展实例缓存中获取Holder对象        Holder<Object> holder = cachedInstances.get(name);        if (holder == null) {            //如果没有获取到就new一个空的Holder实例存入缓存            cachedInstances.putIfAbsent(name, new Holder<>());            holder = cachedInstances.get(name);        }        return holder;    }

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。dubbo中包含了大量的扩展点缓存。这个就是典型的使用空间换时间的做法。也是Dubbo性能强劲的原因之一,包括

  1. 扩展点Class缓存 ,Dubbo SPI在获取扩展点时,会优先从缓存中读取,如果缓存中不存在则加载配置文件,根据配置将Class缓存到内存中,并不会直接初始化。

  2. 扩展点实例缓存 ,Dubbo不仅会缓存Class,还会缓存Class的实例。每次取实例的时候会优先从缓存中取,取不到则从配置中加载,实例化并缓存到内存中。

下面我们来看一下创建拓展对象的过程

        private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();        private T createExtension(String name) {        // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的map,再根据拓展项名称从map中取出相应的拓展类即可        Class<?> clazz = getExtensionClasses().get(name);        if (clazz == null) {            throw findException(name);        }        try {            //从扩展点缓存中获取对应实例对象            T instance = (T) EXTENSION_INSTANCES.get(clazz);            if (instance == null) {                // //如果缓存中不存在此类的扩展点,就通过反射创建实例,并存入缓存                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());                //然后从缓存中获取对应实例                instance = (T) EXTENSION_INSTANCES.get(clazz);            }            // 向实例中注入依赖,通过setter方法自动注入对应的属性实例            injectExtension(instance);            //从缓存中取出所有的包装类,形成包装链            Set<Class<?>> wrapperClasses = cachedWrapperClasses;            if (CollectionUtils.isNotEmpty(wrapperClasses)) {                // 循环创建 Wrapper 实例,形成Wrapper包装链                for (Class<?> wrapperClass : wrapperClasses) {                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));                }            }            //初始化实例并返回            initExtension(instance);            return instance;        } catch (Throwable t) {            throw new IllegalStateException(".....");        }    }

创建拓展类对象步骤分别为:

  1. 通过 getExtensionClasses 从配置文件中加载所有的拓展类,再通过名称获取目标拓展类

  2. 通过反射创建拓展对象

  3. 向拓展对象中注入依赖

  4. 将拓展对象包裹在相应的 Wrapper 对象中

第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。我们先重点分析getExtensionClasses 方法的逻辑getExtensionClasses 方法的逻辑。

从配置文件中加载所有的拓展类

SPI 注解解析过程比较简单,源码如下。只允许一个默认拓展类。

    private void cacheDefaultExtensionName() {        // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入,代表接口类        final SPI defaultAnnotation = type.getAnnotation(SPI.class);        if (defaultAnnotation == null) {            return;        }        String value = defaultAnnotation.value();        if ((value = value.trim()).length() > 0) {            String[] names = NAME_SEPARATOR.split(value);            // 检测 SPI 注解内容是否合法(至多一个默认实现类),不合法则抛出异常            if (names.length > 1) {                throw new IllegalStateException("...");            }            // 设置默认拓展类名称            if (names.length == 1) {                cachedDefaultName = names[0];            }        }    }

从源码中可以看出loadExtensionClasses方法加载配置文件的路径有3个,分别为META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/三个文件夹。方法源码如下:

    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {        loadDirectory(extensionClasses, dir, type, false);    }        private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {        // fileName = 文件夹路径 + type 全限定名        String fileName = dir + type;        try {            Enumeration<java.net.URL> urls = null;            //获取当前线程的类加载器            ClassLoader classLoader = findClassLoader();            // try to load from ExtensionLoader's ClassLoader first            if (extensionLoaderClassLoaderFirst) {                //获取加载ExtensionLoader.class这个类的类加载器                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();                //如果extensionLoaderClassLoaderFirst=true时,且这两个类加载器不同,就优先使用 extensionLoaderClassLoader                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {                    urls = extensionLoaderClassLoader.getResources(fileName);                }            }            // 根据文件名加载所有的同名文件            if(urls == null || !urls.hasMoreElements()) {                if (classLoader != null) {                    urls = classLoader.getResources(fileName);                } else {                    urls = ClassLoader.getSystemResources(fileName);                }            }            if (urls != null) {                while (urls.hasMoreElements()) {                    java.net.URL resourceURL = urls.nextElement();                    // 解析并加载配置文件中配置的实现类到extensionClasses中去                    loadResource(extensionClasses, classLoader, resourceURL);                }            }        } catch (Throwable t) {            logger.error("").", t);        }    }

首先找到文件夹下的配置文件,文件名需为接口全限定名。利用类加载器获取文件资源链接,再解析配置文件中配置的实现类添加到extensionClasses中。我们继续看loadResource是如何加载资源的。

    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {        try {            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {                String line;                // 按行读取配置内容                while ((line = reader.readLine()) != null) {                    final int ci = line.indexOf('#');                    if (ci >= 0) {                        // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略                        line = line.substring(0, ci);                    }                    line = line.trim();                    if (line.length() > 0) {                        try {                            String name = null;                            int i = line.indexOf('=');                            if (i > 0) {                                // 以等于号 = 为界,截取键与值                                name = line.substring(0, i).trim();                                line = line.substring(i + 1).trim();                            }                            if (line.length() > 0) {                                // 通过反射加载类,并通过 loadClass 方法对类进行缓存                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);                            }                        } catch (Throwable t) {                            .....                        }                    }                }            }        } catch (Throwable t) {            logger.error(....);        }    }

loadResource 方法用于读取和解析配置文件,按行读取配置文件,每行以等于号 = 为界,截取键与值,并通过反射加载类,最后通过loadClass方法加载扩展点实现类的class到map中,并对加载到的class进行分类缓存。loadClass方法实现如下

        private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {        //判断配置的实现类是否是实现了type接口        if (!type.isAssignableFrom(clazz)) {            throw new IllegalStateException("...");        }        //根据配置中实现类的类型来分类缓存起来        // 检测目标类上是否有 Adaptive 注解,表示这个类就是一个自适应实现类,缓存到cachedAdaptiveClass        if (clazz.isAnnotationPresent(Adaptive.class)) {            cacheAdaptiveClass(clazz);        // 检测 clazz 是否是 Wrapper 类型,判断依据是是否有参数为该接口类的构造方法,缓存到cachedWrapperClasses        } else if (isWrapperClass(clazz)) {            cacheWrapperClass(clazz);        } else {            // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常            clazz.getConstructor();            // 如果配置文件中key的name 为空,则尝试从Extension注解中获取 name,或使用小写的类名作为name。            // 已经弃用,就不在讨论这种方式            if (StringUtils.isEmpty(name)) {                name = findAnnotationName(clazz);                if (name.length() == 0) {                    throw new IllegalStateException("...");                }            }            //使用逗号将name分割为字符串数组            String[] names = NAME_SEPARATOR.split(name);            if (ArrayUtils.isNotEmpty(names)) {                //如果扩展点配置的实现类使用了@Activate注解,就将对应的注解信息缓存起来                cacheActivateClass(clazz, names[0]);                for (String n : names) {                    //缓存扩展点实现类class和扩展点名称的对应关系                    cacheName(clazz, n);                    //最后将class存入extensionClasses                    saveInExtensionClass(extensionClasses, clazz, n);                }            }        }    }

loadClass方法实现了扩展点的分类缓存功能,如包装类,自适应扩展点实现类,普通扩展点实现类等分别进行缓存。需要注意的是自适应扩展点实现类@Adaptive注解,该注解源码如下

     *For example, given <code>String[] {"key1", "key2"}</code>:     * <ol>     * <li>find parameter 'key1' in URL, use its value as the extension's name</li>     * <li>try 'key2' for extension's name if 'key1' is not found (or its value is empty) in URL</li>     * <li>use default extension if 'key2' doesn't exist either</li>     * <li>otherwise, throw {@link IllegalStateException}</li>     * @return     */    @Documented    @Retention(RetentionPolicy.RUNTIME)    @Target({ElementType.TYPE, ElementType.METHOD})    public @interface Adaptive {        String[] value() default {};    }

该注解的作用是决定哪个自适应拓展类被注入,该目标拓展类是由URL中的参数决定,URL中参数key由该注解的value给出,该key的value作为目标拓展类名称。

@Adaptive注解可以作用的类上与方法上,绝大部分情况下,该注解是作用在方法上,当 Adaptive 注解在类上时,Dubbo 不会为该类生成代理类。注解在方法(接口方法)上时,Dubbo 则会为该方法生成代理类Adaptive 注解在接口方法上,表示拓展的加载逻辑需由框架自动生成。注解在类上,表示拓展的加载逻辑由人工编码完成。

上述的loadClass扫描的是作用在类上。在 Dubbo 中,仅有两个类被@Adaptive注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory
loadClass方法设置缓存cacheAdaptiveClass会导致接口的cacheAdaptiveClass不为空,后面都会默认用这个拓展类,优先级最高。

回到主线,当执行完loadClass方法,配置文件中的所有拓展类已经被加载到map中,到此,关于缓存类加载的过程就分析完了。

Dubbo IOC

getExtensionClasses()方法执行流程完成后,再根据拓展项name从map中取出相应的拓展类即可获取扩展类Class,通过反射创建实例,并通过injectExtension(instance);方法向实例中注入依赖 这一部分在下一篇文章中将会介绍

DUBBO AOP

当执行完injectExtension(T instance)方法,在createExtension(String name)就开始执行wrapper的包装,类似于spring中的AOP,dubbo运用了装饰器模式。

        Set<Class<?>> wrapperClasses = cachedWrapperClasses;        if (CollectionUtils.isNotEmpty(wrapperClasses)) {            // 循环创建 Wrapper 实例,形成Wrapper包装链            for (Class<?> wrapperClass : wrapperClasses) {                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));            }        }

这里的cachedWrapperClasses通过前面的分析已经知道,就是在解析配置文件时判断是否是Wrapper类型的拓展类,++判断依据为构造方法中是否有参数为该接口类++,则缓存到cachedWrapperClasses。

执行wrapperClass.getConstructor(type).newInstance(instance)将获取包装类的构造方法,方法的参数就是该接口类型,并通过反射生成一个包含该拓展类实例的包装对象,再通过injectExtension注入包装对象的依赖。如此循环,得到成Wrapper包装链。这里需注意的是,配置文件中内容靠后的包装类会包装在相对外层。下面是DUBBO AOP的例子,我们继续使用上面的Car接口与实现类,同时新增一个实现类,代码如下

public class CarWrapper implements Car{    private Car car;        public CarWrapper(Car car) {        this.car = car;    }    @Override    public String getBrand() {        System.out.println("校验");        String result = car.getBrand();        System.out.println("记录日志");        return result;    }}

该接口实现了Car,并且持有一个Car对象,同时拥有一个构造方法且该构造方法的参数为Car接口类型,那么该类会被识别为接口的Wrapper类。则可以在方法中做一些切面功能的扩展,再调用car对象执行其方法实现AOP功能。

将配置文件内容中添加wrapper实现类,如下

benz=com.xiaoju.automarket.energy.scm.rpc.Benzbm=com.xiaoju.automarket.energy.scm.rpc.BMcom.xiaoju.automarket.energy.scm.rpc.CarWrapper #包装类

执行如下代码获取benz的拓展类实例后,调用其方法,将会被Wrapper包装类

public class TestAOP {    public static void main(String[] args) {        ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);        Car car = carExtensionLoader.getExtension("benz");        System.out.println(car.getBrand(null));    }}

结果如下

校验benz car记录日志Benz

与我们预想的一致,实现了Wrapper类的切面功能。

看完上述内容,你们掌握dubbo的SPI应用与原理是什么的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注编程网行业资讯频道,感谢各位的阅读!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯