Dubbo的独门绝技,SPI实现原理分析

文章目录

  • 前言
  • 普通SPI实现原理
    • 实例化扩展点源码分析
      • 扩展点加载流程分析
        • LoadingStrategy分析
          • 接口定义
          • 接口实现
          • 加载原理
        • loadClass方法分析
  • 自适应SPI实现原理
    • 自适应扩展代码生成分析
  • 自激活SPI
    • 简单使用
    • 原理分析
      • Activate注解
      • 源码分析
  • IOC实现原理
    • objectFactory介绍
    • 总结
  • AOP实现原理
  • 总结

本专栏对应Dubbo版本:2.7.8

官方文档地址:https://dubbo.apache.org/zh/docsv2.7/dev/

官方GitHub地址:https://github.com/apache/dubbo/releases/tag/dubbo-2.7.8

前言

在上篇文章我们已经对Dubbo中的SPI有了简单的了解,接下来我们通过源码详细了解其实现细节。

在本文中,我将SPI分为普通SPI,与之相对应的是自适应SPI,这个概念是笔者“捏造”的,为了更好的划分文章结构,读者不必纠结字眼。

普通SPI实现原理

核心的API为

// 第一步:获取到对应接口的ExtensionLoader
ExtensionLoader<SpiService> extensionLoader = ExtensionLoader.getExtensionLoader(SpiService.class);
// 第二步:通过ExtensionLoader实例化具体扩展点
SpiService internal = extensionLoader.getExtension("internal");

获取ExtensionLoader的逻辑非常简单,大家自行阅读源码即可,如何创建扩展点才是我们的重点

实例化扩展点源码分析

在没有进行源码分析之前大家应该能想到,要得到一个扩展点实现类对象起码要做这么几件事

  1. 根据接口名找到并解析配置文件,加载对应的扩展点实现类
  2. 类加载成功后就可以反射创建对象了
  3. 之后再对这个对象完成IOC及AOP

实际上Dubbo也确实是这么做的,接下来我们分析其源码

扩展点实例化核心方法如下,源码比较简单,这里我只保留了其骨架,我们先对整体流程有一个认知

// 直接定位到调用的核心方法
// name:为传入的扩展点名称
// wrap:代表是否要进行AOP,默认为true
private T createExtension(String name, boolean wrap) {
  
  // 1️⃣.加载扩展点实现类,得到class对象
  Class<?> clazz = getExtensionClasses().get(name);
  
  // 2️⃣.这里通过反射调用空参构造函数,完成实例化
  T instance = (T) EXTENSION_INSTANCES.get(clazz);
  if (instance == null) {  
    EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
    instance = (T) EXTENSION_INSTANCES.get(clazz);
  }

  // 3️⃣.IOC
  injectExtension(instance);

  // 4️⃣.AOP
  if (wrap) {
    // .....
  }
  
  // 5️⃣.扩展点生命周期,进一步完成初始化
  initExtension(instance);
  return instance;
}

在上面的代码中我们可以清晰的看到整个扩展点实例化分为5步,如下图所示

image-20230104172505803

扩展点加载流程分析

在这一小节中,我们主要分析Dubbo是如何加载扩展点的,核心代码位于org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses,如下:

