当前位置: 首页 > 科技观察

采访重点:说说DubboSPI机制

时间:2023-03-21 14:25:59 科技观察

SPI什么是SPI?SPI是缩写。全称是ServiceProviderInterface。Java本身提供了一套SPI机制。在文件中,服务加载器读取配置文件并加载实现类,这样实现类就可以在运行时为接口动态替换,这也是很多框架组件实现扩展功能的一种手段。今天,DubboSPI机制和JavaSPI有点不同。Dubbo没有使用Java原生的SPI机制,而是对其进行了改进和增强,使得Dubbo可以方便地进行功能扩展。要学习一些东西,你必须带着问题去学习。先问几个问题,再看一下1.什么是SPI(开头有说明)2.DubboSPI和Java原生有什么区别3.这两个实现怎么写JavaSPI是怎么实现的先定义一个接口:publicinterfaceCar{voidstartUp();}然后创建两个实现Car接口的类println("Thetrainstarted");}}然后在项目META-INF/services文件夹下创建一个名为com.example.demo.spi.Car的完全限定接口。在文件内容中写上实现类的完全限定名,如下:com.example.demo.spi.Traincom.example.demo.spi.Truck最后写一段测试代码:publicclassJavaSPITest{@TestpublicvoidtestCar(){ServiceLoaderserviceLoader=ServiceLoader.load(Car.class);serviceLoader.forEach(Car::startUp);}}执行后输出:ThetrainstartedThetruckstartedDubboSPIDubbo是如何实现的?Dubbo使用的SPI并不是Java原生的,而是一种新的实现。它的主要逻辑在ExtensionLoader类中,逻辑并不难。后面再说,看看怎么用。它与Java没有太大区别。基于前面的例子,接口类需要注解@SPI:@SPIpublicinterfaceCar{voidstartUp();}实现类不需要改配置文件,需要放在META-INF/dubbo下。配置写法有些不同,直接看代码:train=com.example.demo.spi.Traintruck=com.example.demo.spi.Truck是最后的测试类,先看代码:publicclassJavaSPITest{@TestpublicvoidtestCar(){ExtensionLoaderextensionLoader=ExtensionLoader.getExtensionLoader(Car.class);Carcar=extensionLoader.getExtension("train");car.startUp();}}执行结果:常用注解@SPI在trainstartedDubbo中SPI被标记为扩展接口@Adaptive自适应扩展实现类标记@Activate自动激活条件标记总结一下两者的区别:使用上的区别Dubbo使用ExtensionLoader而不是ServiceLoader,它的主要逻辑都封装在这个类中,配置文件存放目录不同,Java在META-INF/services中,Dubbo在META-INF/dubbo中,META-INF/dubbo/internalJavaSPI会一次性实例化所有扩展点的实现。如果有扩展实现,初始化会比较费时,不会使用,会造成大量的资源浪费。DubboSPI增加了对扩展点IOC和AOP的支持。一个扩展点可以直接setter注入到其他扩展点。JavaSPI加载过程失败,扩展点的名称不可用。例如:JDK标准的ScriptEngine,getName()获取脚本类型的名称。如果RubyScriptEngine加载RubyScriptEngine类失败是因为它依赖的jruby.jar不存在,则不会提示此失败原因。当用户执行ruby脚本时,会报NoSupportruby??,并不是真正失败的原因。前面三个问题都回答了吗?DubboSPI源码分析是不是很简单DubboSPI是通过ExtensionLoader的getExtensionLoader方法获取一个ExtensionLoader实例,然后通过ExtensionLoader的getExtension方法获取扩展类对象。其中,getExtensionLoader方法用于从缓存中获取扩展类对应的ExtensionLoader。如果没有缓存,则新建一个实例,直接添加代码:publicTgetExtension(Stringname){if(name==null||name.length()==0){thrownewIllegalArgumentException("Extensionname==null");}if("true".equals(name)){//获取默认的扩展实现类returnDefaultExtension();}//用来持有目标对象Holderholder=cachedInstances.get(name);if(holder==null){cachedInstances.putIfAbsent(name,newHolder());holder=cachedInstances.get(name);}Objectinstance=holder.get();//DCLif(instance==null){synchronized(holder){instance=holder.get();if(instance==null){//创建扩展实例instance=createExtension(name);//设置实例到holderholder.set(instance);}}}return(T)instance;}上面代码主要做的事情是先检查缓存。如果缓存不存在,则创建一个扩展对象。接下来看创建过程:privateTcreateExtension(Stringname){//从配置文件中加载所有扩展类,得到“配置项名称”到“配置类”的映射关系表Classclazz=getExtensionClasses().get(name);if(clazz==null){throwfindException(name);}try{Tinstance=(T)EXTENSION_INSTANCES.get(clazz);if(instance==null){//反射创建实例EXTENSION_INSTANCES.putIfAbsent(clazz,clazz.newInstance());instance=(T)EXTENSION_INSTANCES.get(clazz);}//给实例注入依赖injectExtension(instance);Set>wrapperClasses=cachedWrapperClasses;if(wrapperClasses!=null&&!wrapperClasses.isEmpty()){//循环创建一个Wrapper实例for(ClasswrapperClass:wrapperClasses){//传递当前instance作为参数赋予Wrapper构造方法,并通过反射创建Wrapper实例//然后向Wrapper实例注入依赖,最后将Wrapper实例赋值给实例变量instance=injectExtension((T)wrapperClass.getConstructor(type).newInstance(instance));}}returninstance;}catch(Throwablet){thrownewIllegalStateException("Extensioninstance(name:"+name+",class:"+type+")无法实例化:"+t.getMessage(),t);}}这段代码看起来很繁琐,其实并不难。总共只有4件事:1.通过getExtensionClasses获取所有配置扩展类2.通过反射创建对象3.向扩展类注入依赖4.在对应的Wrapper对象中包装扩展类对象在通过名称获取扩展类之前,我们首先需要根据配置文件解析出扩展类名到扩展类的映射关系表,然后根据扩展项名从映射关系表中提取对应的扩展类。相关过程的代码如下:privateMap>getExtensionClasses(){//从缓存中获取加载的扩展类Map>classes=cachedClasses.get();//DCLif(classes==null){synchronized(cachedClasses){classes=cachedClasses.get();if(classes==null){//加载扩展类classes=loadExtensionClasses();cachedClasses.set(classes);}}}returnclasses;}这里也是先查缓存,如果没有缓存,再通过双锁查缓存,判断为空。此时如果classes还是null,则通过loadExtensionClasses加载扩展类。下面是loadExtensionClasses方法的代码privateMap>loadExtensionClasses(){//获取SPI注解,其中type变量为finalSPIdefaultAnnotation=type.getAnnotation(SPI.class);if(defaultAnnotation!=null){Stringvalue=defaultAnnotation.value();if((value=value.trim()).length()>0){//分割SPI注解内容String[]names=NAME_SEPARATOR.split(value);//检查SPI注解内容是否合法,不合法则抛出异常if(names.length>1){thrownewIllegalStateException("morethan1defaultextensionnameonextension...");}//设置默认名称,参考getDefaultExtension方法if(names.length==1){cachedDefaultName=names[0];}}}Map>extensionClasses=newHashMap>();//加载指定文件夹文件下的配置loadDirectory(extensionClasses,DUBBO_INTERNAL_DIRECTORY);loadDirectory(extensionClasses,DUBBO_DIRECTORY);loadDirectory(extensionClasses,SERVICES_DIRECTORY);returnextensionClasses;}loadExtensionClasses方法一共做了两件事,一是解析SPI注解,二是另一种是调用loadDirectory方法加载指定文件夹的配置文件。解析SPI注解的过程比较简单,就不多说了。来看看loadDirectory做了什么urls;ClassLoaderclassLoader=findClassLoader();//根据文件名加载所有同名文件if(classLoader!=null){urls=classLoader.getResources(fileName);}else{urls=类加载器。getSystemResources(fileName);}if(urls!=null){while(urls.hasMoreElements()){java.net.URLresourceURL=urls.nextElement();//加载资源loadResource(extensionClasses,classLoader,resourceURL);}}}catch(Throwablet){logger.error("加载扩展类时出现异常(interface:"+type+",descriptionfile:"+fileName+").",t);}}loadDirectory方法首先通过classLoader获取所有资源链接,然后通过loadResource方法加载资源。下面我们继续关注并查看loadResource方法的实现utf-8"));try{Stringline;//逐行读取配置内容while((line=reader.readLine())!=null){//位置#字符finalintci=line.indexOf('#');if(ci>=0){//截取#之前的字符串,#之后的内容是注释,需要忽略line=line.substring(0,ci);}line=line.trim();if(line.length()>0){try{Stringname=null;inti=line.indexOf('=');if(i>0){//以等号=为界,截取key和valuename=line.substring(0,i).trim();line=line.substring(i+1).trim();}if(line.length()>0){//通过loadClass方法loadClass(extensionClasses,resourceURL,Class.forName(line,true,classLoader),name);}}catch(Throwablet){IllegalStateExceptione=newIllegalStateException("Failedtoloadextensionclass...");}}}}finally{reader.close();}}catch(Throwablet){logger.error("Exceptionwhenloadextensionclass...");}}loadResource方法用于读取和解析配置文件,通过反射加载类,最后调用loadClass方法进行其他操作loadClass方法主要是用来操作缓存的。该方法的逻辑如下:privatevoidloadClass(Map>extensionClasses,java.net.URLresourceURL,Classclazz,Stringname)throwsNoSuchMethodException{if(!type.isAssignableFrom(clazz)){thrownewIllegalStateException("...");}//检测目标类上是否有Adaptive注解;}elseif(!cachedAdaptiveClass.equals(clazz)){thrownewIllegalStateException("...");}//检查clazz是否为Wrapper类型}elseif(isWrapperClass(clazz)){Set>wrappers=cachedWrapperClasses;if(wrappers==null){cachedWrapperClasses=newConcurrentHashSet>();wrappers=cachedWrapperClasses;}//将clazz存储到cachedWrapperClasses缓存中wrappers.add(clazz);//程序进入这个Branch,说明clazz是一个普通的扩展类}else{//检查clazz是否有默认构造函数,如果没有则抛出异常clazz.getConstructor();if(name==null||name.length()==0){//如果名字为空,尝试从Extension注解中获取名字,或者使用小写的类名如namename=findAnnotationName(clazz);if(name.length()==0){thrownewIllegalStateException("...");}}//拆分nameString[]names=NAME_SEPARATOR.split(name);if(names!=null&&names.length>0){Activateactivate=clazz.getAnnotation(Activate.class);if(activate!=null){//如果类上有Activate注解,则使用names数组的第一个元素作为key,//存储name到Activate注解对象的映射关系cachedActivates.put(names[0],activate);}for(Stringn:names){if(!cachedNames.containsKey(clazz)){//存储Class与name的映射关系cachedNames.put(clazz,n);}Classc=extensionClasses.get(n);if(c==null){//存储名到Class的映射关系extensionClasses.put(n,clazz);}elseif(c!=clazz){thrownewIllegalStateException("...");}}}}}综上所述,loadClass方法操作了不同的缓存,如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等。基本上加载缓存类的过程就是分析到这里,其他逻辑不难,仔细看总结就可以理解,调试一下。从设计的角度来看,SPI是Dimiter定律和开闭原则的一种实现。开闭原则:对修改关闭,对扩展开放。这个原理在很多开源框架中很常见,Spring的IOC容器也被广泛使用。迪米特法则:也称为最小知识原则,可以理解为,不应该直接依赖的类不要依赖;在有依赖关系的类之间,尽量只依赖必要的接口。那为什么Dubbo的SPI不直接使用Spring呢?这一点可能是很多开源框架的一点蛛丝马迹,因为它本身作为一个开源框架,必须集成到其他框架中或者一起运行,不能作为依赖对象存在。再者,对于Dubbo来说,直接使用SpringIOCAOP会有一些臃肿的架构,完全没有必要,所以自己实现一套轻量级的方案是最好的方案,请关注下方二维码。转载本文请联系建筑科技专栏公众号。