在上篇文章,我们了解了Dubbo 服务端的暴露流程,在最后,提出关于一行代码的疑问
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
不管是暴露到本地,还是暴露到远程,最后都是调用的**protocol.export,**但是发现不同时候具体的protocol 是不一样的,今天我们就来了解背后的逻辑,这里牵涉出了dubbo 的扩展机制。
可以参考官网的文章: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 serviceLoader = ServiceLoader.load(IRepository.class);
Iterator 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的ExtentionLoader加载。如果没有此ExtensionLoader调用会异常。
@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。方法内部会根据方法的参数,来决定使用哪个扩展。 @Adaptive注解用在类上代表实现一个装饰类,类似于设计模式中的装饰模式,它主要作用是返回指定类,目前在整个系统中AdaptiveCompiler、AdaptiveExtensionFactory这两个类拥有该注解。
类似于Java SPI的ServiceLoader,负责扩展的加载和生命周期维护。
和Java SPI不同,Dubbo中的扩展都有一个别名,用于在应用中引用它们。比如
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
registry=org.apache.dubbo.registry.integration.RegistryProtocol
injvm=org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol
其中的dubbo,registry,injvm就是对应扩展的别名。这样我们在配置文件中使用dubbo或registry就可以了。
和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")
Invoker select(List> 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 parameters
字段,parameters中就包含了loadbalance。Dubbo中获取LoadBalance的代码如下:
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);
使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法获取一个ExtensionLoader的实例,然后调用getExtension,传入一个扩展的别名来获取对应的扩展实例。
前面的内容基本上都是官网的内容,我觉得基本上还是说得很清楚,下面我们在上一篇文章(服务暴露)的基础上扩展和深入,加深理解。
在前面服务暴露过程中有这么一段代码:
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) {
// 省略无关代码
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
// 加载 ConfiguratorFactory,并生成 Configurator 实例,然后通过实例配置 url
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
String scope = url.getParameter(Constants.SCOPE_KEY);
// 如果 scope = none,则什么都不做
if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
// scope != remote,导出到本地
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
// scope != local,导出到远程
if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
//暂时先省略
}
}
this.urls.add(url);
}
我们还是来看如何把服务导出到本地的:exportLocal方法。
private void exportLocal(URL url) {
URL local = URLBuilder.from(url)
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
Invoker invoker=PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local);
Exporter> exporter = protocol.export(invoker);
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}
getInvoker方法我们暂不关注。
@SPI("dubbo")
public interface Protocol {
@Adaptive
Exporter export(Invoker invoker) throws RpcException;
// ...
}
我们看到export 方法被@Adaptive
标注,那么这个会从invoker中获取URL,然后在从URL 中获取protocol 类型,取参数这个过程是被dubbo 代理了,这里的protocol是P``rotocol$Adaptive
,这个也是自动生成的,这里的话,贴一下生成的代码(去掉了一些异常判断),具体的不多讲,更多的可以看一下ExtensionLoader
相关的方法。
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
//从 Invoker 中获取URL
org.apache.dubbo.common.URL url = arg0.getUrl();
//获取扩展名
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
//加载扩展类
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
// export 委托给具体的扩展类
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());
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);
}
public void destroy() {
throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
public int getDefaultPort() {
throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
}
相关的注释写在代码里面了,很简单,下面我们关注protocol.export,我们看一下exportLocal中invoker的URL信息。
下面是我本机测试时候的URL:
injvm://127.0.0.1/org.apache.dubbo.demo.DemoService?
anyhost=true&application=dubbo-demo-annotation-provider&
bean.name=ServiceBean:org.apache.dubbo.demo.DemoService&
bind.ip=192.168.0.58&bind.port=20880&deprecated=false&dubbo=2.0.2&
dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&
methods=sayHello,sayOK&pid=14948&release=&
side=provider×tamp=1575448906108
这里的protocol 就是injvm,暴露到本地,也就是jvm 中,既然如此,那么我们就到protocol 的子类InjvmProtocol
看看:
public class InjvmProtocol extends AbstractProtocol implements Protocol {
public static final String NAME = LOCAL_PROTOCOL;
public static final int DEFAULT_PORT = 0;
private static InjvmProtocol INSTANCE;
public InjvmProtocol() {
INSTANCE = this;
}
public static InjvmProtocol getInjvmProtocol() {
if (INSTANCE == null) {
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(InjvmProtocol.NAME); // load
}
return INSTANCE;
}
@Override
public Exporter export(Invoker invoker) throws RpcException {
// 返回了 InjvmExporter
return new InjvmExporter(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
// 省略其它方法
}
看到这里应该就很明白了,通过我们的URL 获取到具体的Protocol,然后再委托给具体的Protocol 实现类来做导出任务。
下面我们做个简单的小调整,来扩展一下。
private void exportLocal(URL url) {
URL local = URLBuilder.from(url)
// 我们把protocol 改成hello
.setProtocol("hello")
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
Invoker invoker=PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local);
Exporter> exporter = protocol.export(invoker);
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}
接下来,我们定一个HelloProtocol:
public class HelloProtocol extends AbstractProtocol implements Protocol {
public static final String NAME = "hello";
public static final int DEFAULT_PORT = 0;
private static HelloProtocol INSTANCE;
public HelloProtocol() {
INSTANCE = this;
}
public static HelloProtocol getInjvmProtocol() {
if (INSTANCE == null) {
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(HelloProtocol.NAME); // load
}
return INSTANCE;
}
@Override
public Exporter export(Invoker invoker) throws RpcException {
//这里我们可以自定义
System.out.println("this is hello protol,哈哈哈哈哈哈哈哈");
return new InjvmExporter(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
//其它省略
}
这样在导出服务到本地的时候,就会加载 HelloProtocol
,可以自己多多尝试,加深理解。
同样的,在导出到远程的时候,我们查看一下URL,协议头是registry.
registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?
application=dubbo-demo-annotation-provider&dubbo=2.0.2&
export=dubbo%3A%2F%2F192.168.0.57%3A20880%2Forg.apache.dubbo.demo.DemoService%3F
anyhost%3Dtrue%26application%3Ddubbo-demo-annotation-provider%26
bean.name%3DServiceBean%3Aorg.apache.dubbo.demo.DemoService%26
bind.ip%3D192.168.0.57%26bind.port%3D20880%26deprecated%3Dfalse%26
dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26
interface%3Dorg.apache.dubbo.demo.DemoService%26
methods%3DsayHello%2CsayOK%26pid%3D18661%26release%3D%26
side%3Dprovider%26timestamp%3D1575118185436&pid=18661&
registry=zookeeper×tamp=1575118181384
那么我们就可以去看 dubbo 中关于registry的配置,这样我们就可以方便的调试了,同时在上篇文章中,我们是跳过了这部分的介绍,直接定位到RegistryProtocol
中的代码。这里我们就和前面衔接起来了。
registry=org.apache.dubbo.registry.integration.RegistryProtocol
这样通过dubbo 的SPI 机制可以很方便的进行扩展,同时对我们自身来说,也是一个学习的过程,这样我们通过SPI机制,可以实现AOP的一写功能。
到此,我们从Java SPI开始,了解了Dubbo SPI 的基本概念,并结合了Dubbo中的LoadBalance加深了理解。最后,我们还实践了一下,创建了一个自定义LoadBalance,并集成到Dubbo中。相信通过这里理论和实践的结合,大家对Dubbo的可扩展有更深入的理解。 总结一下,Dubbo SPI有以下的特点:
文章中间部分和总结,使用的是官方文档中的描述。
Dubbo可扩展机制实战