private Map<String, Class<?>> loadExtensionClasses() {
  	// 我们可以在@SPI的注解中指定默认要使用的SPI名称
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
		// dubbo中内置了一些加载策略
    for (LoadingStrategy strategy : strategies) {
      loadDirectory(extensionClasses, strategy.directory(), type.getName(),
                strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        // 这里主要是为了向下兼容alibaba Dubbo
        loadDirectory(extensionClasses, strategy.directory(), type.getName().
                replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}

要理解上述代码,我们首先要搞懂LoadingStrategy的作用,其次我们知道在strategies这个集合中放入了哪些LoadingStrategy

LoadingStrategy分析

接口定义

image-20230104172622018

可以看到这个接口继承了Prioritized,Prioritized的主要作用是定义加载的优先级。LoadingStrategy的作用在于定义加载SPI配置文件时的策略,例如:从哪个目录下加载、哪些不需要加载等

public interface LoadingStrategy extends Prioritized {
		// 定义了SPI配置文件的加载地址
    String directory();
		
    // 目前没有看到哪个实现类复写了这个方法,可忽略
    default boolean preferExtensionClassLoader() {
        return false;
    }
		
  	// 排除指定包下的SPI实现类
    default String[] excludedPackages() {
        return null;
    }

  	// 如果一个SPI存在多个同名的实现的时候,是否要进行覆盖
    default boolean overridden() {
        return false;
    }
}
接口实现

在Dubbo中内置了三种LoadingStrategy,分别为:

  1. DubboInternalLoadingStrategy:加载优先级最高(Integer.MIN_VALUE),加载目录为:META-INF/dubbo/internal/,不支持覆盖。这个策略主要是Dubbo内部使用的

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SKPgpOCS-1679484853206)(/Users/mingzhidai/Library/Application Support/typora-user-images/image-20211219161506784.png)]

    注意:这个策略的优先级是最高的,因此META-INF/dubbo/internal/这个目录下的类会被最先加载,同时它是不支持覆盖的,因此如果同名的实现存在多个,第一个会生效

  2. DubboLoadingStrategy:正常加载优先级(0),加载目录为:META-INF/dubbo/,支持覆盖。这个策略是提供给外部开发者使用的,我们平常进行扩展时,更多是使用这个目录。由于这个

    image-20230104172712191

    注意:这个策略的优先级低于DubboInternalLoadingStrategy,因此META-INF/dubbo/这个目录下的类会晚于META-INF/dubbo/internal/中的类被加载,同时它又能支持覆盖已存在的同名实现类,这就意味在META-INF/dubbo/目录中的类的优先级是高于META-INF/dubbo/internal/的,通过这种方式我们就能替换Dubbo中默认的SPI实现,这也体现了内核+插件的思想

  3. ServicesLoadingStrategy:加载目录为:META-INF/services/,加载优先级最低(0),同时也支持覆盖。这个策略兼容了JDK原生SPI的加载目录。

    image-20230104172931663

    注意:这个策略的优先级是最低的,同时它支持覆盖,因为这个目录下的类的优先级是最高的,会覆盖之前加载的类。

    整个优先级及是否覆盖的设计也体现了一个思想:越靠近应用,优先级越高,方便扩展

加载原理

LoadingStrategy的加载本身使用的是JDK原生的SPI,加载逻辑见org.apache.dubbo.common.extension.ExtensionLoader#loadLoadingStrategies,如下图所示:

image-20230104173020888我们查看Dubbo Jar包内的META-INF/services/目录也能看到以下内容

image-20230104173050037

loadClass方法分析

在了解了LoadingStrategy之后,我们回头继续分析扩展点实现类的加载流程

  1. org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses ☞ 入口

    ⇣⇣⇣⇣

  2. org.apache.dubbo.common.extension.ExtensionLoader#loadDirectory ☞ 加载目录

    ⇣⇣⇣⇣

  3. org.apache.dubbo.common.extension.ExtensionLoader#loadResource ☞ 一个目录下会有很多配置文件,逐个解析配置文件

    ⇣⇣⇣⇣

  4. org.apache.dubbo.common.extension.ExtensionLoader#loadClass ☞ 通过配置文件可以拿到全类名,加载并缓存

整个流程还是非常清晰的,先加载目录,通过对前面LoadingStrategy学习大家应该知道,不同加载策略的加载目录是不同的。目录加载完成后会得到一个配置文件列表,之后通过loadResource方法逐个加载配置文件,在这个过程中可以拿到所有扩展点实现的全类名,最后加载所有的扩展点实现类,并进行缓存。关于前面配置文件的加载较为简单,本文不做详细介绍,读者可自行阅读源码。我们着重看loadClass方法的处理逻辑,代码如下:

下面的代码忽略了一些简单及异常处理,只保留了核心逻辑

// extensionClasses:一个缓存map,key为SPI名称,value为对应的实现类Class对象
// resourceURL:这个参数传入进来只是为了更好的描述异常信息
// clazz:本次要被加载的类,通过Class.forName(”全类名“, true, classLoader)加载而来
// overridden:对应的加载策略是否允许覆盖已存在的类
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,boolean overridden) throws NoSuchMethodException {
  	// 1️⃣.根据是否存在Adaptive注解判断实现类是否是一个Adaptive类,并进行缓存
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz, overridden);
    // 2️⃣.根据是否存在一个参数类型为当前SPI接口的构造函数判断是否是一个wrapper类,用于AOP
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        // 3️⃣.SPI实现类必须要有一个空参构造函数,
        // 这里相当于一个检查,保证缓存的扩展点实现是可用的
        // 如果不存在这个构造函数,这一步就会抛出异常
        clazz.getConstructor();
      	
        // 4️⃣.如果使用在配置文件中没有对这个SPI扩展点命名,
        // 那么尝试直接从实现类中解析出该扩展点的名称
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
        }
      	
      	// 使用逗号对该name进行切割
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            // 5️⃣.缓存自激活扩展实现类,关于自激活我们在后文中分析
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
              	// 缓存扩展点名称,只会缓存第一个名称
                cacheName(clazz, n);
              	// 6️⃣.缓存扩展点实现,一个扩展点实现可以有多个名称
              	// 但一个名称只能对应一个扩展点实现
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

