电子说
ExtensionLoader的构造方法传入一个Class,代表当前扩展点对应的SPI接口。
每个ExtensionLoader实例管理自己Class的扩展点,包括加载、获取等等。
type:当前扩展点对应spi接口class;
objectFactory:扩展点工厂AdaptiveExtensionFactory,主要用于setter注入,后面再看。
ExtensionLoader提供静态方法,构造ExtensionLoader实例。
单例往往要针对一个范围(scope)来说,比如Spring中所说的单例,往往是在一个BeanFactory中,而一个应用可以运行多个BeanFactory。又比如Class对象是单例,往往隐含的scope是同一ClassLoader。
ExtensionLoader在一个扩展点接口Class下只有一个实例,而每个扩展点实现实例在全局只有一个。
ExtensionLoader的成员变量可以分为几类
普通扩展点相关:
active扩展点相关:
adaptive扩展点相关:
wrapper扩展点相关:
ExtensionLoader#getExtensionClasses:
当需要加载某个扩展点实现实例前,总会优先加载该扩展点所有实现Class,并缓存到cachedClasses中。
ExtensionLoader#loadExtensionClasses:加载所有扩展点实现类
ExtensionLoader#cacheDefaultExtensionName:加载SPI注解中的value属性,作为默认扩展点名称,默认扩展点只能存在一个。
ExtensionLoader会扫描classpath三个路径下的扩展点配置文件:
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自动生成,优先取用户硬编码实现的自适应扩展点。
假如有个水果扩展类,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扩展点。
假设现在有个秒杀水果扩展点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动态代理来实现。
其实这个需求和feign的FeignClient、mybatis的Mapper都比较像,写完接口就相当于写完实现。
针对同一个扩展点type设计一个 AdaptiveFactoryBean 。
public class AdaptiveFactoryBean implements FactoryBean
核心逻辑在InvocationHandler#invoke代理逻辑中,和AdaptiveClassCodeGenerator#generateMethodContent一样。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!cachedMethod2Adaptive.containsKey(method)) {
throw new UnsupportedOperationException();
}
// 1. 获取URL
int urlIdx = cachedMethod2URLIndex.get(method);
URL url = (URL) args[urlIdx];
// 2. 从url里获取扩展点名
Adaptive adaptive = cachedMethod2Adaptive.get(method);
String extName = null;
for (String key : adaptive.value()) {
extName = url.getParameter(key);
if (extName != null) {
break;
}
}
if (extName == null) {
extName = defaultExtName;
}
if (extName == null) {
throw new IllegalStateException();
}
// 3. 获取扩展点
Object extension =
ExtensionLoader.getExtensionLoader(type).getExtension(extName);
// 4. 委派给扩展点
return method.invoke(extension, args);
}
为了注入所有包含Adaptive注解方法的扩展点AdaptiveFactoryBean,提供一个批量注册BeanDefinition的 AdaptiveBeanPostProcessor ,实现比较粗糙,主要为了说明问题。
public class AdaptiveBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private final String packageToScan;
private Environment environment;
public AdaptiveBeanPostProcessor(String packageToScan) {
this.packageToScan = packageToScan;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false, this.environment) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
// 有Adaptive注解方法
return beanDefinition.getMetadata()
.hasAnnotatedMethods("org.apache.dubbo.common.extension.Adaptive");
}
};
scanner.addIncludeFilter(new AnnotationTypeFilter(SPI.class));
Set
测试验证:
@Configuration
public class AdaptiveFactoryBeanTest {
@Bean
public AdaptiveBeanPostProcessor adaptiveBeanPostProcessor() {
return new AdaptiveBeanPostProcessor("x.y.z.myext4");
}
@Test
void test() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(AdaptiveFactoryBeanTest.class);
applicationContext.refresh();
SecKillFruit secKillFruit = applicationContext.getBean("x.y.z.myext4.SecKillFruit$Adaptive_Spring", SecKillFruit.class);
URL url = new URL("myProtocol", "1.2.3.4", 1010, "path");
// 0元秒杀苹果
url = url.addParameters("fruitType", "apple");
int money = secKillFruit.howMuch(url);
assertEquals(0, money);
// 1元秒杀香蕉
url = url.addParameters("fruitType", "banana");
money = secKillFruit.howMuch(url);
assertEquals(1, money);
// 无URL方法异常
assertThrows(UnsupportedOperationException.class, secKillFruit::howMuch);
}
}
是不是用spring+动态代理来说明,更加容易理解了。
无论是对于普通扩展点/包装扩展点/自适应扩展点,所有的扩展点实例都会经过依赖注入。
InjectExt是个扩展点,有实现InjectExtImplA,InjectExtImplA有一个Inner的setter方法。
public class InjectExtImplA implements InjectExt {
private Inner inner;
public void setInner(Inner inner) {
this.inner = inner;
}
@Override
public Inner getInner() {
return inner;
}
}
Inner是个扩展点,且能生成自适应扩展实现。
@SPI
public interface Inner {
@Adaptive
String echo(URL url);
}
Inner有InnerA实现。
public class InnerA implements Inner {
@Override
public String echo(URL url) {
return "A";
}
}
测试方法,InjectExtImplA 被自动注入了Inner的自适应实现 。
@Test
void testInject() {
ExtensionLoader
ExtensionLoader#injectExtension依赖注入,循环每个setter方法,找到入参Class和属性名。
通过ExtensionFactory搜索依赖,整个注入过程的异常都被捕获。
ExtensionFactory也是SPI接口。
这里走硬编码实现的 AdaptiveExtensionFactory ,循环每个ExtensionFactory扩展点,通过type和name找扩展点实现。
ExtensionFactory扩展点有两个实现。
原生的 SpiExtensionFactory , 没有利用setter的属性name ,直接获取type对应的自适应扩展点。
这也是为什么案例中,被注入的扩展点用了Adaptive。
Spring相关的SpringExtensionFactory支持从多个ioc容器中,通过getBean(setter属性名,扩展点)获取bean。
ExtensionLoader#getExtension可以获取单个扩展点实现。
ExtensionLoader#getSupportedExtensionInstances可以获取所有扩展点实现。
现在 需要根据条件,获取一类扩展点实现,这就是所谓的激活扩展点 。
以Spring为例,如何利用Qualifier做到这点。
假设现在有个用户接口,根据用户类型和用户等级有不同实现。
public interface User {
}
利用Qualifier注解,Category代表用户类型,Level代表用户等级。
@Qualifier
public @interface Category {
String value();
}
@Qualifier
public @interface Level {
int value();
}
针对User有四种实现,包括vip1级用户、vip2级用户、普通用户、普通2级用户。
@Component
@Category("vip")
@Level(1)
public static class VipUser implements User {
}
@Component
@Category("vip")
@Level(2)
public static class GoldenVipUser implements User {
}
@Component
@Category("normal")
public static class UserImpl implements User {
}
@Component
@Category("normal")
@Level(2)
public static class UserImpl2 implements User {
}
通过Qualifier,可以按照需求注入不同类型等级用户集合,做业务逻辑。
@Configuration
@ComponentScan
public class QualifierTest {
@Autowired
@Category("vip")
private List
和上面Spring的案例一样,替换成ExtensionLoader实现,看起来语义差不多。
用户等级作为分组,在URL参数上获取用户等级。
@Activate(group = {"vip"}, value = {"level:1"})
public class VipUser implements User {
}
@Activate(group = {"vip"}, value = {"level:2"}, order = 1000)
public class GoldenVipUser implements User {
}
@Activate(group = {"normal"}, order = 10)
public class UserImpl implements User {
}
@Activate(group = {"normal"}, value = {"level:2"}, order = 500)
public class UserImpl2 implements User {
}
测试方法如下,发现与Spring的Qualifier有相同也有不同。
比如通过group=vip和url不包含level去查询:
1)UserImpl和UserImpl2查不到,因为group不满足;
2)VipUser和GoldenVipUser查不到,因为url必须有level,且分别为1和2;
又比如通过group=null和level=2去查询:
1)UserImpl没有设置Activate注解value,代表对url没有约束,且查询条件group=null,代表匹配所有group,所以可以查到;
2)VipUser对url有约束,必须level=1,所以查不到;
3)GoldenVipUser和UserImpl2,都满足level=2,且查询条件group=null,代表匹配所有group,所以都能查到;
@Test
void testActivate() {
ExtensionLoader
类加载阶段,激活扩展点在普通扩展点分支逻辑中。
所以 激活扩展点只是筛选普通扩展点的方式 ,属于普通扩展点的子集。
ExtensionLoader#getActivateExtension获取激活扩展点的入参包含三部分:
1)查询URL;2)查询扩展点名称集合;3)查询分组
其中1和3用于Activate匹配,2用于直接从getExtension获取扩展点加在Activate匹配的扩展点之后。
重点看isMatchGroup和isActive两个方法。
isMatchGroup :如果查询条件不包含group,则匹配,如果查询条件包含group,注解中必须有group与其匹配。
isActive :匹配url
1)Activate没有value约束,匹配
2)url匹配成功条件:如果注解value配置为k:v模式,要求url参数kv完全匹配;如果注解value配置为k模式,只需要url包含kv参数即可。其中k还支持后缀匹配。
比如@Activate(value = {"level"})只需要url中有level=xxx即可,
而@Activate(value = {"level:2"})需要url中level=2。
本文分析了Dubbo2.7.6的SPI实现。
ExtensionLoader相较于java的spi能按需获取扩展点,还有很多高级特性,与Spring的ioc和aop非常相似。
看似ExtensionLoader的功能都能通过Spring实现,但是Dubbo不想依赖Spring,所以造了套轮子。
题外话:非常夸张的是,Dubbo一个RPC框架,竟然有27w行代码,而同样是RPC框架的sofa-rpc5.9.0只有14w行。
除了很多com.alibaba的老包兼容代码,轮子是真的多,早期版本连json库都是自己实现的,现在是换成fastjson了。
ExtensionLoader#getExtension(name),普通扩展点通过扩展名获取。
@SPI
public interface MyExt {
String echo(URL url, String s);
}
创建普通扩展点分为四个步骤
1)无参构造
2)依赖注入
3)包装
4)初始化
如果扩展点实现包含该扩展点的单参构造方法,被认为是包装扩展点。
public class WrapperExt implements Ext {
public WrapperExt(Ext ext) {
}
}
包装扩展点无法通过扩展名显示获取,而是在用户获取普通扩展点时,自动包装普通扩展点,返回给用户,整个过程是透明的。
ExtensionLoader#getAdaptiveExtension获取自适应扩展点。
每个扩展点最多只有一个自适应扩展点。
自适应扩展点分为两类:硬编码、动态生成。
硬编码自适应扩展点,在扩展点实现class上标注Adaptive注解,优先级高于动态生成自适应扩展点。
@Adaptive
public class AdaptiveFruit implements Fruit {
}
动态生成自适应扩展点。
出现的背景是,dubbo中有许多依赖URL上下文选择不同扩展点策略的场景,如果通过硬编码实现,会有很多重复代码。
动态生成自适应扩展点,针对@Adaptive注解方法且方法参数有URL的扩展点,采用javassist字节码技术,动态生成策略实现。
@SPI
public interface SecKillFruit {
@Adaptive("fruitType")
int howMuch(URL context);
}
激活扩展点属于普通扩展点的子集。
激活扩展点利用Activate注解,根据条件匹配一类扩展点实现 。
@Activate(group = {"vip"}, value = {"level:2"}, order = 1000)
public class GoldenVipUser implements User {
}
ExtensionLoader#getActivateExtension:通过group和URL查询一类扩展点实现。
@Test
void testActivate() {
ExtensionLoader
无论是普通/包装/自适应扩展点,在暴露给用户使用前,都会进行setter依赖注入。
依赖注入对象可来源于两部分:
1)SpiExtensionFactory根据type获取自适应扩展点
2)SpringExtensionFactory根据setter属性+type从ioc容器获取扩展点
全部0条评论
快来发表一下你的评论吧 !