Java SPI
在进行分析Dubbo SPI机制之前,我们还是从我们熟悉的java spi机制入手,其实在我们平常使用的开发框架中, 处处都是使用了SPI机制,比如我们使用的JDBC,日志框架等,我们可以根据配置集成我们需要的数据库例如mysql、oracle 等,下面从一个简单的例子来看一下Java SPI;
Java Spi demo:
public interface Tea { String getTeaName(); } public class GreenTea implements Tea { @Override public String getTeaName() { return "green"; } } public class RedTea implements Tea { @Override public String getTeaName() { return "red"; } } import java.util.ServiceLoader; public class JavaSpiTest { public static void main(String[] args) { ServiceLoader<Tea> load = ServiceLoader.load(Tea.class); for (Tea tea : load) { System.out.println(tea.getTeaName()); } } }
还有一个最重要的文件:文件路径为 resources/MET-INF/services。文件名称为接口的全限定名,在本demo中就是com.tuling.javaspi.Tea。文件内容如下:
com.tuling.javaspi.GreenTea com.tuling.javaspi.RedTea
这样我们在文件中写入Tea接口的具体实现类,就可以得到不同的实现,所以JDBC只需要暴露一个标准接口,具体的实现有各自的厂商自定义。然后用户项目中进行对应jar的和配置即可。这种 机制给各个框架提供了非常好可 扩展机制。
缺点
通过上面的代码我们会发现一个缺点,就是我们不能实现按需加载,也就说系统会一次性将文件中的所有的实现类都实例化了,这在性能和资源上都是一种浪费。
思考:我们如何实现按需加载呢?
思路:如果我们在文件中配置成key=value的规则,但时候我们在获取的时候,传入key 就可以实现了。
Dubbo SPI
熟悉Dubbo的童鞋们都知道,Dubbo提供了极高的可扩展机制,所以说SPI贯穿了整个dubbo的核心 。
下面我们先来从一张图整体认识一下,Dubbo的SPI机制整体源码流程图:
上图是小编在读SPI机制源码的时候根据自己的总结画出的源码中核心的方法流转图,各位小可爱可以根据上面的流程图阅读源码,效率会更高一些。
SPI核心代码详解
在我们看Dubbo的源码的时候经常看到上面的这种代码的写法,他们分别对应自适应扩展点(默认扩展点)、指定名称的扩展点、激活扩展点,在这里我们先对这些概念有一个认识,后面我们会详细介绍
ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension(); ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name); ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);
例如:
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
Protocol接口,在运行的时候dubbo会判断一下应该选用这个Protocol接口的哪个实现类来实例化对象。
它会去找你配置的Protocol,将你配置的Protocol实现类加载到JVM中来,然后实例化对象,就用你配置的那个Protocol实现类就可以了。
上面那行代码就是dubbo里面大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态的根据配置去找到对应的实现类。如果你没有配置,那就走默认的实现类。
@SPI("dubbo") public interface Protocol { int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); }
在dubbo自己的jar中,在META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol文件中:
filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper mock=org.apache.dubbo.rpc.support.MockProtocol dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol injvm=org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol rmi=org.apache.dubbo.rpc.protocol.rmi.RmiProtocol hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol http=org.apache.dubbo.rpc.protocol.http.HttpProtocol org.apache.dubbo.rpc.protocol.webservice.WebServiceProtocol thrift=org.apache.dubbo.rpc.protocol.thrift.ThriftProtocol native-thrift=org.apache.dubbo.rpc.protocol.nativethrift.ThriftProtocol memcached=org.apache.dubbo.rpc.protocol.memcached.MemcachedProtocol redis=org.apache.dubbo.rpc.protocol.redis.RedisProtocol rest=org.apache.dubbo.rpc.protocol.rest.RestProtocol xmlrpc=org.apache.dubbo.xml.rpc.protocol.xmlrpc.XmlRpcProtocol registry=org.apache.dubbo.registry.integration.RegistryProtocol qos=org.apache.dubbo.qos.protocol.QosProtocolWrapper
所以这就看到了dubbo的SPI机制默认是怎么玩的了,其实就是Protocol接口,@SPI(“dubbo”) 说的是,通过 SPI 机制来提供实现类,实现类是通过 dubbo 作为默认 key 去配置文件里找到的,配置文件名称与接口全限定名一样的,通过 dubbo 作为 key 可以找到默认的实现类就是 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
如果想要动态替换掉默认的实现类,需要使用 @Adaptive 接口,Protocol 接口中,有两个方法加了 @Adaptive 注解,就是说那俩接口会被代理实现。
比如这个 Protocol 接口搞了俩 @Adaptive 注解标注了方法,在运行的时候会针对 Protocol 生成代理类,这个代理类的那俩方法里面会有代理代码,代理代码会在运行的时候动态根据 url 中的 protocol 来获取那个 key,默认是 dubbo,你也可以自己指定,你如果指定了别的 key,那么就会获取别的实现类的实例了。
核心类、核心接口
ExtensionLoader
ExtensionLoader表示某个接口的扩展点加载器,可以用来加载某个扩展点的实例。在ExtensionLodader中 除开有有几个非常重要的属性
1、Class<?> type:表示当前ExtensionLoader实例是哪个接口的扩展点加载器
2、ExtensionFactory objectFactory:扩展点工厂(对象工厂),可以获得某个对象
ExtensionLoader和ExtensionFactory的区别在于:
ExtensionLoader最终所得到的对象是Dubbo SPI机制产生的
ExtensionFactory最终所得到的对象可能是Dubbo SPI机制所产生的,也可能是从Spring容器中所获得的对象
前面我们介绍了三种获得扩展点的方式,我们现在来介绍另一个非常重要的方法:
createExtension(String name)方法
在调用createExtension(String name)方法去创建一个扩展点实例时,要经过以下几个步骤:
根据name找到对应的扩展点实现
根据实现类生成一个实例,把实现类和对应生成的实例进行缓存
对生成出来的实例进行依赖注入(给实例的属性进行赋值)
对依赖注入后的实例进行AOP(Wrapper),把当前接口类的所有的Wrapper全部一层一层包裹在实例对象上,没包裹个Wrapper后,也会对Wrapper对象进行依赖注入
返回最终的Wrapper对象
getExtensionClasses()方法
getExtensionClasses()是用来加载当前接口所有扩展点实现类的,返回一个Map。之后可以从这个map中按照指定的那么获取对应的扩展点实现类。
当把当前接口的所有实现类都记载出来以后也会进行缓存,下次需要的时候直接从缓存中拿。
Dubbo在加载一个接口的扩展点时,思路是这样的:
1、根据接口的全限定名去META-INF/dubbo/internal/目录下寻找对应的文件,调用loadResource方法进行加载
2、根据接口的全限定名去META-INF/dubbo/目录下寻找对应的文件,调用loadResource方法进行加载
3、根据接口的全限定名去META-INF/services/目录下寻找对应的文件,调用loadResource方法进行加载
loadResource方法
loadResource方法就是完成对文件内容的解析,按行进行解析,回解析出“=”两边的内容,“=”左边的内容就是扩展点的name,右边的内容就是扩展点实现类,并且会利用ExtensionLoader类的类加载器来加载扩展点实现。然后调用loadClass方法对name和扩展点实例进行详细的解析,并且最终把他们放到map中。
loadClass()方法
loadClass方法会做下面几件事情:
1、当前扩展点实现类上是否存在@Adaptive注解,如果存在则把类认为是当前接口的默认自适应类(接口代理类),并把该类存到cachedAdaptiveClass属性上。
2、当前扩展点实现是否一个当前接口的Wrapper类,如何判断是否是Wrapper类?就是看当前类中是否存在一个构造方法,该构造方法只有一个参数,参数类型就为接口类型,如果存在这一个这样的构造方法,那么这个类就是该接口类的Wrapper类,如果是,则把该类添加到cachedWrapperClasses中去,cachedWrapperClasses是一个set。
3、如果不是自适应类,或者也不是Wrapper类,则判断时是否存在name,如果不存在name,那么就会报错。
4、如果有多个name,则判断一下当前扩展点实现类上是否存在@Adaptive注解, 如果存在则把该类添加到cachedActivates中,cachedWrapperClasses是一个map。
5、最后,遍历多个name,把每个name和对应的实现类存到extensionClasses中去,extensionClasses就是上文所提到的map。
至此,加载类就走完了。
回到createExtension(String name)方法中的逻辑,当前这个接口的所有扩展点实现类都扫描完了之后,就可以根据用户所指定的名字,找到对应的实现类了,然后进行实例化,然后进行IOC(依赖注入)和AOP。
Dubbo中的IOC
1、根据当前 实例的类,找到这个类中的setter方法,进行依赖注入
2、先分析出setter()方法的参数类型pt
3、在截取出setter方法所对应的属性名property
4、调用objectFactory.getExtension(pt, property)得到一个对象,这里就会从Spring容器 或通过DubboSpi机制得到一个对象,比较特殊的是,如果是通过DubboSpi机制得到的对象,是pt这个类型的一个自适应对象(代理对象)。
5、再反射调用setter方法进行注入。
Dubbo中的AOP
dubbo中也实现了一套非常简单的AOP,就是利用Wrapper,如果一个接口的扩展点中包含了多个Wrapper类,那么在实例化完某个扩展点后,就会利用这些Wrapper类对这个实例进行包裹,比如:现在有一个DubboProtocol的实例,同时对于Protocol这个接口还有很多的Wrapper,比如ProtocolFilterWrapper、ProtocolListenerWrapper,那么,当对DubboProtocol的实例完成了IOC之后,就会先调用new ProtocolFilterWrapper(DubboProtocol实例)生成一个新的Protocol的实例,再对此实例进行IOC,完了之后,会再调用new ProtocolListenerWrapper(ProtocolFilterWrapper实例)生成一个新的Protocol的实例,然后进行IOC,从而完成DubboProtocol实例的AOP。
小结
至此,Dubbo中关于SPI机制实现的基本原理都介绍完了,其中流程图中涉及到的核心方法都进行了解析,当然源码中还有一些其他的实现方法,感兴趣的读者可以继续进行分析。