上面代码逻辑非常简单,配合注释大家基本能看懂,注释中提到的自激活扩展实现类我会在后文进行分析。经过上面的分析,我们可以得到下面一张关系图:

image-20230104173217275

  1. 一个SPI接口,对应一个ExtensionLoader
  2. ExtensionLoader中会按照不同的分类将加载的类缓存到不同字段中

到目前为止,我们已经知道了扩展点实现类是如何加载的, 对于整个扩展点实例化的流程我们已经完成了一大部分,再回过头来看一下整个扩展点实例化的流程图,如下:

image-20230104173326824

按照图中流程,我现在应该跟大家介绍Dubbo中的IOC及AOP,不过由于IOC依赖了自适应SPI,所以这里我们先来详细了解一下自适应SPI,关于其使用我已经在之前的文章中介绍过了,本文更多的是分析原理

自适应SPI实现原理

使用自适应扩展的API如下:

ExtensionLoader<OrderService> orderServiceExtensionLoader = ExtensionLoader.getExtensionLoader(SPI接口).getAdaptiveExtension();

自适应扩展的核心代码如下:

private Class<?> getAdaptiveExtensionClass() {
    // 实际上就是在加载配置文件中的类,并进行缓存 
    getExtensionClasses();
    // 是否存在被Adaptive注解修饰的自适应扩展类
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
  	// 如果不存在的话,通过代码生成创建一个自适应扩展类
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

private Class<?> createAdaptiveExtensionClass() {
  // 生成代码,其实就是字符串的拼接
  String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
  ClassLoader classLoader = findClassLoader();
  // 编译,并返回具体的Class对象
  org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
  return compiler.compile(code, classLoader);
}

通过上面的代码要知道的是,自适应SPI有两种形式

  1. 在扩展点实现类上添加Adaptive注解,指定由这个类来实现自适应逻辑
  2. 如果我们没有添加Adaptive注解, 那么Dubbo会帮我们生成一个自适应类来完成适配逻辑

自适应扩展代码生成分析

关于代码生成的细节本文不做过多分析,主要是一些字符串的拼接,生成code字符串后再进行编译。我们重点关注最终生成的代码,首先我们编写一个测试接口,如下:

@SPI
public interface AdaptiveSpi {

    /**
     * 注意这个方法有两个特征
     * <p>
     * 1.方法上有adaptive注解
     * <p>
     * 2.方法的参数中有一个URL,需要注意的是这是一个org.apache.dubbo.common.URL,不是java.net.URL
     */
    @Adaptive("adaptive1")
    void adaptiveMethod1(URL url);

    @Adaptive("adaptive2")
    void adaptiveMethod2(URLHolder url);

    @Adaptive
    void adaptiveMethod3(URLHolder url, Invocation invocation);

    /**
     * 普通方法,用于观察最终生成的代码
     */
    void normalMethod();

    class URLHolder {

        private final URL url;

        public URLHolder(URL url) {
            this.url = url;
        }

        public URL getUrl() {
            return url;
        }
    }
}

对应的生成的自适应扩展实现类代码如下:

限于篇幅问题,在下面的代码中我省去了一些检查相关代码。例如:url不能为空,扩展点名称不能为空等

public class AdaptiveSpi$Adaptive implements com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi {
  public void adaptiveMethod1(com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi.URLHolder arg0) {
    org.apache.dubbo.common.URL url = arg0.getUrl();
    String extName = url.getParameter("adaptive1");
    com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi extension = (com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi) ExtensionLoader.getExtensionLoader(com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi.class).getExtension(extName);
    extension.adaptiveMethod2(arg0);
  }

  public void adaptiveMethod2(org.apache.dubbo.common.URL arg0) {
    org.apache.dubbo.common.URL url = arg0;
    String extName = url.getParameter("adaptive2");
    com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi extension = (com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi) ExtensionLoader.getExtensionLoader(com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi.class).getExtension(extName);
    extension.adaptiveMethod1(arg0);
  }

  public void adaptiveMethod3(com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi.URLHolder arg0, org.apache.dubbo.rpc.Invocation arg1) {
    org.apache.dubbo.common.URL url = arg0.getUrl();
    String methodName = arg1.getMethodName();
    String extName = url.getMethodParameter(methodName, "adaptive.spi", "null");
    com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi extension = (com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi) ExtensionLoader.getExtensionLoader(com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi.class).getExtension(extName);
    extension.adaptiveMethod3(arg0, arg1);
  }

  public void normalMethod() {
    throw new UnsupportedOperationException("The method public abstract void com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi.normalMethod() of interface com.easy4coding.dubbo.spi.service.adaptive.AdaptiveSpi is not adaptive method!");
  }
}

观察上面3个方法,我们可以得出以下结论:

  1. 自适应SPI接口上中的方法如果要用到自适应的能力,必须要有@Adaptive注解,例如的normalMethod直接抛出了UnsupportedOperationException。所谓自适应能力,我们在上篇文章中已经解释过了,就是根据方法调用的参数去适配真实的SPI实现类

  2. 自适应SPI实际调用的是其它实际的扩展点实现类,调用的API就是我们之前分析过的ExtensionLoader.getExtensionLoader(扩展点名称).getExtension()

  3. 真正要使用的扩展点的名称是从URL的参数中解析出来的,同时会将@Adaptive注解中的值作为key,从而去从URL中解析出扩展点名称,如果@Adaptive注解中没有配置value属性的话,那么会将类名转换为key,如果类名是驼峰命名的方式的话,那么会将驼峰转换为"."分隔的形式,例如AdaptiveSpi会被转换为adaptive.spi(见adaptiveMethod3中的逻辑)

  4. 另外,根据自适应方法(被@Adaptive注解修饰的方法)的参数中是否存在org.apache.dubbo.rpc.Invocation,Dubbo从URL中解析扩展点名称的方式也存在一些差异,如果存在Invocation类型的参数,那么调用的是url.getMethodParameter(见adaptiveMethod3中的逻辑),如果不存在Invocation类型的参数调用的是url.getParameter

    org.apache.dubbo.rpc.Invocation代表一次具体的RPC调用,它持有调用过程中的变量,比如方法名,参数等

自激活SPI

接下来我们学习自激活SPI,什么是自激活SPI呢?顾名思义,这一类的SPI扩展点是不需要显式的传入扩展点名称来获取的,而是当满足一定条件时,会自动返回。看到这里,你可能还不明白,没有关系,我们来看一个例子。

简单使用

@SPI
public interface ActivateSpi {
    void activateMethod();
}

public class FirstActivateSpiImpl implements ActivateSpi {
    @Override
    public void activateMethod() {
        System.out.println("activate first");
    }
}

// ❤️ 注意这个注解
@Activate("second")
public class SecondActivateSpiImpl implements ActivateSpi {
    @Override
    public void activateMethod() {
        System.out.println("activate second");
    }
}

public static void main(String[] args) {        
    URL url = new URL("","",0);
    url =  url.addParameter("second","1");
  	// 明确表明要加载的扩展点是first
    String[] extensionNames  = new String[]{"first"};
    final List<ActivateSpi> activateExtension = ExtensionLoader.getExtensionLoader(ActivateSpi.class).getActivateExtension(url, extensionNames);
    activateExtension.forEach(ActivateSpi::activateMethod);
}

// 程序输出:
// activate second
// activate first

不忘忘记要添加配置文件哈~

first=com.easy4coding.dubbo.spi.service.activate.FirstActivateSpiImpl
second=com.easy4coding.dubbo.spi.service.activate.SecondActivateSpiImpl

在上面的例子中我们可以看到,当我们调用getActivateExtension时,显示传入的扩展点名称是first,但此方法返回的扩展点不仅仅有firstsecond也被加载了,这说明second这个扩展点自己激活了自己,这就是自激活SPI,接下来我们来分析一下其实现原理

原理分析

核心的API如下:

// key:用于从url中获取到具体的扩展点名称
public List<T> getActivateExtension(URL url, String key, String group) {
  			// 通过key,从url中获取对应的value,并将其作为扩展点名称
        String value = url.getParameter(key);
        return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);  
}
// values:代表要显式获取到的扩展点名称,具体源码我们在后文中分析
public List<T> getActivateExtension(URL url, String[] values, String group) {...}

