返回顶部
首页 > 资讯 > 后端开发 > JAVA >趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
  • 794
分享到

趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了

dubbojava面试后端开发语言原力计划 2023-08-16 15:08:55 794人浏览 薄情痞子
摘要

👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、kafka源码系列、JUC源

  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式Spring源码系列、Netty源码系列、kafka源码系列、JUC源码系列、duubo源码系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

在这里插入图片描述

一、引言

兄弟们,上次的故障结果出来了

还好销售团队给力,没有让客户几千万的单子丢掉,成功挽回了本次损失

image-20230717003322489

不过内部处罚还是相对严重,年终奖悬了

image-20230717003338736

这也告诫我们 要对生产保持敬畏之情!

恰巧最近领导看我在写 dubbo 源码系列,看到我们的项目中用了 SPI 扩展

于是给我一个将功补过的机会,让我好好的分析分析 DubboSPI 的扩展机制,进行组内技术分享

作为一个常年分享 源码系列 文章的选手,当然不会拒绝!

乾坤未定,你我皆是黑马,冲!

二、SPI是什么

SPI 全称 Service Provider Interface ,是 Java 提供的一套用来被第三方实现或者扩展的 api,它可以用来启用框架扩展和替换组件。

img

Java SPI 实际上是 基于接口的编程+策略模式+配置文件 组合实现的动态加载机制。

Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。

将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

所以 SPI 的核心思想就是解耦

三、使用介绍

我们定义一个接口:City

@SPIpublic interface City {    String getCityName();}

实现其两个类:

  • BeijingCity
public class BeijinGCity implements City{    @Override    public String getCityName() {        return "北京";    }}
  • TianjinCity
public class TianjinCity implements City{    @Override    public String getCityName() {        return "天津";    }}

重点来了:我们要在 resources 文件夹下面建立一个路径:META-INF/dubbo

然后我们建立一个 txt 名为:com.dubbo.provider.SPI.Dubbo.City,如下:

我们在这个文件中写上各实现类的路径:

beijing=com.dubbo.provider.SPI.Dubbo.BeijingCitytianjin=com.dubbo.provider.SPI.Dubbo.TianjinCity

有的朋友可能会问,这里为什么和 Java SPI 的实现不同?

这也正是 Dubbo 实现精准实例化的原因,我们后面也会聊到

测试方法:

public class DubboSPITest {    public static void main(String[] args) {        ExtensionLoader loader = ExtensionLoader.getExtensionLoader(City.class);        City tianjin = loader.getExtension("beijing");        System.out.println(tianjin.getCityName());    }}

测试结果:

北京

从这里我们可以看出,Dubbo 可以通过 loader.getExtension("beijing") 精确的生成我们需要的实例

精确生成是如何实现的呢?我们继续往下看

四、原理介绍

在源码介绍之前,我们先说几个原理细节,防止大家后面的源码看迷糊

1、SPI注解

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

SPI 注解中,存在两个参数:valuescope

value

  • 作用:如果某个 SPI 扩展没有指定实现类名称,则会使用 @SPI 注解中指定的默认值

