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