我们基于以上API进行分析其原理

Activate注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    
    // 匹配通过API调用时方法参数中传入的group
    String[] group() default {};
    
    // 用于跟Url中的参数进行匹配,只有匹配成功扩展点才会激活
    String[] value() default {};

    // 从2.7版本已经过时,用于排序,指定顺序在某一扩展点前
    @Deprecated
    String[] before() default {};

    // 从2.7版本已经过时,用于排序,指定顺序在某一扩展点后
    @Deprecated
    String[] after() default {};
		
  	// 用于排序
    int order() default 0;
}

Activate注解中的group及value属性主要用于跟之前提到的API中传入的参数进行匹配,另外几个属性主要用于对加载的扩展点进行排序。

源码分析

核心代码位于:org.apache.dubbo.common.extension.ExtensionLoader#getActivateExtension(URL,String[],String)

image-20230104174109615

整个代码逻辑并不难,核心逻辑可以分为两部分

  1. 判断传入的扩展点名称中是否包含-default,如果包含,代表不需要自激活的扩展点,只加载名称在values数组中的扩展点实现。

    -代表指定要排除某一个扩展点,例如传入的valuse集合为:[“name1”,“name2”,“-name3”],则代表要使用"name1","name2"及自激活的SPI扩展点实现,同时排除名称为"name3"的扩展点实现

    代码如下:

    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
      			// 前文已经分析过,调用这个方法会去读取SPI配置文件并加载所有的类缓存到不同的集合中
            getExtensionClasses();
      			// cachedActivates中缓存的是被Activate注解修饰的扩展点实现
            for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
              	// name是扩展点名称
                String name = entry.getKey();
                // value是Activate注解
                Object activate = entry.getValue();
                String[] activateGroup, activateValue;
              	// 获取注解中的值,用于后续匹配
                if (activate instanceof Activate) {
                    activateGroup = ((Activate) activate).group();
                    activateValue = ((Activate) activate).value();
                // 兼容alibaba Dubbo
                } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                } else {
                    continue;
                }
              	// 👀,这里是重点哦~~~~!
                if (isMatchGroup(group, activateGroup)
                        && !names.contains(name)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)
                        && isActive(activateValue, url)) {
                    activateExtensions.add(getExtension(name));
                }
            }
      			// 我们前文已经分析过Activate注解,知道他有排序能力
            // 这里就是根据注解中的before、after及order进行排序
            activateExtensions.sort(ActivateComparator.COMPARATOR);
        }
    

    上面代码的核心在于,如何判断一个SPI实现是不是自激活的,对应判断条件如下:

    1. isMatchGroup(group, activateGroup),分组是否匹配。在group参数不为空的情况下,要求Activate注解中的group的值必须跟方法参数group的值匹配。

    2. !names.contains(name),不能是显示获取的扩展点,主要是为了排序,显示加载的扩展点会在后面进行统一加载

    3. !names.contains(REMOVE_VALUE_PREFIX + name),不能是显示排除的扩展点

    4. isActive(activateValue, url),必须跟URL中的key是匹配的,代码如下,注释中已经做了详细介绍,笔者不再过多介绍

      image-20230104174258489

  2. 加载指定名称的扩展点实现

    	  List<T> loadedExtensions = new ArrayList<>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(REMOVE_VALUE_PREFIX)
                    && !names.contains(REMOVE_VALUE_PREFIX + name)) {
                if (DEFAULT_KEY.equals(name)) {
                    if (!loadedExtensions.isEmpty()) {
                        // default代表了自激活扩展点,这里主要是为了排序
                        activateExtensions.addAll(0, loadedExtensions);
                        loadedExtensions.clear();
                    }
                } else {
                    loadedExtensions.add(getExtension(name));
                }
            }
        }
        if (!loadedExtensions.isEmpty()) {
          	// 最终都放入activateExtensions集合中统一返回
            activateExtensions.addAll(loadedExtensions);
        }
    

