小程序
传感搜
传感圈

Dubbo源码(一)SPI vs Spring

2023-05-21 23:34:18
关注

ExtensionLoader

1、私有构造

ExtensionLoader的构造方法传入一个Class,代表当前扩展点对应的SPI接口。

每个ExtensionLoader实例管理自己Class的扩展点,包括加载、获取等等。

type:当前扩展点对应spi接口class;

objectFactory:扩展点工厂AdaptiveExtensionFactory,主要用于setter注入,后面再看。

2、单例

ExtensionLoader提供静态方法,构造ExtensionLoader实例。

单例往往要针对一个范围(scope)来说,比如Spring中所说的单例,往往是在一个BeanFactory中,而一个应用可以运行多个BeanFactory。又比如Class对象是单例,往往隐含的scope是同一ClassLoader。

ExtensionLoader在一个扩展点接口Class下只有一个实例,而每个扩展点实现实例在全局只有一个。

3、成员变量

ExtensionLoader的成员变量可以分为几类

普通扩展点相关:

active扩展点相关:

adaptive扩展点相关:

wrapper扩展点相关:

4、加载扩展点Class

ExtensionLoader#getExtensionClasses:

当需要加载某个扩展点实现实例前,总会优先加载该扩展点所有实现Class,并缓存到cachedClasses中。

ExtensionLoader#loadExtensionClasses:加载所有扩展点实现类

ExtensionLoader#cacheDefaultExtensionName:加载SPI注解中的value属性,作为默认扩展点名称,默认扩展点只能存在一个。

ExtensionLoader会扫描classpath三个路径下的扩展点配置文件:

  • META-INF/dubbo/internal:dubbo框架自己用的
  • META-INF/dubbo/:用户扩展用的
  • META-INF/services/:官方也没建议这样使用

ExtensionLoader#loadDirectory:

1)类加载器:优先线程类加载器,其次ExtensionLoader自己的类加载器;

2)扫描扩展点配置文件;

3)加载扩展类;

ExtensionLoader#loadResource:加载文件中每一行,key是扩展名,value是扩展实现类名。

ExtensionLoader#loadClass:最终将每个扩展实现类Class按照不同的方式,缓存到ExtensionLoader实例中。

普通扩展点

案例

对于一个扩展点MyExt:

@SPI
public interface MyExt {
   String echo(URL url, String s);
}

MyExtImplA实现MyExt:

public class MyExtImplA implements MyExt {
   @Override
   public String echo(URL url, String s) {
       return "ext1";
   }
}

可以配置多个扩展点实现META-INF/dubbo/x.y.z.MyExt:

A=x.y.z.impl.MyExtImplA
B=x.y.z.impl.MyExtImplB

使用ExtensionLoader#getExtention获取对应扩展点:

@Test
void testExtension() {
   ExtensionLoader

这种用法,对于用户来说,和beanFactory极为类似,当然实现并不同。

MyExt a = beanFactory.getBean("A", MyExt.class);

原理

ExtensionLoader#getExtention固然有单例缓存(cachedInstances),这个直接跳过。

ExtensionLoader#createExtension:创建扩展点实现

0)getExtensionClasses:确保所有扩展点class被加载

1)通过无参构造,实例化扩展点instance

2)injectExtention:对扩展点instance执行setter注入,暂时忽略

3)包装类相关,暂时忽略

4)执行instance的初始化方法

initExtension:初始化

ExtensionLoader和Spring的创建bean流程相比,确实很像,比如:

