dubbo中定义了很多的扩展点,用SPI注解声明的接口就是一个扩展点。扩展点的每一个实现称为extension。dubbo使用ExtensionLoader来加载所有的Extension,ExtensionLoader的作用和spring容器有点像,spring容器负责创建和管理所有配置的bean,而ExtensionLoader负责加载所有的扩展点实现,不仅如此,ExtensionLoader也支持对扩展点进行依赖注入(后面会讲到),还支持扩展点的Wrapper机制。
我们都知道JDK也提供了SPI机制,那么为什么dubbo还要实现一套扩展点机制呢?相比JDK标准的SPI,dubbo的扩展点机制在以下几个方面进行了改进:
在jar包的META-INF/dubbo/,META-INF/dubbo/internal/和META-INF/services/,这三个目录下放置SPI接口全限定名作为为文件名的文件,文件的内容为:配置名=扩展实现类全限定名,多个扩展点实现用换行分隔。举个例子,Protocol扩展点的配置文件位于dubbo-rpc这个jar包的META-INF/dubbo/internal/目录下,文件名是com.alibaba.dubbo.rpc.Protocol,内容如下:
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
当然这个文件是在dubbo自己的jar包下的,我们当然可以自己写一个类实现com.alibaba.dubbo.rpc.Protocol,然后在自己的jar包的/和META-INF/services/或者META-INF/dubbo/目录下放置一个名为com.alibaba.dubbo.rpc.Protocol的文件,把实现类的名字作为文件内容,dubbo也会加载我们的这个扩展点实现。事实上dubbo会扫描所有classpath下的SPI配置文件,将相同文件名的内容进行合并,平等地对待预定义和外部提供的扩展点实现。
需要注意的是,ExtensionLoader会cache扩展点的实现,因此所有扩展点的实现类都必须是线程安全的,作为单例提供服务。
如果一个SPI接口有多个实现,这时候需要在每一个实现的前后都加一段逻辑(比如统计rt),该怎么办呢,要去改每一个实现类的代码吗?如果实现类第三方jar包中的怎么办?关于这点,dubbo作者早就想到了,他们提供了扩展点的Wrapper机制。wrapper其实也是一个扩展点实现,它和被wrapper的扩展点实现一样,都实现了相同的SPI接口,不同的是wrapper有一个拷贝构造函数。举个例子,dubbo-rpc-api这个jar包中配置了如下两个扩展点实现:
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
那么当获取名为dubbo的这个扩展点实现单例的时候,ExtensionLoader首先会创建DubboProtocol对象,然后用
ProtocolFilterWrapper和ProtocolListenerWrapper对其进行封装,返回封装之后的对象,实际进行调用的时候,调用顺序是这样的:
ProtocolListenerWrapper => ProtocolFilterWrapper => DubboProtocol
dubbo对扩展点实现进行封装的代码如下:
if (wrapperClasses != null && wrapperClasses.size() > 0) {
for (Class> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
dubbo以这种形式实现了扩展点的代理和AOP机制。
前面说dubbo的ExtensionLoader有点类似spring容器,是一个专门管理扩展点的容器,那么依赖注入功能肯定是少不了的,不然如果不同的扩展点有相互依赖的,还得自己拿到一个个扩展点,去手动注入,太麻烦了。dubbo也替我们想到了这一点,它能够对扩展点实现进行依赖注入。其实现原理很简单,代码如下:
private T injectExtension(T instance) {
try {
if (objectFactory != null) {//ExtensionFactory自己不需要这个逻辑
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
//FIXME 通过SpiExtensionFactory获取adaptiveExtension add by jileng
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
一句话概括就是遍历set方法,获取参数的类型和名字,然后以此在容器中寻找这样的扩展点。
不过具体的寻找依赖的工作是由objectFactory成员完成的,它的声明如下
private final ExtensionFactory objectFactory;
ExtensionFactory也是一个扩展点:
@SPI
public interface ExtensionFactory {
/**
* Get extension.
*
* @param type object type.
* @param name object name.
* @return object instance.
*/
T getExtension(Class type, String name);
}
那ExtensionFactory是怎么寻找依赖的呢?其实ExtensionFactory有两个扩展点实现SpiExtensionFactory和SpringExtensionFactory。SpiExtensionFactory负责寻找名称是name类型是type的扩展点实现,而SpringExtensionFactory负责在spring容器中寻找名称为name的bean。而AdaptiveExtensionFactory是ExtensionFactory的Adaptive实现,它负责协调其它实现扩展点实现来完成工作。下面会介绍Adaptive扩展点实现的作用。
上文说了,SpiExtensionFactory会寻找扩展点实现作为依赖注入的对象,但是如果扩展点有多个实现,那么该选哪一个实现呢?换个方式提问,既然有多个扩展点实现,那么在引用扩展点的时候怎么决定用哪一个实现呢?如果写死的话,也就失去了扩展点提供的灵活性。dubbo使用Adaptive扩展点解决这个问题。
下面的代码是SpiExtensionFactory寻找依赖的逻辑:
public class SpiExtensionFactory implements ExtensionFactory {
public T getExtension(Class type, String name) {
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
ExtensionLoader loader = ExtensionLoader.getExtensionLoader(type);
if (loader.getSupportedExtensions().size() > 0) {
//返回Adaptive Extension
return loader.getAdaptiveExtension();
}
}
return null;
}
}
可见ExtensionLoader注入的依赖扩展点是一个Adaptive实例。Adaptive实例也是一个扩展点实现,但是它本身不干活,它的作用是根据URL(Dubbo使用URL对象传递配置信息)动态地决定由哪个具体的扩展点实现来干活,然后把工作代理给真正干活的实现。Adaptive扩展点可以用@Adaptive注解来显式声明,每一个扩展点最多只能有一个显式声明的Adaptive扩展点。如果没有显式声明Adaptive扩展点怎么办呢?答案是动态生成一个!如下面的代码所示:
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();
return compiler.compile(code, classLoader);
}
其中createAdaptiveExtensionClassCode方法是用来生成源代码的,实现代码比较复杂,但是逻辑却很简单,总结起来就是:为扩展点接口的每一个带有@Adaptive注解的方法生成一个方法体,方法体的逻辑是找到参数中类型是URL的对象或者参数中的URL成员,然后根据方法@Adaptive注解上声明的key,去url对象里面找对应的value,这个value就是真正干活的扩展点实现的名称,然后从ExtensionLoader中获取这个扩展点实现,把工作代理给它。下面是一个例子,便于理解:
public interface Transporter {
@Adaptive({"server", "transport"})
Server bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({"client", "transport"})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
对于bind方法表示,Adaptive实现先查找”server”key,如果该Key没有值则找”transport”key值,来决定代理到哪个实际扩展点。
dubbo其实是一个微内核的架构,很多功能都是通过扩展点来实现的,业务方可以方便的添加自己想要的功能,比如打印rpc日志、上报rt监控等。我们自己在写代码尤其是框架性的代码时,可以借鉴下dubbo的思路。