不知道大家有没有注意到,在分析上面两步时,文中都提到了一个词:排序。主要是因为这个方法最终返回的扩展点集合包含了两部分内容

  1. 自激活的扩展点
  2. 指定名称,显示申明要使用的扩展点(通过values参数申明)

对于自激活的扩展点,由于它们都被Activate注解修饰,因此可以直接依赖Activate注解对其进行排序,但是另外一部分显示要申明的扩展点要怎么办呢?Dubbo的做法是

  • 默认自激活扩展点的顺序高于显式申明要使用的扩展点,所以最后一行代码调用了activateExtensions.addAll(loadedExtensions)
  • 可以在values参数中通过传入default值的方式来指定自激活扩展点的顺序。举个例子,如果传入的values值为:[“name1”,“name2”,“default”,“name3”],那么"name1",“name2"对应的扩展点实现的顺序高于自激活扩展点的顺序,自激活扩展点的顺序高于"name3"对应的扩展点实现。如果传入的如果传入的values值为[“name1”,“name2”,“name3”],不包含"default"的情况下,自激活扩展点的顺序高于"name1”,“name2”,"name3"对应的扩展点实现

到现在为止我们已经基本已经掌握了Dubbo中的SPI的核心内容,接下来我们要学习更高阶的知识了☞Dubbo中的IOC及AOP

