Dubbo调用方refer过程

1,125 阅读5分钟

一、引入

本文引用dubbo的源代码版本为2.7.8-release。

主要以客户端视角来分析dubbo获取服务接口代理类的过程。

首先看一下消费端引用并调用服务接口的代码:

@Test
public void test() {
    ApplicationConfig application = new ApplicationConfig();
    application.setName("test-reference-retry");
    RegistryConfig registry = new RegistryConfig();
    registry.setAddress("multicast://224.5.6.7:1234");
    ProtocolConfig protocol = new ProtocolConfig();
    protocol.setName("mockprotocol"); 
    
    ReferenceConfig<DemoService> rc = new ReferenceConfig<DemoService>();
    rc.setApplication(application);
    rc.setRegistry(registry);
    rc.setInterface(DemoService.class.getName());
    // 获取服务接口
    DemoService demoService = rc.get();
    
    // 获取接口实例后,可以进行调用
    // 略
}

在上面的代码中,最关键的点就是通过rc.get()获取服务接口的实例,这篇文章主要的内容也是对其进行解读分析。在此之前,可以简单的考虑下,如果让你用一种较简单的方式来实现ReferenceConfig的get方法,你会怎么实现?

这里讨论服务的接口是一个远程的提供者提供的,且接口的实现类的逻辑也是在远端,所以不能通过常规的初始化方式来获取到接口的实例。既然要和远端的服务进行通信,就需要通过网络进行数据传输。下面是通过jdk动态代理和最基本的网络通信模型实现的一个简单案例:


public class RefInvocationHandler implements InvocationHandler {
    private Class clazz;
    
    // 远程主机
    private String host;
    // 远程主机的端口
    private int port;
    
    public RefInvocationHandler(Class clazz, String host, int port) {
        this.clazz = clazz;
        this.port = port;
        this.host = host;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 远程连接
        Socket socket = new Socket(host, port);
        // 然后通过socket将接口名称,方法名称,参数传给服务提供方
        // 在这里,先简单的这样传输...
        ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
        output.writeUTF(method.getName());
        output.writeObject(clazz.getName());
        output.writeObject(method.getParameterTypes());
        output.writeObject(arguments);
        
        // 等待服务提供方计算结果后协会
        ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
        Object result = input.readObject();
        
        // 省略了流的关闭等细节
        // ....
        
        return result;
    }
}


// get
public <T> T get() {
    return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new RefInvocationHandler(invoker));
}

与动态代理本地已有的实现类最大的区别在,本地已有实现类会将代理的对象实例化,然后在调用代理实例时触发invoke调用中使用method.invoke(proxy, args)来调用被代理的实例方法,而在远程调用这里,则是将接口名、方法名、参数传输给服务提供方。

当然,上面这种实现的方式只是一个最简单粗糙的实现,只是为了引出一个大致的实现方向,下面来看看dubbo是怎么实现的。

二、ReferenceConfig代码分析

2.1 init()

1.如下图,get方法比较简单,具体的实现跟进init()。

dubbo-refer-01.png

2.如下为整个init()方法代码

dubbo-refer-02.png

此方法的代码较长,在这里主要关注的是上面红色框里的创建代理类createProxy的方法,在此之前的代码,主要包括了两个部分:

  • 第一部分为上图代码中绿色框,主要是做初始化及一些检查
  • 第二部分为绿色框与红色框之间的区域,主要作用是构造入参map,此map作为参数在构造代理对象时使用

2.2 createProxy(Map<String, String> map)

dubbo-refer-03.png

如上,从createProxy方法的代码实现中可以明确的看出有3种创建代理类的场景,也就是injvm、通过指定url端对端、通过注册中心三种场景。下面主要关注通过单注册中心的方式创建的过程:

dubbo-refer-04.png

如上图黄色框中,通过注册中心创建的过程代码包括了对注册中心配置的检查以及一些附加参数的添加等细节,最后会将注册中心的url添加到urls中,然后在红色框中的代码中创建invoker,如下:

private static final Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));

注意到在这里会使用dubbo的SPI扩展自适应获取Protocol的实现类,而refer方法是被注解@Adaptive修饰的,如下图:

dubbo-refer-05.png

那么这里选择Protocol的实现类时是根据URL的protocol来进行选择,这里的URL为注册中心的url,也就是形如registry://...,因此会选择RegistryProtocol去进行调用(选择的细节可参考Dubbo SPI实现相关资料)。

那接下来就是,查看RegistryProtocol的refer方法的实现。

2.3 RegistryProtocol

dubbo-refer-06.png

RegistryProtocol的refer代码的内容比较简洁,首先会判断要代理的类是不是注册中心的实现,以及配置了group的实现,在这里我们重点关注最后两行的实现。

1)先来看Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY))的实现:

dubbo-refer-07.png

通过dubbo spi实现机制可知,在这里会获取到Cluster的包装类实例MockClusterWrapper,MockClusterWrapper包含了FailoverCluster的实例。

2)再来看doRefer的实现

dubbo-refer-08.png

在doRefer方法中会去注册中心订阅,此处不展开了。我们聚焦在生成Invoker的实现,由上面可知此处的cluster为MockClusterWrapper的实例,具体实现如下:

由此可见,invoker最终会返回MockClusterInvoker的实例。

关于生成MockClusterInvoker具体的细节,包装类MockClusterWrapper的属性cluster为FailoverCluster,可以跟进下this.cluster.join(direcroty)方法,此处不再继续展开。

最终,MockClusterInvoker会持有FailoverClusterInvoker的实例。

2.4 ReferenceConfig#createProxy

dubbo-refer-10.png

通过ProxyFactory的实现类来完成动态代理类的生成,有javassist和jdk两种生成方式,可以通过proxy参数进行配置或设置,默认是使用javassist的方式生成,最终使用InvokerInvocationHandler来持有invoker。

dubbo-refer-11.png

三 、相关配置

下面介绍通过调用方配置来分别进行3种不同类型的调用方式。

1.本地调用

// xml
<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService"  injvm="true" .../>

// 配置类
ReferenceConfig.setInjvm(Boolean.True);

2.点对点直连

// xml
<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />

// 配置类
ReferenceConfig.setUrl("xxxx");

通过配置url来直连提供者,多个用逗号隔开。点对点直连常用与开发、测试环境联调及相关问题排查。

3.注册中心

没有配置本地调用和点对点直连的url,会检查注册中心配置,通过注册中心来获取服务信息,也就是常用的场景。

// xml
<dubbo:registry id="rid" address="multicast://xx.x.x.x:1234" />
<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService"  registry="rid" />

// 配置类
RegistryConfig registry = new RegistryConfig();
registry.setAddress("multicast://xx.x.x.x:1234");
ReferenceConfig.setRegistry(registry);

四、总结

本文对Dubbo客户端引用服务接口的源码进行解读,可以通过引用的方式,injvm、直连、注册中心三种方式进行服务引用,最终是通过动态代理的方式持有Invoker的某个实例,如在此文中的MockClusterInvoker实例。此时,我们已对客户端获取服务接口的代理类进行了解,由此我们可以以InvokerInvocationHandler的实现为入口进一步了解调用的过程,从而对负载均衡、集群容错、线程模型、通信模型等dubbo的特性进行学习。