原创 吴就业 147 0 2019-10-13
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/0c034a8080464521ac4775c51fab979f
作者:吴就业
链接:https://wujiuye.com/article/0c034a8080464521ac4775c51fab979f
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
本篇文章写于2019年10月13日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。
SPI全称是Service Provider Interface,直译就是服务提供者接口,是一种服务发现机制,是Java的一个内置标准,允许不同的开发者去实现某个特定的服务。SPI 的本质是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件,加载实现类,实现在运行时动态替换接口的实现类。理论总是枯燥的。
为什么要掌握SPI。Dubbo官方文档的源码导读部分介绍到,如果大家想要学习Dubbo的源码,SPI机制务必弄懂。可想而知,学习SPI对阅读Dubbo源码的重要性。是否有点危言耸听,我来解释下,SPI机制在Dubbo中的地位。
Dubbo整个框架是由一个个组件构成的,每个组件实现的是不同分层的逻辑,Dubbo就是通过SPI机制加载所有的组件。Dubbo总体分为业务层、RPC层、Remote层,而RPC层又可细分为代理层、注册中心层、集群负载层、监视器层、协议层,Remote层又可细分为信息交换层、传输层、序列化层。每一个细分的层都使用了SPI机制,实现灵活的组装使用,这也是Dubbo框架的优秀设计思想。
Dubbo的SPI并非使用Java提供的SPI,完全是自己实现的一套SPI机制,并对其进行了增强,如通过字节码实现动态代理类。我们先来了解下Java的SPI,再学习Dubbo的SPI,就能分辨出它们的不同之处。
先来个Hello Word认识一下SPI。随便定义一个接口,比如LoginService,假设我们有多种登录方式。
如果用Shiro框架实现系统的用户权限管理,则提供一个ShiroLoginService。
如果我不想搞那么麻烦,我就直接使用Spring MVC的拦截器实现用户授权验证,那么就提供一个SpringLoginService。
我想通过修改配置文件的方式而不修改代码实现权限验证框架的切换,如何实现呢。用SPI,通过运行时从配置文件中读取实现类,加载使用配置的实现类。首先,需要在resources目录下新建一个目录META-INF,并在META-INF目录下创建services目录,用来存放配置文件。为什么一定要是services目录呢,一会看java的实现源码就知道了。
配置文件名为接口LoginService全类名,文件中写入使用的实现类的全类名。只要是在META-INF/services目录下,只要文件名是接口的全类名,那么编写配置文件内容的时候,IDEA就会自动提示有哪些实现类,很强大的IDEA。
[com.wujiuye.spi.LoginService配置文件内容]
com.wujiuye.spi.ShiroLoginService
编写个main方法试下Java提供的SPI。
ServiceLoader就是java提供的SPI机制的实现,调用load传入接口名获取到一个ServiceLoader实例,此时配置文件中注册的实现类是还没有加载到JVM的,只有通过iterator遍历获取的时候,才会去加载实现类,并实例化实现类。
好了,我们不需要关心例子输出的是什么,想必看都看出来了。需要说明的是,例子中配置的只有一个实现类,但其实我们是可以配置N多个的,并且iterator遍历的顺序就是配置文件中注册的实现类的顺序。
多实现类,如果非要想一个适用的业务场景的话,拦截器、过滤器等可插拔的设计模式,使用SPI加载是最好不过了。又或者一个画图程序,定义一个形状接口,实现类可以有矩形、三角形等,后期又加了圆形,就可以通过配置的方式支持圆形,完全不用修改任何代码。这样说,你能想到SPI的强大了吗?
从例子中可以看出,我们分析Java SPI的实现源码需要从ServiceLoader的load方法入手,看源码首先就是要找到入口。但是分析Dubbo与Spring整合的源码的时候入口就没有那么好找了,即便找到了入口,由于Dubbo的多组件架构,也很容易迷路,其实这些都算容易的,最难的是看多线程框架的源码,除非有指南针,否则很容易就迷失在代码的海洋里,典型的代表就是Netty。
ServiceLoader的源码是很容易理解的,就是根据传入的接口,获取到接口的全类型名,将前缀”/META-INF/services”与类型名“com.wujiuye.spi.LoginService”拼接,就能定位到配置文件,然后就是获取类加载器,类加载器就是当前线程的上下文类加载器,根据类加载器获取资源文件,读取配置文件中的字符串,解析字符串,将解析出来的实现类全类名添加到一个数组,返回一个ServiceLoader实例,然后在遍历迭代器的时候再通过Class.forName加载类,最后实例化实现类,就是这么简单。
第一步是拿到当前线程的类加载器,调用ServiceLoader.load(Class<S> service,ClassLoader classLoader)
方法实例化ServiceLoader。所以说,我们也可以自己指定类加载器。
接着看ServiceLoader的构造方法都做了些什么工作。
构建方法中判断如果类加载器为空,则使用系统类加载器,然后调用reload方法创建一个懒加载迭代器LazyIterator。所以我们调用ServiceLoader对象的iterator方法获取到的迭代器就是这个LazyIterator。
拿到迭代器后,接着我们会遍历迭代器,看下hashNext方法。
第一次调用hashNext方法configs是为空的,重点看第一条红色处,获取接口的全类名与前缀拼接拿到文件的路径。
private static final String PREFIX = "META-INF/services/";
这也就说明了为什么配置文件一定是放在“META-INF/services”目录下。
这里还会再做一次类加载器的检测,如果类加载器为空,则使用系统类加载器获取资源。最后是解析获取注册的所有实现类的类名,pending是一个迭代器Iterator。解析过程就不看了,无非就是获取文件流,从流中读取文件内容,根据换行符获取实现类类名。
最后看下调用迭代器的next方法获取一个实现类的过程。
图中第一处画红色的地方,就是加载实现类,第二处红线就是通过Class的newInstance方法获取new一个对象。cast方法只是做强制类型转换。整个源码再简单不过了。
建议看下官方文档写的Dubbo SPI源码分析,写得比较详细。这里我想用我的方式去介绍Dubbo SPI。传送门:https://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html
我之前下载了Dubbo的2.7.0版本的源码,所以我就基于Dubbo2.7.0介绍了。SPI相关的源码在dubbo-common模块下,extension包下,extension顾名思义,就是扩展,该包就是Dubbo扩展点机制实现的核心代码。
Dubbo的SPI实现是ExtensionLoader这个类,作用跟Java SPI的ServiceLoader一样。实现配置文件的读取解析,生成实例。但Dubbo的SPI需要在接口上添加注解@SPI。
Dubbo SPI的配置文件与Java SPI配置文件的写法不同,Dubbo是通过key-value的格式为接口配置实现类的,但这并不意味着一个接口只能有一个实现类。
比如:
@SPI("dubbo")
public interface Protocol {
}
Protocol接口的@SPI注解value指定了dubbo,但这并不意味着只能使用
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
的配置,而是默认使用dubbo作为key(name),而dubbo=xxx的配置是指定dubbo协议的实现类,是的,在这个例子中,key只是用来限制dubbo协议只能配置一种实现类的。但是我们可以使用不同的协议,在调用ExtensionLoader获取Protocol实现类时,会根据你配置的协议作为key,从META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol配置文件中取得key指定的实现类。
与其说dubbo整个项目的源码神奇,不如说我一直很讨厌maven,因为它的配置太多太难懂,Dubbo这种项目中包含模块模块中又包含模块的,我看不懂maven的build配置的插件是怎么编译。比如Dubbo的RPC层。
不过我知道的是,多个模块编译后资源目录下相同的文件名的文件内容会合并到一起,所以dubbo协议的实现模块的org.apache.dubbo.rpc.Protocol只配置
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
hession协议的实现模块的org.apache.dubbo.rpc.Protocol只配置
hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol
编译后合并在一起就是
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol
接着我们说下@Activate注解和@Adaptive注解。
@Activate自动激活
被@Activate注解注释的扩展点默认被激活启用,还可以通过注解的value指定该扩展点在什么条件下被自动激活。比如扩展点Filter的多个实现类:MonitorFilter和FutureFilter。(父类ListenableFilter实现了Filter扩展点)
@Activate(group = {PROVIDER, CONSUMER})
public class MonitorFilter extends ListenableFilter {
}
MonitorFilter在生产者和消费者环境下都默认激活。
@Activate(group = CommonConstants.CONSUMER)
public class FutureFilter extends ListenableFilter {
}
FutureFilter只在消费者端环境下被激活。
获取实现类不能再是使用ExtensionLoader的
public T getExtension(String name) // name就是配置文件中的key
方法,而是
public List<T> getActivateExtension(URL url, String[] values)
当然,这是dubbo为适配不同用途做的扩展功能,为支持一个扩展点有多个实现类需要同时启用的场景。这是Dubbo SPI实现Java SPI的支持一个接口多个实现类同时生效的特性,称之为自动激活。同时做了增强,即可以指定在某个条件下才激活。
@Activate注解是注释在扩展点(接口)的实现类上面的,所有该扩展点被@Activate注释的实现类都会在指定条件下自动激活。
@Adaptive自适应扩展
@Adaptive注解在方法上则该方法会被增强,这就是Dubbo自适应扩展机制,最重要的三个字:自适应。所以我们重点关注:什么是自适应,怎么实现,实现的目的是什么?带着问题去看。
在 Dubbo 中,很多拓展都是通过SPI机制进行加载的,比如 协议Protocol、负载均衡LoadBalance等。有些拓展希望在方法被调用时,根据运行时参数进行加载(这就是目的)。自适应拓展机制的实现逻辑比较复杂,Dubbo会使用javassist为拓展接口生成具有代理功能的代码,然后通过jdk编译这段代码得到Class类(这就是怎么实现的)。最后再通过反射创建代理类。
一个接口中有多个方法被@Adaptive注释时,Dubbo会遍历所有方法,对被@Adaptive注释的方法生成代理代码,所以,同一个接口的多个@Adaptive方法都在同一个代理类中,生成代码的是org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator这个类,使用javassist生成字节码,这比asm容易多了,很容易看懂。
有使用@Adaptive注解的类,需要使用getAdaptiveExtension方法获取实现类实例,你会看到getAdaptiveExtension不需要指定name,因为一个接口只能有一个动态代理实现类,这个代理类是在运行时才生成的,而根据name去SPI取实现类是在代理中完成的。
public T getAdaptiveExtension()
@Adaptive注解的value属性,是配置根据URL的哪个参数来作为name,通过SPI获取实现类(这就是自适应)。比如value=protocol时,使用javasist生成的动态代理实现的方法中,会生成url.getProtocol方法获取属性值。没错,@Adaptive注解的value就是用来指定调用URL参数对象的哪个get方法的。@Adaptive注解的方法必须有URL参数,或者像Invoker这种提供getUrl方法的,继承Node接口的接口都有这个方法,这是在Node接口中定义的。
那dubbo的SPI自适应扩展主要用来做什么呢?注意,这里是重点。
前面说的,被@Adaptive注解的方法在运行时会通过javassist生成动态代理类,而这个动态代理类做的事情就是根据运行时随时可能会变化的参数,动态通过SPI加载具体的实现类,然后再调用实现类的方法。
比如,rpc远程调用时,会在url上携带参数,如调用的目标XxxService的yy方法,而服务提供者XxxService有多个实现类,那么就可以在url上指定使用哪个实现类(配置文件中该实现类的key),然后再通过SPI获取到该实现类的实例。
Javassist生成的代码,就是拿到方法的URL参数,从URL中获取动态配置的参数,然后通过SPI加载具体的实现类,最后调用实现类的方法。所以,先判断方法URL参数是否为null,如果为null抛出异常,否则从URL中获取参数,如果没有获取到,也是抛出异常,如果获取到,就通过SPI获取实例。
举例:
比如XxxService自适应扩展的实现类为AdaptiveXxxService,这是通过运行时javassist生成的。
public class AdaptiveXxxService implements XxxService {
public Object yy(URL url) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
// 1.从 URL 中获取 XxxService 名称
String xxxServiceName = url.getParameter("xxxServiceName");
if (xxxServiceName == null) {
throw new IllegalArgumentException("xxxServiceName == null");
}
// 2.通过 SPI 加载具体的 XxxService
XxxService xxxService = ExtensionLoader
.getExtensionLoader(XxxService.class)
.getExtension(xxxServiceName);
// 3.调用目标方法
return xxService.yy(URL url);
}
}
一个远程调用实例,URL如下。
dubbo://127.0.0.1:9000/XxxService?xxxServiceName=default
假设XxxService的配置文件内容如下
default=DefaultXxxService
那么通过SPI自适应机制就能获取到DefaultXxxService实例,最终调用的是DefaultXxxService的方法。
Dubbo协议层Protocol就用到了这个特性,比如调用url是dubbo://127.0.0.1:9000/com.wujiuye.spi.XxxService…..,那么协议就是dubbo(key=dubbo),SPI自适应扩展机制会生成Protocol的代理类。调用export发布服务,与调用refer调用服务,都是走自适应扩展机制,根据协议动态从SPI中获取实现类的实例。
@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 SPI最难理解的也就这些内容了,我不分析具体源码,因为官方分析的已经很到位了。
相信通过本篇的学习,大家都能够掌握什么是SPI了,看完后面试不再需要背什么是SPI,直接说源码。Dubbo框架相对来说,是一个比较容易入门与学习源码的框架,因为有中文文档,对我这种英盲而言,真的太爽了。至今为止,Netty是我读过源码的框架中最难读懂的一个框架。
每学习一个框架,都能从中学到不同的设计思想,不得不佩服Dubbo的设计者,正是分层的设计架构与Dubbo SPI的支持、恰到好处的设计模式的使用,让Dubbo具有非常高的扩展性。
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
IP信息库是按区间存储的,拿到一个ip得要知道它所属的范围才能知道它对应哪条记录。本篇介绍如何使用Redis的Sorted Set数据结构实现支持范围查找的IP库缓存方案。
笔者最近的一次重构项目选择用dubbo去实现服务间的调用,选择dubbo作为分布式的RPC远程服务调用框架,但笔者在使用的过程中遇到了很多疑难问题,网上搜不到一篇能解决我疑问的文章,无奈,只能选择自己从源码中寻找答案。
Java8提供的流式编程Stream,相信大家每天都在用。但是读过源码的,我猜也没有几个,包括我。只是最近使用上遇到些问题,不得不去深入了解,所以我花了点时间粗略看了一下,但关于并行流的逻辑我也没理解清楚。
第一次将分布式技术应用到实际项目中就遇到分布式事务的问题,好在不是那种严格要求双写一致性的事务问题。我了解的分布式事务解决方案有两种,分别是XA和TCC,今天要分享的是,我如何使用TCC处理项目中分布式事务问题。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。