通过前文我们应该知道,IOC及AOP发生在扩展点实例化的过程中,整个流程图如下:

image-20230104174750325

核心代码如下:

private T createExtension(String name, boolean wrap) {
  
  // 1️⃣.加载扩展点实现类,得到class对象
  Class<?> clazz = getExtensionClasses().get(name);
  
  // 2️⃣.这里通过反射调用空参构造函数,完成实例化
  T instance = (T) EXTENSION_INSTANCES.get(clazz);
  if (instance == null) {  
    EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
    instance = (T) EXTENSION_INSTANCES.get(clazz);
  }

  // 3️⃣.IOC
  injectExtension(instance);

  // 4️⃣.AOP
  if (wrap) {
    // .....
  }
  
  // 5️⃣.扩展点生命周期,进一步完成初始化
  initExtension(instance);
  return instance;
}

接下来我们来分析整个IOC的过程,代码并不难哈~😎

IOC实现原理

核心代码如下:

image-20230104174825660

可以看到代码真的非常简单,大概逻辑分为这么几步

  1. 判断setter方法上是否有DisableInject注解,这个注解代表不需要进行注入
  2. objectFactory中获取到一个指定名称的对象并反射调用setter方法进行注入

所以,我们要理解IOC首先要搞懂objectFactory是什么

objectFactory介绍

image-20230104174855866

我们可以看到,objectFactory是在创建ExtensionLoader的过程中被初始化的,同时它是一个自适应SPI,对应的接口类型为:ExtensionFactory。这种情况,我们首先去查找它的SPI配置文件,如下:image-20230104174916424

在其配置文件中存在三个SPI实现类

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

我们分别查看对应的三个SPI实现会发在,在AdaptiveExtensionFactory这个实现类有一个@Adaptive注解,代表调用API获取自适应SPI时,真正返回的是这个类,这个类做的更多的是适配的工作,真正干活的还是SpringExtensionFactorySpiExtensionFactorySpringExtensionFactory这个类的作用是从Spring容器中获取到指定的Bean,而SpiExtensionFactory这个类的作用是依赖Dubbo的SPI机制获取到指定的对象,它们都是服务于Dubbo的IOC机制。

因为代码比较简单,这里我们就看一下AdaptiveExtensionFactory的代码:

  1. AdaptiveExtensionFactory

image-20230104175008863

注意哦:dubbo中SPI实现类的优先级会高于spring容器中bean的优先级

现在我们已经知道了objectFactory的作用,那么IOC就已经没有任何秘密。简单总结一下:

总结

  1. IOC发生的时机:在实例化SPI实现时进行IOC
  2. IOC的条件
    • 存在setter方法
    • setter方法上没有DisableInject注解
    • 在进行IOC时,被注入的对象是通过objectFactory获取,objectFactory是通过自适应SPI进行初始化的,实际上它会优先尝试使用Dubbo的SPI获取一个对象,如果获取到了,只会返回。如果没有获取到,那么会再次从Spring容器中获取一个对应的bean。

