extensionloader1、私有构造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、加载扩展点classextensionloader#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:
@spipublic 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.myextimplab=x.y.z.impl.myextimplb使用extensionloader#getextention获取对应扩展点:
@testvoid 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:
@spipublic 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.myext2implawrappera=x.y.z.myext2.impl.myextwrapperawrapperb=x.y.z.myext2.impl.myextwrapperb测试验证,echo方法输出wrapb>>>wrapa>>>a。
@testvoid testwrapper() { extensionloader但是包装扩展点不能通过getextension显示获取 ,比如:
// 包装类无法通过name直接获取@testvoid testwrapper_illegalstateexception() { extensionloader原理包装类之所以不暴露给用户直接获取,是因为包装类提供类似aop的用途,对于用户来说是透明的。
类加载阶段在类加载阶段,iswrapperclass判断一个扩展类是否是包装类,如果是的话放入cachedwrapperclasses缓存。
对于包装类,不会放入普通扩展点的缓存map,所以无法通过getextension显示获取。
判断是否是包装类,取决于扩展点实现clazz是否有对应扩展点type的单参构造方法。
实例化阶段包装类实例化,是通过extensionloader.getextension(a)获取普通扩展点触发的,而返回的会是一个包装类。
即 如果一个扩展点存在包装类,客户端通过getextension永远无法获取到原始扩展点实现 。
包装类是硬编码实现的:
1)本质上包装的顺序是无序的,取决于扩展点配置文件的扫描顺序。(springaop可以设置顺序)
2)包装类即使只关注扩展点的一个方法,也必须要实现扩展点的所有方法,扩展点新增方法如果没有默认实现,需要修改所有包装类。(springaop如果用户只关心其中一个方法,也可以实现,因为是动态代理)
3)性能较好。(无反射)
自适应扩展点对于一个扩展点type,最多只有一个自适应扩展点实现。
可以通过用户硬编码实现,也可以通过dubbo自动生成,优先取用户硬编码实现的自适应扩展点。
硬编码(adaptive注解class)案例假如有个水果扩展类,howmuch来统计交易上下文中该水果能卖多少钱。
@spipublic 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差不多。
@adaptivepublic class adaptivefruit implements fruit { private final set测试方法如下,用户购买苹果和香蕉,共花费3元。
核心api是extensionloader#getadaptiveextension获取自适应扩展点实现。
@testvoid testadaptivefruit() { extensionloader原理在类加载阶段,被adaptive注解修饰的扩展点class会被缓存到cachedadaptiveclass。
注意,adaptive注解类也不会作为普通扩展点暴露给用户,即不能通过extensionloader.getextension通过扩展名直接获取。
extensionloader#getadaptiveextension获取自适应扩展点。
实例化阶段,无参构造反射创建adaptive扩展点,并执行setter注入。
dubbo优先选取用户实现的adaptive扩展点实现,否则会动态生成adaptive扩展点。
动态生成(adaptive注解method)案例假设现在有个秒杀水果扩展点seckillfruit。
相较于刚才的fruit扩展点, 区别在于入参改为了url,且方法加了adaptive注解 。
@spipublic 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.seckillapplebanana=x.y.z.myext4.impl.seckillbanana假设场景,每次只能秒杀一种水果,需要根据上下文不同,决定秒杀的是哪种水果,计算不同的价钱。
有下面的测试案例,关键点在于url里增加了sec.kill.fruit=扩展点名,零编码实现根据url走不同策略。
sec.kill.fruit是seckillfruit驼峰解析为小写后用点分割得到。
@testvoid testadaptivefruit2() { extensionloader也可以通过指定adaptive注解的value,让获取扩展点名字的逻辑更加清晰。
比如取url中的fruittype作为获取扩展名的方式。
@spipublic 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 { private final class? type; /* 扩展点 */ private final string defaultextname; /* 默认扩展名 */ private final map核心逻辑在invocationhandler#invoke代理逻辑中,和adaptiveclasscodegenerator#generatemethodcontent一样。
@overridepublic 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测试验证:
@configurationpublic 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是个扩展点,且能生成自适应扩展实现。
@spipublic interface inner { @adaptive string echo(url url);}inner有innera实现。
public class innera implements inner { @override public string echo(url url) { return a; }}测试方法,injectextimpla 被自动注入了inner的自适应实现 。
@testvoid 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代表用户等级。
@qualifierpublic @interface category { string value();}@qualifierpublic @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@componentscanpublic 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,所以都能查到;
@testvoid 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),普通扩展点通过扩展名获取。
@spipublic 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注解,优先级高于动态生成自适应扩展点。
@adaptivepublic class adaptivefruit implements fruit {}动态生成自适应扩展点。
出现的背景是,dubbo中有许多依赖url上下文选择不同扩展点策略的场景,如果通过硬编码实现,会有很多重复代码。
动态生成自适应扩展点,针对@adaptive注解方法且方法参数有url的扩展点,采用javassist字节码技术,动态生成策略实现。
@spipublic 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查询一类扩展点实现。
@testvoid testactivate() { extensionloader依赖注入无论是普通/包装/自适应扩展点,在暴露给用户使用前,都会进行setter依赖注入。
依赖注入对象可来源于两部分:
1)spiextensionfactory根据type获取自适应扩展点
2)springextensionfactory根据setter属性+type从ioc容器获取扩展点
VIAVI 4G和5G实验室及外场测试平台支持全球五个国家的O-RAN Plugfest大会
经典多目标跟踪算法DeepSORT的基本原理和实现
欧盟获强制要求手机厂商将电池设计成便于更换
联璧金融崩盘!斐讯0元购抛弃合作伙伴求生,以后还能投资什么?
别让MCU、内核或编程语言干扰设计
Dubbo源码(一)SPI vs Spring
华为IP自动驾驶网络为千行百业输送数字动能
铁氧体电感厂家科普磁环电感定制升级主要指的是什么
电力监控系统在集成电路硅片生产基地项目的应用
兽药残留速测仪的使用方法及性能
马斯克的十年火箭发射史
关于叶面积分析仪功能特点的详细说明
开创电子产业评选之先河的品牌盛会 今年又创新纪录
FPGA电路中的毛刺现象
西门子PLC五个系列的特点和区别
苹果股东向库克提起诉讼,因隐瞒中国市场对iPhone需求下滑的实情
苹果AR眼镜的最新专利先知道
人工智能技术的发展需要三个要素:数据、算法和算力
健身房中的智能镜子给健身房带来了新的变化
微型手机化学传感器问世