1)Spring可以通过各种方式选择bean的一个构造方法创建一个bean(AbstractAutowireCapableBeanFactory#createBeanInstance),而ExtensionLoader只能通过无参构造创建扩展点;

2)Spring可以通过多种方式进行依赖注入(AbstractAutowireCapableBeanFactory#populateBean),比如Aware接口/setter/注解等,而ExtensionLoader只能支持setter注入;

3)Spring可以通过多种方式进行初始化(AbstractAutowireCapableBeanFactory#initializeBean),比如PostConstruct注解/InitializingBean/initMethod等,而ExtensionLoader只支持InitializingBean(LifeCycle)这种方式;

包装扩展点

案例

上面在ExtensionLoader#createExtension的第三步,可能会走包装扩展点逻辑。

假设有个扩展点MyExt2:

@SPI
public interface MyExt2 {
   String echo(URL url, String s);
}

有普通扩展点实现MyExt2ImplA:

public class MyExt2ImplA implements MyExt2 {
   @Override
   public String echo(URL url, String s) {
       return "A";
   }
}

除此以外,还有两个实现MyExt2的扩展点的MyExtWrapperA和MyExtWrapperB, 特点在于他有MyExt2的单参数构造方法

public class MyExtWrapperA implements MyExt2 {

   private final MyExt2 myExt2;

   public MyExtWrapperA(MyExt2 myExt2) {
       this.myExt2 = myExt2;
   }

   @Override
   public String echo(URL url, String s) {
       return "wrapA>>>" + myExt2.echo(url, s);
   }
}
public class MyExtWrapperB implements MyExt2 {

   private final MyExt2 myExt2;

   public MyExtWrapperB(MyExt2 myExt2) {
       this.myExt2 = myExt2;
   }

   @Override
   public String echo(URL url, String s) {
       return "wrapB>>>" + myExt2.echo(url, s);
   }
}

然后编写配置文件META-INF/x.y.z.myext2.MyExt2:

A=x.y.z.myext2.impl.MyExt2ImplA
wrapperA=x.y.z.myext2.impl.MyExtWrapperA
wrapperB=x.y.z.myext2.impl.MyExtWrapperB

测试验证,echo方法输出wrapB>>>wrapA>>>A。

@Test
void testWrapper() {
   ExtensionLoader

但是包装扩展点不能通过getExtension显示获取 ,比如:

// 包装类无法通过name直接获取
@Test
void testWrapper_IllegalStateException() {
   ExtensionLoader

原理

包装类之所以不暴露给用户直接获取,是因为包装类提供类似aop的用途,对于用户来说是透明的。

类加载阶段

在类加载阶段,isWrapperClass判断一个扩展类是否是包装类,如果是的话放入cachedWrapperClasses缓存。

对于包装类,不会放入普通扩展点的缓存map,所以无法通过getExtension显示获取。

判断是否是包装类,取决于扩展点实现clazz是否有对应扩展点type的单参构造方法。

实例化阶段

包装类实例化,是通过ExtensionLoader.getExtension("A")获取普通扩展点触发的,而返回的会是一个包装类。

如果一个扩展点存在包装类,客户端通过getExtension永远无法获取到原始扩展点实现

包装类是硬编码实现的:

1)本质上包装的顺序是无序的,取决于扩展点配置文件的扫描顺序。(SpringAOP可以设置顺序)

2)包装类即使只关注扩展点的一个方法,也必须要实现扩展点的所有方法,扩展点新增方法如果没有默认实现,需要修改所有包装类。(SpringAOP如果用户只关心其中一个方法,也可以实现,因为是动态代理)

3)性能较好。(无反射)

自适应扩展点

对于一个扩展点type,最多只有一个自适应扩展点实现。

可以通过用户硬编码实现,也可以通过dubbo自动生成,优先取用户硬编码实现的自适应扩展点。

硬编码(Adaptive注解Class)

案例

假如有个水果扩展类,howMuch来统计交易上下文中该水果能卖多少钱。

@SPI
public interface Fruit {
   int howMuch(String context);
}

有苹果香蕉等实现,负责计算自己能卖多少钱。

public class Apple implements Fruit {
   @Override
   public int howMuch(String context) {
       return context.contains("apple") ? 1 : 0;
   }
}
public class Banana implements Fruit {
   @Override
   public int howMuch(String context) {
       return context.contains("banana") ? 2 : 0;
   }
}

这里引入一个AdaptiveFruit,在类上加了Adaptive注解,目的是统计上下文中所有水果能卖多少钱。

getSupportedExtensionInstances这个方法能加载所有扩展点,并依靠Prioritized接口实现排序,这个原理忽略,和Spring的Ordered差不多。