AOP实现原理

AOP的实现就更加简单了,实现AOP的关键是依赖通过loadExtensionClasses方法加载的wrapper类,还记得loadExtensionClasses干了啥吗?回顾一下这张图

image-20230104175045034

AOP的核心代码如下:

image-20230104175117204Dubbo的AOP说白了就是依靠wrapper类来对真实的扩展点进行一次包装,wrapper类持有一个真实的扩展点实现引用!

总结

这篇文章我们分析了Dubbo中的各种SPI实现,以及Dubbo基于SPI扩展出来功能:IOC、AOP。我相信只要大家认真看完这篇文章,积极动手实践,搞懂Dubbo的SPI是没有任何问题。要学好Dubbo,掌握SPI是第一步,整个框架中用到SPI的地方数不胜数。

谢谢你看完这篇文章,也请你谢谢认真学习的自己!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/2474.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Chapter7.1:频域分析法理论基础

该系列博客主要讲述Matlab软件在自动控制方面的应用&#xff0c;如无自动控制理论基础&#xff0c;请先学习自动控制系列博文&#xff0c;该系列博客不再详细讲解自动控制理论知识。 自动控制理论基础相关链接&#xff1a;https://blog.csdn.net/qq_39032096/category_10287468…

JS 处理后台返回的数据

前言 常规情况下&#xff0c;我们可以把后台返回给我们的数据直接渲染在前台页面上&#xff0c;但不排除一些特殊的情况需要我们对源数据进行处理&#xff0c;例如 element 上传组件&#xff0c;在编辑页面中的回显指定参数为 name 和 url&#xff0c;但是后台返回的如果不是这…

【MySQL】1 MySQL的下载、安装与配置|提供安装包

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 目前,已开了以下专栏,欢迎关注与指导 1️⃣Java基础知识系统学习(持续更文中…) 2️⃣UML(已更完) 3️⃣MySQL(持续更文中…) MYSQL的下载、安装与配置1.下载MySQL5.71.1安装包的获…

C++入门教程||C++ 数字||C++ 数组

C 数字通常&#xff0c;当我们需要用到数字时&#xff0c;我们会使用原始的数据类型&#xff0c;如 int、short、long、float 和 double 等等。这些用于数字的数据类型&#xff0c;其可能的值和数值范围&#xff0c;我们已经在 C 数据类型一章中讨论过。C 定义数字我们已经在之…

NSSCTF-[NCTF 2021]狗狗的秘密

题目链接&#xff1a;NSSCTF 根据题目标签&#xff0c;这道题考了SMC&#xff0c;xtea和base47。 无壳&#xff0c;载入IDA&#xff0c;看main函数可知输入长度是42。然后创造了新线程&#xff0c;进入线程开始地址StartAddress。 是一个赋值语句就没别的了&#xff0c;很迷。…

【5G RRC】NR测量事件介绍

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

【STL四】序列容器——vector容器

【STL容器】序列容器——vector容器一、简介二、头文件三、模板类四、成员函数1、迭代器2、元素访问3、容量4、修改操作五、demo1、容量reserve、capacity、shrink_to_fit2、修改操作pop_back()、push_back3、修改操作insert()4、修改操作emplace()5、修改操作erase()、swap()、…

202209-3 CCF 防疫大数据 满分题解(超详细讲解 + 注释代码) + 解题思路(STL模拟)

问题描述 解题思路 首先题意是给出n天的漫游信息以及n天的风险地区名单 求n天的风险人群 根据题意肯定要将漫游信息存储下来&#xff0c;用结构体数组比较合适 在判断该用户是否是风险人群时&#xff0c;需要判断[d1, d]区间内地点r是否是风险地区&#xff0c;所以需要把地点…

JAVA开发(自研项目的开发与推广)

https://live.csdn.net/v/284629 案例背景&#xff1a; 作为JAVA开发人员&#xff0c;我们可以开发无数多的web项目&#xff0c;电商系统&#xff0c;小程序&#xff0c;H5商城。有时候作为技术研发负责人&#xff0c;项目做成了有时候也需要对内进行内测&#xff0c;对外进行…

PHP+vue+elementUI高校食堂校园餐厅点餐系统

