一、SPI 机制 介绍
在 Dubbo 中, SPI 机制贯穿整个 Dubbo 的核心,不断反复反复的使用,后面就可以看到。
SPI 全称为 Service Provider Interface,是一种服务发现机制。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求
本质原理
SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。
目的
很容易的通过 SPI 机制为我们的程序提供拓展功能。Dubbo 通过 SPI 机制加载所有的组件.
原生JavaSPI
1.定义一个接口(Robot),两个实现类A、B,并且在META-INF/services 文件夹下创建一个名称为org.apache.spi.Robot(Robot 的全限定名)的文件 。
2.文件内容为实现类A、B的全限定的类名(key=接口名,value=实现类)。
3.然后下面代码直接传入一个接口就可以调用实现类的方法了
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
System.out.println("Java SPI");
serviceLoader.forEach(Robot::sayHello);
}
}
原生JavaSPI应用
JDBC的java.sql.Driver 就是这么实现的,JDK只提供Driver接口,各个厂商比如MySQL在自己包的META-INF/services里写好实现类。
jdk 提供接口本身不关心怎么实现
Dubbo SPI
Dubbo 重新实现了一套功能更强的 SPI 机制
逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader可以加载指定的实现类。
配置文件需放置在 META-INF/dubbo 路径下,如下:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
key名字,value是类,同时实现类记得加@SPI 注解
然后与JavaSPi不同的是,只需要传名字就可以获得指定的实现类
两者对比
SPI 的缺点
-1. JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现。
如果你在 META-INF/service 下的文件里面加了 N个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到,那么会很浪费资源
-2. 如果扩展点加载失败,会导致调用方报错,而且这个错误很难定位到是这个原因
Dubbo 实现的SPI后,可以针对自己的协议、拦截、集群、路由、负载均衡、序列化、容器… 几乎里面用到的所有功能,都可以实现自己的扩展,这个是 dubbo 比较强大的一点
总结:总的来说,思路和 SPI 是差不多。 都是基于约定的路径下制定配置文件。目的,通过配置的方式轻松实现功能的扩展
二、DubboSPI扩展点实现原理
基本代码是:
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
1.首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,
2.然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。
1.getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单
2.具体看ExtensionLoader 的 getExtension 方法
getExtension =》createExtension
根据一个名字来获得一个对应类的实例, getExtension一个是缓存中取,另外是双重检查确保创建,主要逻辑在createExtension
- 通过 getExtensionClasses 获取所有的拓展类,再取那个name对应的class
- 通过class反射创建拓展对象(包含先从缓存取)
- 向拓展对象中注入依赖(先反射获取所有方法,找setter注入)
- 将拓展对象包裹在相应的 Wrapper 对象中
getExtensionClasses 获取所有的拓展类
通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),比较简单就是缓存、双重检查、加载读取固定位置文件(/META-INF/dubbo || /META-INF/services )。
保存到一个 HashMap 中(classes), key=name, value=对应配置的类的实例
三、自适应扩展点Adaptive
有时,有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。(动态的,比如dubbo网络传过来的实现类临时去加载)
对于这个矛盾的问题,Dubbo 通过自适应拓展机制很好的解决了。
首先 Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类。
举例
@SPI("dubbo")
public interface Protocol {
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
int getDefaultPort();
}
1.Protocol 这个接口来,它里面定义了 export 和 refer 两个抽象方法,这两个方法分别带有@Adaptive 的标识,标识是一个自适应方法。
2.Protocol 是一个通信协议的接口,具体有多种实现, 取决于我们在使用 dubbo 的时候所配置的协议名称。而这里的方法层面的 Adaptive 就决定了当前这个方法会采用何种协议来发布服务。
3.接口上写了@SPI(“dubbo”),即自适应方法默认使用dubbo协议的意思,调用时传了URL参数则用URL里分析出的协议
4.Protocol有多种实现,配置文件里如下(默认取@SPI配置的叫dubbo的):
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
@Adaptive注解
@Adaptive,自适应扩展点的标识注解,可以作用在类上、接口上。
1.作用在类上:
在Dubbo中只有2个类上有Adaptive注解修饰,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory,表示拓展的加载逻辑由人工编码完成,不是自动生成的。更多时候,Adaptive 是注解在接口方法上的,表示拓展的加载逻辑需由框架自动生成。
2.作用在接口上:
如上文提到的,Adaptive注解作用在接口上时,能在程序运行时,根据**参数(分析出URL参数)**动态生成接口的Adaptive自适应拓展类,下面针对此种情况做具体分析
实现源码分析
入口getAdaptiveExtension
检查缓存,缓存未命中,则调用 createAdaptiveExtension 方法创建自适应拓展。
createAdaptiveExtension
- 调用 getAdaptiveExtensionClass 方法获取自适应拓展 Class 对象
- 通过反射进行实例化
- 调用 injectExtension 方法向拓展实例中注入依赖
getAdaptiveExtensionClass
private Class<?> createAdaptiveExtensionClass() {
// 构建自适应拓展代码
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
// 获取编译器实现类
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// 编译代码,生成 Class
return compiler.compile(code, classLoader);
}
一定要注意这里面这个code!!!!!!!!!!!这个就是生成的代码!可以直接debug看:
package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
public void destroy() {
throw new UnsupportedOperationException("...");
}
public int getDefaultPort() {
throw new UnsupportedOperationException("...");
}
public org.apache.dubbo.rpc.Exporter export(
org.apache.dubbo.rpc.Invoker arg0)
throws org.apache.dubbo.rpc.RpcException {
if (arg0 == null) {
throw new IllegalArgumentException("...");
}
if (arg0.getUrl() == null) {
throw new IllegalArgumentException("...");
}
org.apache.dubbo.common.URL url = arg0.getUrl();
String extName = ((url.getProtocol() == null) ? "dubbo"
: url.getProtocol());
if (extName == null) {
throw new IllegalStateException(
"Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" +
url.toString() + ") use keys([protocol])");
}
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class)
.getExtension(extName);
return extension.export(arg0);
}
public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0,
org.apache.dubbo.common.URL arg1)
throws org.apache.dubbo.rpc.RpcException {
if (arg1 == null) {
throw new IllegalArgumentException("url == null");
}
org.apache.dubbo.common.URL url = arg1;
String extName = ((url.getProtocol() == null) ? "dubbo"
: url.getProtocol());
if (extName == null) {
throw new IllegalStateException(
"Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" +
url.toString() + ") use keys([protocol])");
}
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class)
.getExtension(extName);
return extension.refer(arg0, arg1);
}
}
看看code怎么生成的!
String code = createAdaptiveExtensionClassCode();
public String generate() {
// no need to generate adaptive class since there's no adaptive method found.
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}
StringBuilder code = new StringBuilder();
code.append(generatePackageInfo());
code.append(generateImports());
code.append(generateClassDeclaration());
Method[] methods = type.getMethods();
for (Method method : methods) {
code.append(generateMethod(method));
}
code.append("}");
if (logger.isDebugEnabled()) {
logger.debug(code.toString());
}
return code.toString();
}
依次是包信息、导包信息、类描述、再是方法体,重点看方法体,以动态生成Protocol类的refer方法为例:
方法体生成generateMethodContent
private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
return generateUnsupported(method);
} else {
int urlTypeIndex = getUrlTypeIndex(method);
// 找到参数里的URL参数 !
if (urlTypeIndex != -1) {
// Null Point check
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// did not find parameter in URL type
code.append(generateUrlAssignmentIndirectly(method));
}
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
boolean hasInvocation = hasInvocationArgument(method);
code.append(generateInvocationArgumentNullCheck(method));
code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
code.append(generateExtNameNullCheck(value));
code.append(generateExtensionAssignment());
// return statement
code.append(generateReturnAndInvocation(method));
}
return code.toString();
}
看到这里,发现个关键,它在找参数里有没有URL参数,从debug出的源码也可以得出个结论
结论
生成代码那块,它会去自适应方法参数里找URL(或者参数的属性里找URL,比如参数invoker里有URL),再根据URL的情况去适应不同实现,没有找到URL则使用注解里默认的实现。
具体问题具体去debug看那个code代码是什么!直接看代理生成的源码