@Adaptive
public class AdaptiveFruit implements Fruit {
   private final Set

测试方法如下,用户购买苹果和香蕉,共花费3元。

核心api是ExtensionLoader#getAdaptiveExtension获取自适应扩展点实现。

@Test
void testAdaptiveFruit() {
   ExtensionLoader

原理

在类加载阶段,被Adaptive注解修饰的扩展点Class会被缓存到cachedAdaptiveClass。

注意,Adaptive注解类也不会作为普通扩展点暴露给用户,即不能通过ExtensionLoader.getExtension通过扩展名直接获取。

ExtensionLoader#getAdaptiveExtension获取自适应扩展点。

实例化阶段,无参构造反射创建Adaptive扩展点,并执行setter注入。

dubbo优先选取用户实现的Adaptive扩展点实现,否则会动态生成Adaptive扩展点。

动态生成(Adaptive注解Method)

案例

假设现在有个秒杀水果扩展点SecKillFruit。

相较于刚才的Fruit扩展点, 区别在于入参改为了URL,且方法加了Adaptive注解

@SPI
public interface SecKillFruit {
   @Adaptive
   int howMuch(URL context);
}

苹果秒杀0元,香蕉秒杀1元。

public class SecKillApple implements SecKillFruit {
   @Override
   public int howMuch(URL context) {
       return 0;
   }
}
public class SecKillBanana implements SecKillFruit {
   @Override
   public int howMuch(URL context) {
       return 1;
   }
}

扩展点配置文件META-INF/x.y.z.myext4.SecKillFruit:

apple=x.y.z.myext4.impl.SecKillApple
banana=x.y.z.myext4.impl.SecKillBanana

假设场景,每次只能秒杀一种水果,需要根据上下文不同,决定秒杀的是哪种水果,计算不同的价钱。

有下面的测试案例,关键点在于URL里增加了sec.kill.fruit=扩展点名,零编码实现根据URL走不同策略。

sec.kill.fruit是SecKillFruit驼峰解析为小写后用点分割得到。

@Test
void testAdaptiveFruit2() {
   ExtensionLoader

也可以通过指定Adaptive注解的value,让获取扩展点名字的逻辑更加清晰。

比如取URL中的fruitType作为获取扩展名的方式。

@SPI
public interface SecKillFruit {
   @Adaptive("fruitType")
   int howMuch(URL context);
}

原理

由于Dubbo内部就是用URL做全局上下文来用,你可以理解为字符串无所不能。

所以为了减少重复代码,很多策略都通过动态生成自适应扩展来实现。

ExtensionLoader#createAdaptiveExtensionClass:如果没有用户Adaptive注解实现扩展点,走这里动态生成。

关键点在于AdaptiveClassCodeGenerator#generate如何生成java代码。

扩展点接口必须有Adaptive注解方法,否则getAdaptiveExtension会异常。

关键在于generateMethodContent如何实现adaptive方法逻辑。

对于没有Adaptive注解的方法,直接抛出异常。

对于Adaptive注解的方法,分为四步:

1)获取URL:优先从参数列表里直接找URL,降级从一个有URL的getter方法的Class里获取URL,否则异常;

2)决定扩展名:优先从Adaptive注解value属性获取,否则取扩展点类名去驼峰加点;

3)获取扩展点:调用ExtensionLoader.getExtension;

4)委派给目标扩展实现:调用目标扩展的目标方法,传入原始参数列表;

比如针对SecKillFruit,最终生成的代码如下。

对于Dubbo来说,虽然扩展点不同,但是都用URL上下文,就可以少写重复代码。

public class SecKillFruit$Adaptive implements x.y.z.myext4.SecKillFruit {
   // Adaptive注解方法,通过ContextHolder.getUrl获取URL
   public int howMuch2(x.y.z.myext4.ContextHolder arg0) {
       if (arg0 == null)
           throw new IllegalArgumentException("...");
       if (arg0.getUrl() == null)
           throw new IllegalArgumentException("...");
       org.apache.dubbo.common.URL url = arg0.getUrl();
       String extName = url.getParameter("fruitType");
       if (extName == null)
           throw new IllegalStateException("...");
       x.y.z.myext4.SecKillFruit extension = (x.y.z.myext4.SecKillFruit)
                                              ExtensionLoader
                                              .getExtensionLoader(x.y.z.myext4.SecKillFruit.class)
                                              .getExtension(extName);
       return extension.howMuch2(arg0);
   }
   // Adaptive注解方法,直接从参数列表中获取URL
   public int howMuch(org.apache.dubbo.common.URL arg0) {
       if (arg0 == null) throw new IllegalArgumentException("url == null");
       org.apache.dubbo.common.URL url = arg0;
       String extName = url.getParameter("fruitType");
       if (extName == null)
           throw new IllegalStateException("...");
       x.y.z.myext4.SecKillFruit extension = (x.y.z.myext4.SecKillFruit)
                                              ExtensionLoader
                                              .getExtensionLoader(x.y.z.myext4.SecKillFruit.class)
                                              .getExtension(extName);
       return extension.howMuch(arg0);
   }

   // 没有Adaptive注解的方法
   public int howMuch() {
       throw new UnsupportedOperationException("...");
   }
}

Spring+jdk动态代理实现

上面原理分析不太好理解,这个事情也可以用Spring+jdk动态代理来实现。

其实这个需求和feign的FeignClient、mybatis的Mapper都比较像,写完接口就相当于写完实现。

针对同一个扩展点type设计一个 AdaptiveFactoryBean

public class AdaptiveFactoryBean implements FactoryBean




您觉得本篇内容如何
评分

评论

您需要登录才可以回复|注册

提交评论

提取码
复制提取码
点击跳转至百度网盘