运行环境:phpstudy/wamp/xammp等 开发语言&#xff1a;php 后端框架&#xff1a;Thinkphp5 前端框架&#xff1a;vue.js 服务器&#xff1a;apache 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat/phpmyadmin 开发软件&#xff1a;hbuilderx/vscode/Dreamweaver/PhpSt…

项目管理工具哪个好?最新排名

项目管理工具当下已经成为项目团队的重要榜首&#xff0c;一款合适好用的项目管理工具可以帮助处理很多机械化工作&#xff0c;将管理者更多精力投入到更有价值的工作中&#xff0c;还可以帮助团队组织和计划项目&#xff0c;跟踪进度&#xff0c;处理预算和协作。该如何挑选帮…

什么是Vue

✅作者简介&#xff1a;CSDN一位小博主&#xff0c;正在学习前端&#xff0c;欢迎大家一起来交流学习&#x1f3c6; &#x1f4c3;个人主页&#xff1a;白月光777的CSDN博客 &#x1f525;系列专栏&#xff1a;Vue从入门到进阶 &#x1f4ac;个人格言&#xff1a;但行好事&…

【面试题】大厂面试官:你做过什么有亮点的项目吗?

大厂面试题分享 面试题库前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★地址&#xff1a;前端面试题库前言大厂面试中除了问常见的算法网络基础&#xff0c;和一些八股文手写体之外&#xff0c;经常出现的一个问题就是&#xff0c;你做过什么项目…

React--》状态管理工具—Mobx的讲解与使用

目录 Mobx的讲解与使用 Mobx环境配置 Mobx的基本使用 Mobx计算属性的使用 Mobx监听属性的使用 Mobx处理异步的使用 Mobx的模块化 Mobx的讲解与使用 Mobx是一个可以和React良好配合的集中状态管理工具&#xff0c;mobx和react的关系相当于vuex和vue之间的关系&#xff0…

膜拜!阿里自爆十万字Java面试手抄本,脉脉一周狂转50w/次

最近&#xff0c;一篇题为《阿里十万字Java面试手抄本》的文章在社交媒体平台上引起了广泛关注。这篇文章由一位阿里工程师整理了阿里Java面试的经验&#xff0c;并分享给了大家。这篇文章一经发布&#xff0c;就在短时间内获得了数十万的转发量&#xff0c;让许多Java程序员受…

Linux 网络编程学习笔记——四、HTTP 通信

目录 一、HTTP 代理服务器的工作原理 在 HTTP 通信链上&#xff0c;客户端和目标服务器之间通常存在某些中转代 理服务器&#xff0c;它们提供对目标资源的中转访问。一个 HTTP 请求可能被多个代理服务器转发&#xff0c;后面的服务器称为前面服务器的上游服务器。代理服务器…

基于OpenCV的人脸识别

目录 &#x1f969; 前言 &#x1f356; 环境使用 &#x1f356; 模块使用 &#x1f356; 模块介绍 &#x1f356; 模块安装问题: &#x1f969; OpenCV 简介 &#x1f356; 安装 OpenCV 模块 &#x1f969; OpenCV 基本使用 &#x1f356; 读取图片 &#x1f357; 【…

技术人的管理学-业务管理

主要内容前言制定计划遇到的问题&#xff1f;过程监控遇到的问题&#xff1f;复盘改进遇到的问题&#xff1f;通过PDCA循环解决业务管理问题总结前言 没有人天生就会管理&#xff0c;优秀的管理者都是在知行合一的过程中成长起来的&#xff0c;他们既需要系统的管理知识&#…

Java基础知识之Map的使用

一、Map介绍 Map是用于保存具有映射关系的数据集合&#xff0c;它具有双列存储的特点&#xff0c;即一次必须添加两个元素&#xff0c;即一组键值对><Key,Value>&#xff0c;其中Key的值不可重复&#xff08;当Key的值重复的时候&#xff0c;后面插入的对象会将之前插…

单片机中按键检测函数详细分析经典

​ 目录 一、如何进行按键检测 1.从裸机的角度分析 2.从OS的角度分析 二、最简单的按键检测程序 三、为什么要了解FIFO 四、什么是FIFO 五、按键FIFO的优点 六、按键 FIFO 的实现 1.定义结构体 2.将键值写入FIFO 3.从FIFO读出键值 4.按键检测程序 5.按键扫描 7.…