该文章内容发布已经超过一年,请注意检查文章中内容是否过时。
在Dubbo的官网上,Dubbo描述自己是一个高性能的RPC框架。今天我想聊聊Dubbo的另一个很棒的特性, 就是它的可扩展性。 如同罗马不是一天建成的,任何系统都一定是从小系统不断发展成为大系统的,想要从一开始就把系统设计的足够完善是不可能的,相反的,我们应该关注当下的需求,然后再不断地对系统进行迭代。在代码层面,要求我们适当的对关注点进行抽象和隔离,在软件不断添加功能和特性时,依然能保持良好的结构和可维护性,同时允许第三方开发者对其功能进行扩展。在某些时候,软件设计者对扩展性的追求甚至超过了性能。
在谈到软件设计时,可扩展性一直被谈起,那到底什么才是可扩展性,什么样的框架才算有良好的可扩展性呢?它必须要做到以下两点:
Dubbo很好的做到了上面两点。这要得益于Dubbo的微内核+插件的机制。接下来的章节中我们会慢慢揭开Dubbo扩展机制的神秘面纱。
通常可扩展的实现有下面几种:
Dubbo作为一个框架,不希望强依赖其他的IoC容器,比如Spring,Guice。OSGI也是一个很重的实现,不适合Dubbo。最终Dubbo的实现参考了Java原生的SPI机制,但对其进行了一些扩展,以满足Dubbo的需求。
既然Dubbo的扩展机制是基于Java原生的SPI机制,那么我们就先来了解下Java SPI吧。了解了Java的SPI,也就是对Dubbo的扩展机制有一个基本的了解。如果对Java SPI比较了解的同学,可以跳过。
Java SPI(Service Provider Interface)是JDK内置的一种动态加载扩展点的实现。在ClassPath的META-INF/services
目录下放置一个与接口同名的文本文件,文件的内容为接口的实现类,多个实现类用换行符分隔。JDK中使用java.util.ServiceLoader
来加载具体的实现。
让我们通过一个简单的例子,来看看Java SPI是如何工作的。
public interface IRepository {
void save(String data);
}
public class MysqlRepository implements IRepository {
public void save(String data) {
System.out.println("Save " + data + " to Mysql");
}
}
public class MongoRepository implements IRepository {
public void save(String data) {
System.out.println("Save " + data + " to Mongo");
}
}
META-INF/services
目录添加一个文件,文件名和接口全名称相同,所以文件是META-INF/services/com.demo.IRepository
。文件内容为:com.demo.MongoRepository
com.demo.MysqlRepository
ServiceLoader<IRepository> serviceLoader = ServiceLoader.load(IRepository.class);
Iterator<IRepository> it = serviceLoader.iterator();
while (it != null && it.hasNext()){
IRepository demoService = it.next();
System.out.println("class:" + demoService.getClass().getName());
demoService.save("tom");
}
在上面的例子中,我们定义了一个扩展点和它的两个实现。在ClassPath中添加了扩展的配置文件,最后使用ServiceLoader来加载所有的扩展点。 最终的输出结果为: class:testDubbo.MongoRepository Save tom to Mongo class:testDubbo.MysqlRepository Save tom to Mysql
Java SPI的使用很简单。也做到了基本的加载扩展点的功能。但Java SPI有以下的不足:
所以Java SPI应付一些简单的场景是可以的,但对于Dubbo,它的功能还是比较弱的。Dubbo对原生SPI机制进行了一些扩展。接下来,我们就更深入地了解下Dubbo的SPI机制。
在深入学习Dubbo的扩展机制之前,我们先明确Dubbo SPI中的一些基本概念。在接下来的内容中,我们会多次用到这些术语。
是一个Java的接口。
扩展点的实现类。
扩展点实现类的实例。
第一次接触这个概念时,可能不太好理解(我第一次也是这样的…)。如果称它为扩展代理类,可能更好理解些。扩展的自适应实例其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。比如一个IRepository的扩展点,有一个save方法。有两个实现MysqlRepository和MongoRepository。IRepository的自适应实例在调用接口方法的时候,会根据save方法中的参数,来决定要调用哪个IRepository的实现。如果方法参数中有repository=mysql,那么就调用MysqlRepository的save方法。如果repository=mongo,就调用MongoRepository的save方法。和面向对象的延迟绑定很类似。为什么Dubbo会引入扩展自适应实例的概念呢?
@SPI注解作用于扩展点的接口上,表明该接口是一个扩展点。可以被Dubbo的ExtensionLoader加载。如果没有此ExtensionLoader调用会异常。
@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。方法内部会根据方法的参数,来决定使用哪个扩展。 @Adaptive注解用在类上代表实现一个装饰类,类似于设计模式中的装饰模式,它主要作用是返回指定类,目前在整个系统中AdaptiveCompiler、AdaptiveExtensionFactory这两个类拥有该注解。
类似于Java SPI的ServiceLoader,负责扩展的加载和生命周期维护。
和Java SPI不同,Dubbo中的扩展都有一个别名,用于在应用中引用它们。比如
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
其中的random,roundrobin就是对应扩展的别名。这样我们在配置文件中使用random或roundrobin就可以了。
和Java SPI从/META-INF/services
目录加载扩展配置类似,Dubbo也会从以下路径去加载扩展配置文件:
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services
在了解了Dubbo的一些基本概念后,让我们一起来看一个Dubbo中实际的扩展点,对这些概念有一个更直观的认识。
我们选择的是Dubbo中的LoadBalance扩展点。Dubbo中的一个服务,通常有多个Provider,consumer调用服务时,需要在多个Provider中选择一个。这就是一个LoadBalance。我们一起来看看在Dubbo中,LoadBalance是如何成为一个扩展点的。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
LoadBalance接口只有一个select方法。select方法从多个invoker中选择其中一个。上面代码中和Dubbo SPI相关的元素有:
RandomLoadBalance.NAME
是一个常量,值是"random",是一个随机负载均衡的实现。
random的定义在配置文件META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.LoadBalance
中:random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
可以看到文件中定义了4个LoadBalance的扩展实现。由于负载均衡的实现不是本次的内容,这里就不过多说明。只用知道Dubbo提供了4种负载均衡的实现,我们可以通过xml文件,properties文件,JVM参数显式的指定一个实现。如果没有,默认使用随机。
loadbalance
表示方法参数中的loadbalance的值作为实际要调用的扩展实例。
但奇怪的是,我们发现select的方法中并没有loadbalance参数,那怎么获取loadbalance的值呢?select方法中还有一个URL类型的参数,Dubbo就是从URL中获取loadbalance的值的。这里涉及到Dubbo的URL总线模式,简单说,URL中包含了RPC调用中的所有参数。URL类中有一个Map<String, String> parameters
字段,parameters中就包含了loadbalance。Dubbo中获取LoadBalance的代码如下:
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);
使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法获取一个ExtensionLoader的实例,然后调用getExtension,传入一个扩展的别名来获取对应的扩展实例。
本节中,我们通过一个简单的例子,来自己实现一个LoadBalance,并把它集成到Dubbo中。我会列出一些关键的步骤和代码,也可以从这个地址(https://github.com/vangoleo/dubbo-spi-demo)下载完整的demo。
首先,编写一个自己实现的LoadBalance,因为是为了演示Dubbo的扩展机制,而不是LoadBalance的实现,所以这里LoadBalance的实现非常简单,选择第一个invoker,并在控制台输出一条日志。
package com.dubbo.spi.demo.consumer;
public class DemoLoadBalance implements LoadBalance {
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
System.out.println("DemoLoadBalance: Select the first invoker...");
return invokers.get(0);
}
}
添加文件:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance
。文件内容如下:
demo=com.dubbo.spi.demo.consumer.DemoLoadBalance
通过上面的两步,已经添加了一个名字为demo的LoadBalance实现,并在配置文件中进行了相应的配置。接下来,需要显式的告诉Dubbo使用demo的负载均衡实现。如果是通过spring的方式使用Dubbo,可以在xml文件中进行设置。
<dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />
在consumer端的dubbo:reference中配置<loadbalance=“demo”>
启动Dubbo,调用一次IHelloService,可以看到控制台会输出一条DemoLoadBalance: Select the first invoker...
日志。说明Dubbo的确是使用了我们自定义的LoadBalance。
到此,我们从Java SPI开始,了解了Dubbo SPI 的基本概念,并结合了Dubbo中的LoadBalance加深了理解。最后,我们还实践了一下,创建了一个自定义LoadBalance,并集成到Dubbo中。相信通过这里理论和实践的结合,大家对Dubbo的可扩展有更深入的理解。 总结一下,Dubbo SPI有以下的特点:
下一篇,我们将会一起深入Dubbo的源码,更深入的了解Dubbo的可扩展机制。