dubbo拓展点机制在dubbo中应用广泛,使框架中的接口与实现完全解耦,给予了dubbo强大的定制、拓展能力。
1.注解
1.1 @SPI
@SPI注解在一个接口上,表示这是一个dubbo拓展点,同时dubbo的扩展点接口也必须用@SPI注解才能被加载,否则会抛出异常。
源码:
private static <T> boolean withExtensionAnnotation(Class<T> type) {
return type.isAnnotationPresent(SPI.class);
}
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
@SPI注解可以给一个参数,表示此接口默认使用的扩展点,如@SPI("imp1"),即getDefaultExtension返回的结果。
1.2 @Adaptive
@Adaptive有两个作用,当它注解在一个拓展点实现类上时,表示这个实现类是自适应实现类,即getAdaptiveExtension返回的结果。当它注解在接口的方法上时,表示这是一个自适应方法,此时getAdaptiveExtension返回一个动态编译的代理类,根据实际调用时参数来决定使用哪个拓展点。
1.3@Activate
@Activate注解可以同时激活一个拓展点一组不同的实现。可以设置分组名和排序信息,通过getActivateExtension可以同时激活一组符合要求的拓展点实现。
2.原理
Dubbo SPI思想源于Java SPI,是对Java原生SPI的改进。Java SPI基于策略模式,只管定义接口,接口的实现由应用外的配置来决定,这样就实现了接口和实现相分离,系统更灵活,如jdbc就只依赖与driver接口,具体使用什么数据库由相应的驱动jar包决定。具体步骤如下:
1.接口使用方定义服务接口
2.接口提供方实现接口
3.接口提供方在META-INF/services目录下建立文件名为接口名,文件内容为实现类的配置文件
4.接口使用方通过ServiceLoader.load来加载具体实现类
Dubbo SPI主要有几点不同:
1.扩展点配置目录为:META-INF/dubbo、META-INF/dubbo/internal、META-INF/services,在其中任意一个目录中新建文件都可,如果有冲突,按照/dubbo/internal/,/dubbo,/services的优先顺序
2.配置文件名还是接口名,内容不再是实现类名,而是,推展的名称=实现类名,如:
META-INF/dubbo/com.service.ITestInterface文件内容为:imp1=com.service.TestImp3
com.service.ITestInterface是接口名,imp1是拓展点点名称,
co m.service.TestImp1为接口的一个实现类,多个实现用换行符分割
3.接口需要用@SPI注解,Java SPI是不需要的
4.使用ExtensionLoader来加载拓展点实现。
3.常用加载方法的使用
我们使用一个测试接口和几个简单的打印实现来测试拓展点加载使用。
测试接口定义如下:
@SPI("imp2")
public interface ITestInterface {
@Adaptive({"key1","key2"})
void dosth1(URL url);
@Adaptive({"key2","key1"})
void dosth2(URL url);
@Adaptive
default void doDefault(URL url)
{
System.out.println("doDefault called!");
}
}
META-INF/dubbo/com.service.ITestInterface中定义来三个拓展点:
imp1=com.service.TestImp1
imp2=com.service.TestImp2
imp3=com.service.TestImp3
通过打印不同来区分加载的是哪个
public class TestImp1 implements ITestInterface {
public void dosth1(URL url) {
System.out.println("impl1 dosth1 called!");
}
public void dosth2(URL url) {
System.out.println("impl1 dosth2 called!");
}
}
public class TestImp2 implements ITestInterface {
public void dosth1(URL url) {
System.out.println("impl2 dosth1 called!");
}
public void dosth2(URL url) {
System.out.println("impl2 dosth2 called!");
}
}
public class TestImp3 implements ITestInterface {
public void dosth1(URL url) {
System.out.println("impl3 dosth1 called!");
}
public void dosth2(URL url) {
System.out.println("impl3 dosth2 called!");
}
}
1.getExtension(String name)
根据拓展名获取拓展,依次获取imp1,imp2,imp3三个拓展点:
String[] extNames = new String[]{"imp1", "imp2", "imp3"};
for (String name : extNames)
{
ITestInterface test = ExtensionLoader.getExtensionLoader(ITestInterface.class).getExtension(name);
test.dosth1(null);
test.dosth2(null);
}
依次打印:
impl1 dosth1 called!
impl1 dosth2 called!
impl2 dosth1 called!
impl2 dosth2 called!
impl3 dosth1 called!
impl3 dosth2 called!
2.getDefaultExtension()
获取默认实现,及@SPI注解的实现,我的的demo里面是imp2,
@SPI("imp2")
public interface ITestInterface
测试:
ITestInterface defaultExt = ExtensionLoader.getExtensionLoader(ITestInterface.class).getDefaultExtension();
defaultExt.dosth1(null);
defaultExt.dosth2(null);
打印结果:
impl2 dosth1 called!
impl2 dosth2 called!
符合预期。
3.getAdaptiveExtension()
getAdaptiveExtension获取自适应的实现,和前面讲的@ Adaptive注解搭配使用。
当@Adaptive注解在实现类上时,getAdaptiveExtension返回此实现类。先在TestImp1上注解@Adaptive测试下:
@Adaptive
public class TestImp1 implements ITestInterface
ITestInterface adaptExt = ExtensionLoader.getExtensionLoader(ITestInterface.class).getAdaptiveExtension();
URL url1 = new URL("test", null, 0, new HashMap<String, String>() {{
put("key1", "imp1");
put("key2", "imp2");
}});
adaptExt.dosth1(url1);
adaptExt.dosth2(url1);
打印
impl1 dosth1 called!
impl1 dosth2 called!
说明getAdaptiveExtension返回了imp1的实现。此时imp1为自适应拓展,不再是普通拓展了,所以getExtension(imp1)会返回null。
接下来将类上的@Adaptive去掉,注解在接口的方法上
@Adaptive({"key1","key2"})
void dosth1(URL url);
@Adaptive({"key2","key1"})
void dosth2(URL url);
注解的参数key1,key2为方法调用时寻找自适应实现的key,通过URL的parameters获取对应的value来确定要调用的实现类,注解中可设置多个key,按照先后顺序进行查找。
ITestInterface adaptExt = ExtensionLoader.getExtensionLoader(ITestInterface.class).getAdaptiveExtension();
URL url1 = new URL("test", null, 0, new HashMap<String, String>() {{
put("key1", "imp1");
put("key2", "imp2");
}});
adaptExt.dosth1(url1);
adaptExt.dosth2(url1);
在dosth1,dosth2中传入URL参数,设置key1为imp1,key2为imp2,由于dosth1中@Adaptive参数顺序是key1,key2, dosth2中@Adaptive参数顺序是key2,key1,所以可以预测dosth1最终调用的是imp1的实现,dosth2最终调用的是imp2的实现,打印也证明了这一点:
impl1 dosth1 called!
impl2 dosth2 called!
这是如何实现的呢?原理是动态生成java代码,编译一个临时类ITestInterface$Adaptive,代码中对接口方法的实现逻辑为:从从URL中根据key的顺序获取拓展点名,加载对应的拓展点进行调用,如上面自适应类dosth1方法的代码如下:
public void dosth1(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("key1", url.getParameter("key2", "imp2"));
if(extName == null) throw new IllegalStateException("Failed to get extension (com.service.ITestInterface) name from url (" + url.toString() + ") use keys([key1, key2])");
com.service.ITestInterface extension = (com.service.ITestInterface)ExtensionLoader.getExtensionLoader(com.service.ITestInterface.class).getExtension(extName);
extension.dosth1(arg0);
}
如果实现类和接口方法上同时注解了@Adaptive,则以实现类优先,getAdaptiveExtension直接返回被注解的实现类。
4.总结
dubbo拓展点机制在框架中使用得非常广泛,如:Protocol、Cluster、Transporter等很多重要的接口都是用ExtensionLoader进行加载的。接口和实现分离,这样增加拓展性的同时也增加了学习门槛,经常容易找不到代码在哪里实现的,对拓展点机制的学习能够降低阅读源码的门槛。
网友评论