scope:指定 SPI 扩展实现类的作用域( Constants.SINGLETON

  • Constants.FRAMEWORK(框架作用域):实现类在 Dubbo 框架中只会创建一个实例,并且在整个应用程序中共享。
  • Constants.APPLICATION(应用程序作用域):实现类在应用程序上下文中只会创建一个实例,并且在整个应用程序中共享。
  • Constants.MODULE(模块作用域):实现类在模块上下文中只会创建一个实例,并且在整个模块中共享。
  • Constants.SELF(自定义作用域):实现类的作用范围由用户自行定义,可以是任何范围。

当然,这里 Dubbo 默认的是 Constants.APPLICATION,我们也只需要关注这个即可。

五、源码剖析

1、Loader的创建

我们 DubboSPIExtensionLoader.getExtensionLoader(City.class) 开始,看一看其实现方案

public  ExtensionLoader getExtensionLoader(Class type) {    // 1、校验    checkDestroyed();    // 2、是否有本地Loader缓存    ExtensionLoader loader = (ExtensionLoader) extensionLoadersMap.get(type);    // 3、是否有本地Scope缓存    ExtensionScope scope = extensionScopeMap.get(type);        // 4、如果当前的Scope为空    // 4.1 获取当前接口类的SPI注解    // 4.2 获取当前注解的scope    // 4.3 放入scope缓存    if (scope == null) {        SPI annotation = type.getAnnotation(SPI.class);        scope = annotation.scope();        extensionScopeMap.put(type, scope);    }    // 5、如果加载器为空且当前是SELF,直接创建loader    if (loader == null && scope == ExtensionScope.SELF) {        loader = createExtensionLoader0(type);    }    // 6、如果当前加载器为空,去父类找加载器    if (loader == null) {        if (this.parent != null) {            loader = this.parent.getExtensionLoader(type);        }    }    // 7、如果父类也没有实例化,那么实例化并放入缓存    if (loader == null) {        loader = createExtensionLoader(type);    }    // 8、返回加载器    return loader;}

从上面的源码我们可以看到,获取 ExtensionLoader 采用了 缓存 + 父类继承 的模式

这种继承机制设计得比较巧妙,可以避免重复加载类,提高系统性能。

2、获取实例

Dubbo 通过 loader.getExtension("tianjin") 获取对应的实例

public T getExtension(String name) {    T extension = getExtension(name, true);    return extension;}public T getExtension(String name, boolean wrap) {    // 1、校验    checkDestroyed();        // 2、参数为true,表明采用默认的实现类    // 2.1 我们上面SPI中的value参数,若指定tianjin,则采用tianjin的实现类    if ("true".equals(name)) {        return getDefaultExtension();    }        String cacheKey = name;    if (!wrap) {        cacheKey += "_origin";    }    // 3、查看当前缓存中是否含有该实例    // 3.1 如果当前的cacheKey没有Holder的话,创建一个    final Holder holder = getOrCreateHolder(cacheKey);        // 4、如果实例为空,采用DCL机制创建实例    Object instance = holder.get();    if (instance == null) {        synchronized (holder) {            instance = holder.get();            if (instance == null) {                instance = createExtension(name, wrap);                holder.set(instance);            }        }    }    return (T) instance;}private Holder getOrCreateHolder(String name) {    // 1、获取当前name的Holder    Holder holder = cachedInstances.get(name);    // 2、没有则创建并扔进缓存    if (holder == null) {        cachedInstances.putIfAbsent(name, new Holder<>());        holder = cachedInstances.get(name);    }    // 3、返回    return holder;} 

Holder 类是一个简单的容器类,用于保存某个对象的引用

DubboExtensionLoader 类中,Holder 类被用于实现对 SPI 扩展实现类的缓存

Holder 结构如下:

public class Holder {    private volatile T value;    public void set(T value) {        this.value = value;    }    public T get() {        return value;    }}

我们创建实例一共有以下几部分:

  • 解析文件配置得到对应的类
  • 通过实例化创建相关的类
  • 初始化之前前置操作
  • 依赖注入
  • 初始化之后后置操作
  • Wrapper 的包装
  • 是否具有生命周期管理的能力

我们挨个的讲解

2.1 解析文件配置

Class clazz = getExtensionClasses().get(name);private Map> getExtensionClasses() {    // 1、从缓存中获取类的信息    Map> classes = cachedClasses.get();        // 2、DCL创建(经典的单例设计模式)    if (classes == null) {        synchronized (cachedClasses) {            classes = cachedClasses.get();            if (classes == null) {                // 3、加载类信息并放至缓存中                classes = loadExtensionClasses();                cachedClasses.set(classes);            }        }    }    return classes;}private Map> loadExtensionClasses() throws InterruptedException {    // 1、校验    checkDestroyed();        // 2、是否有默认的类    // 2.1 我们之前聊过的SPI注解的value机制    cacheDefaultExtensionName();      // 3、这里有三个文件解析器    // 3.1 DubboInternalLoadingStrategy:解析META-INF/dubbo/internal/    // 3.2 DubboLoadingStrategy:解析META-INF/dubbo/    // 3.3 ServicesLoadingStrategy:解析META-INF/services/    // 3.4 解析文件并放至缓存    Map> extensionClasses = new HashMap<>();    for (LoadingStrategy strategy : strategies) {        loadDirectory(extensionClasses, strategy, type.getName());        if (this.type == ExtensionInjector.class) {            loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());        }    }    // tianjin:"class com.msb.dubbo.provider.SPI.Dubbo.TianjinCity"    // beijing:"class com.msb.dubbo.provider.SPI.Dubbo.BeijingCity"    return extensionClasses;}

2.2 实例化创建

// 1、从缓存中获取T instance = (T) extensionInstances.get(clazz);if (instance == null) {    // 2、缓存为空则创建并放至缓存    extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));    instance = (T) extensionInstances.get(clazz);}// 1、获取当前类的所有的构造方法// 2、判断是否有符合的构造方法,若没有则报错// 3、有符合的构造犯法,返回即可private Object createExtensionInstance(Class type) throws ReflectiveOperationException {    return instantiationStrategy.instantiate(type);}

2.3 前置处理

类似 Spirng 的前置处理器,之前也说过,感兴趣的可以看一下,整体思路区别不大

instance = postProcessBeforeInitialization(instance, name);private T postProcessBeforeInitialization(T instance, String name) throws Exception {    if (extensionPostProcessors != null) {        for (ExtensionPostProcessor processor : extensionPostProcessors) {            instance = (T) processor.postProcessBeforeInitialization(instance, name);        }    }    return instance;}

2.4 依赖注入

  • 首先,如果依赖注入器为 null,则直接返回传入的实例。
  • 然后,遍历传入实例的所有方法,找到所有的 setter 方法。
  • 对于每个 setter 方法,如果标注了 @DisableInject 注解,则跳过该方法,不进行注入。
  • 如果 setter 方法的参数类型是基本类型,则跳过该方法,不进行注入。
  • 如果 setter 方法的参数类型不是基本类型,则尝试从依赖注入器中获取该类型对应的实例,并调用该 setter 方法进行注入。
  • 如果获取实例失败,则记录错误日志
  • 最后,返回注入后的实例。
injectExtension(instance);private T injectExtension(T instance) {    for (Method method : instance.getClass().getMethods()) {        // 1、如果不是setter方法,直接跳过        if (!isSetter(method)) {            continue;        }                // 2、包含了DisableInject注解,直接跳过        if (method.isAnnotationPresent(DisableInject.class)) {                continue;            }                        if (method.getDeclaringClass() == ScopeModelAware.class) {                continue;            }                // 3、如果是基本数据类型,跳过            if (instance instanceof ScopeModelAware || instance instanceof ExtensionAccessorAware) {                if (ignoredInjectMethodsDesc.contains(ReflectUtils.getDesc(method))) {                    continue;                }            }            Class pt = method.getParameterTypes()[0];            if (ReflectUtils.isPrimitives(pt)) {                continue;            }            try {                // 4、依赖注入器中获取该类型对应的实例,并调用该 setter 方法进行注入                // 4.1 这里直接拿取的ListableBeanFactory->DefaultListableBeanFactory                String property = getSetterProperty(method);                Object object = injector.getInstance(pt, property);    // 5、将当前的对象注入到实例里面                if (object != null) {                    method.invoke(instance, object);                }            }    return instance;}

2.5 后置操作

  • 类似 Spirng 的后置处理器,之前也说过,感兴趣的可以看一下,整体思路区别不大
instance = postProcessAfterInitialization(instance, name);private T postProcessAfterInitialization(T instance, String name) throws Exception {    if (instance instanceof ExtensionAccessorAware) {        ((ExtensionAccessorAware) instance).setExtensionAccessor(extensionDirector);    }    if (extensionPostProcessors != null) {        for (ExtensionPostProcessor processor : extensionPostProcessors) {            instance = (T) processor.postProcessAfterInitialization(instance, name);        }    }    return instance;}

2.6 Wrapper 的包装

2.6.1 Wrapper缓存

在讲该部分之前,我们先来看 cachedWrapperClasses 这个缓存的来历:

在我们上面解析文件配置时,会进行 loadClass,这里不仅会解析正常的类,也会解析 Wrapper 类,方便后面的包装

private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class clazz, String name,boolean overridden) {    if (isWrapperClass(clazz)) {        cacheWrapperClass(clazz);    }}

从这里我们可以看到,最关键的当属判断当前的 Class 是不是属于 WrapperClass

protected boolean isWrapperClass(Class clazz) {    // 1、获取构造方法    Constructor[] constructors = clazz.getConstructors();    // 2、从构造方法中取出参数为 1 且类型等于当前接口的    for (Constructor constructor : constructors) {        if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0] == type) {            return true;        }    }    return false;}

而具体的实现如下:

public class CityWrapper implements City{    private City city;    // 怎样判断扩展点还是aop切面呢?    // 通过是否有这样的一个构造方法来判断    public CityWrapper(City city) {        this.city = city;    }    @Override    public String getCityName() {        return "文明城市" + city.getCityName();    }}

了解这个之后,我们再来看看 Dubbo 如何处理这些类似 AOP 的包装

2.6.2 Wrapper实现
if (wrap) {    List> wrapperClassesList = new ArrayList<>();// 1、判断是否有Wrapper缓存    // 1.1 将缓存放入当前    // 1.2 排序 + 翻转    if (cachedWrapperClasses != null) {        wrapperClassesList.addAll(cachedWrapperClasses);        wrapperClassesList.sort(WrapperComparator.COMPARATOR);        Collections.reverse(wrapperClassesList);    }    // 2、当前的wrapper缓存不为空    if (CollectionUtils.isNotEmpty(wrapperClassesList)) {        // 循环包装        for (Class wrapperClass : wrapperClassesList) {            // 3、获取Wrapper注解,是否需要包装(正常都是包装的)            Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);            // 4、判断下是否包装条件            boolean match = (wrapper == null) ||                ((ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) &&                    !ArrayUtils.contains(wrapper.mismatches(), name));            // 5、符合包装            // 5.1 将当前类封装至wrapper中            // 5.2 做一些后置处理            if (match) {                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));                instance = postProcessAfterInitialization(instance, name);            }        }    }}

通过这种方式,我们可以创建不同的 wrapper,实现 AOP 的作用

读过 从源码全面解析 dubbo 服务端服务调用的来龙去脉从源码全面解析 dubbo 消费端服务调用的来龙去脉 的文章,这时候应该理解最后的那些 过滤器 怎么实现的了

比如,我们现在有两个 wrapper 类,分别是 CityWrapperCityWrapper2,实现类是 TianjinCity

那么,我们最终 TianjinCity 返回的实例如下:

  • CityWrapper
    • CityWrapper2
      • TianjinCity

image-20230716215711952

不得不说,这个包装还是有点秀秀的

2.7 生命周期管理

  • 实现 Lifecycle 的接口
initExtension(instance);

六、流程图

高清图片私聊博主获取

在这里插入图片描述

七、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

如果你也对 后端架构中间件源码 有兴趣,欢迎添加博主微信hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:

来源地址:https://blog.csdn.net/qq_40915439/article/details/131757391

--结束END--

本文标题: 趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了

本文链接: https://lsjlt.com/news/371519.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作