Spring解析并注册Dubbo consumer端过程详解

551 阅读11分钟

这里debug的代码用的是github上dubbo项目的dubbo-demo里的dubbo-demo-xml下的代码。这里以默认的dubbo通信协议为debug的代码,由于这一篇都是dubbo框架内部实现,所以建议先看下dubbo官网上的一篇讲dubbo的设计原则的文章,有助于理解代码,这里先贴出dubbo官网上的架构图,然后我们在看代码时,比对架构图一起分析: 在这里插入图片描述

下面是架构图中的各层说明:

  • config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类
  • proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory
  • registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService
  • cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance
  • monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService
  • protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter
  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
  • transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
  • serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool

demo里的consumer端的xml配置:

<beans 
       // xmlns:xsi是xsi标签命名空间
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       // xmlns:dubbo是dubbo标签的命名空间
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       // 当前那xml文件默认的命名空间
       xmlns="http://www.springframework.org/schema/beans"
       // xsi:schemaLocation 配置了每个命名空间对应里配置规范,用来做格式校验
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <dubbo:application name="demo-consumer"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181" timeout="6000"/>
    
    <!--    协议配置-->
    <dubbo:protocol name="dubbo"/>
    
    <!--    consumer配置-->
    <dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService" timeout="6000" />

</beans>

1、将dubbo:reference配置解析成BeanDefinition

Dubbo的consumer配置是<dubbo:reference>,在《spring解析dubbo标签配置过程》中可以看到这个配置被解析成BeanDefinition类的过程,时序图如下: 在这里插入图片描述

2、通过BeanDefinition实例化ReferenceConfig对象

BeanDefinition的BeanClass属性值为org.apache.dubbo.config.spring.ReferenceConfig.class类,该类实现了FactoryBean接口(FactoryBean接口可以说就是为了这种通过动态代理技术生成bean的场景设计的,Spring在注入时,会注入该对象的getObject()方法返回真正的实例)。getBean()详细逻辑见这里,debug截图如下: 在这里插入图片描述

3、依赖注入时,触发ReferenceConfig这个FactoryBeangetObject()调用,返回代理类

Spring在依赖注入时也会调用getBean(String beanName)方法来获取已经实例化的bean,通过beanName会获取第二步中实例化的ReferenceConfig对象,而该对象是BeanFactory,所以最终会调用getObject()方法,debug截图如下: 在这里插入图片描述

getObject()截图如下: 在这里插入图片描述

getObject()代码如下,我这里省略了部分分支代码:

public Object getObject() {
        return this.get();
}

public synchronized T get() {
    ... 
    this.init();
    ...
    return this.ref;
}

public synchronized void init() {
    // 初始化dubbo启动类
    if (this.bootstrap == null) {
        this.bootstrap = DubboBootstrap.getInstance();
        this.bootstrap.init();
    }
    
    ...
    
    // map保存所有创建consumer代理类的相关配置信息
    Map<String, String> map = new HashMap();
    map.put("side", "consumer");
    // 往map中插入dubbo版本号、时间戳、进程pid运行时信息
    ReferenceConfigBase.appendRuntimeParameters(map);
    ...
    
    // 传入map,创建代理类
    this.ref = this.createProxy(map);
    ...
}
  
private T createProxy(Map<String, String> map) {
    ...
    // 检查注册中心配置,并根据注册中心配置生成注册中心的URL
    this.checkRegistry();
    List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
    if (CollectionUtils.isNotEmpty(us)) {
        for(Iterator var3 = us.iterator(); var3.hasNext(); this.urls.add(u.addParameterAndEncoded("refer", StringUtils.toQueryString(map)))) {
            u = (URL)var3.next();
            monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
            if (monitorUrl != null) {
                map.put("monitor", URL.encode(monitorUrl.toFullString()));
            }
        }
    }
    ...
    // 构建invoker链
    this.invoker = REF_PROTOCOL.refer(this.interfaceClass, (URL)this.urls.get(0));
    ...        
    // 通过ProxyFactory创建代理
    return PROXY_FACTORY.getProxy(this.invoker);
}

下面是结合dubbo架构图画的主要步骤的时序图: 在这里插入图片描述

3.1 初始化Dubbo启动类DubboBootstrap

这个DubboBootstrap是dubbo的启动类,类似于springbootSpringApplication类,存储了整个dubbo环节的各种配置信息,负责provider的服务暴露及consumer的服务订阅、维护dubbo容器的生命周期,这里先跳过,继续研究我们的consumer实例化过程

3.2 构建注册中心对象

代码在createProxy(Map<String, String> map)方法里,核心代码如下

this.checkRegistry();
List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
if (CollectionUtils.isNotEmpty(us)) {
// 获取注册中心的URL,并将当前consumer配置的元数据信息放到URL的Parameter属性里
    for(Iterator var3 = us.iterator(); var3.hasNext(); this.urls.add(u.addParameterAndEncoded("refer", StringUtils.toQueryString(map)))) {
        ...
    }
}

<dubbo:registry>配置中心对应的bean是RegistryConfig.class,解析过程和<dubbo:reference>一样,被解析出来的配置都会放到一个全局的ConfigManager中,这里会从这个ConfigManager中读取注册中心配置。dubbo里的常见的配置都会缓存起来,在《spring的xml文件里dubbo标签解析过程》里可以看到负责解析dubbo自定义标签的是DubboNamespaceHandler,每种配置对应的实体类如下: 在这里插入图片描述 这些类都继承了AbstractConfig类,这个抽象类有个被@PostConstruct注解标注的addIntoConfigManager()方法(该注解会告诉Spring容器在实例化该对象后,会执行注解标注的方法),会将当前对象缓存到一个全局的ConfigManager对象中:

    @PostConstruct
    public void addIntoConfigManager() {
        ApplicationModel.getConfigManager().addConfig(this);
    }

Url可以说是dubbo主链路里分量非常重的一个参数封装类,在主链路里封装参数一路传递,这里就是讲注册中心的元数据与consumer的元数据封装到一个URL中,传递到下游的方法里,主要存储一些元数据信息,这里就封装了注册中心地址、consumer的接口、方法等信息,debug截图如下: 在这里插入图片描述

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

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

这里要先讲下SPI扩展技术:

dubbo通过SPI技术来扩展相关组件,上面代码中的REF_PROTOCOL对象就是通过SPI来扩展的,原理就是找到META-INF/dubbo/internal下配置的扩展类,比如我下面的截图: 在这里插入图片描述

从这里可以看出Protocol.class协议扩展有很多种,那么最终调用哪一个实现类来执行refer方法呢?关键是在getAdaptiveExtension(),这个方法返回的是一个适配器类,这个适配器的refer方法会根据url里的protol协议来调用getExtension(extName),比如以registry://开头的url需要用注册中心的协议实现类来执行,我贴出这个类的代码:

public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
      ...
      // 获取URL中的协议,默认dubbo
        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);
    }

getExtension(extName)方法会返回一个包装类,截图如下: 在这里插入图片描述

可以看出dubbo通过SPI做扩展时,配置文件里的类会分为三种:

  • 适配器类:主要是通过url里的信息来适配出合适的包装类
  • 包装类:会将真正的实现类包装起来,形成一个调用链,这个包装类可以理解成一个代理类,里面可以加上过滤、监听等逻辑,比如截图里的ProtocolListenerWrapper和ProtocolFilterWrapper两个包装类
  • 真正的实现类

而调用链就是 通过适配器类找到对应包装类,然后通过包装类形成一个调用链,最后调用真正的实现类,下面的loadClass方法负责加载META-INF/dubbo/internal下配置的扩展类,可以看出区分三种类的逻辑,截图如下: 在这里插入图片描述

REF_PROTOCOL.refer(this.interfaceClass, (URL)this.urls.get(0))方法,就是调用最外层的包装类ProtocolListenerWrapper的refer方法:

// ProtocolListenerWrapper类的refer方法
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        return (Invoker)(UrlUtils.isRegistry(url) ? this.protocol.refer(type, url) : new ListenerInvokerWrapper(this.protocol.refer(type, url), Collections.unmodifiableList(ExtensionLoader.getExtensionLoader(InvokerListener.class).getActivateExtension(url, "invoker.listener"))));
    }
    
    // ProtocolFilterWrapper类的refer方法
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        return UrlUtils.isRegistry(url) ? this.protocol.refer(type, url) : buildInvokerChain(this.protocol.refer(type, url), "reference.filter", "consumer");
    }

这里由于是Registry协议,所以直接调用的是RegistryProtocol类的refer方法,主干代码如下:

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        url = this.getRegistryUrl(url);
        // 获取注册中心对象
        Registry registry = this.registryFactory.getRegistry(url);
        ...
        return this.doRefer(this.cluster, registry, type, url);
    }

这里的registryFactory也是通过SPI来扩展的,由于注册中心用的是zookeeper,所以这里的registry对象是ZookeeperRegistry,堆栈截图如下: 在这里插入图片描述

3.3 构建服务发现对象

每一个配置的Consumer都会对应一个服务发现对象,服务发现的主要功能是监听注册中心的该接口下的节点变化,包括服务提供方、路由配置等信息在节点发生变化时,通知注册中心更新本地的配置信息。主干代码如下:

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
        // 构建服务发现对象
        RegistryDirectory<T> directory = new RegistryDirectory(type, url);
        directory.setRegistry(registry);
        directory.setProtocol(this.protocol);
        ... 
        
        // 通过SPI扩展机制 设置服务发现的默认配置的路由链
        directory.buildRouterChain(subscribeUrl);
      
        ...
    }

通过SPI扩展配置构建路由链,并保存到服务发现对象中:

public void buildRouterChain(URL url) {
        this.setRouterChain(RouterChain.buildChain(url));
    }
private RouterChain(URL url) {
        List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class).getActivateExtension(url, "router");
        List<Router> routers = (List)extensionFactories.stream().map((factory) -> {
            return factory.getRouter(url);
        }).collect(Collectors.toList());
        this.initWithRouters(routers);
    }

3.4 将Consumer信息发布到注册中心

还是在doRefer方法,删除次要代码后如下:

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
        ...
        if (!"*".equals(url.getServiceInterface()) && url.getParameter("register", true)) {
            directory.setRegisteredConsumerUrl(this.getRegisteredConsumerUrl(subscribeUrl, url));
            // 将Consumer信息发布到注册中心
            registry.register(directory.getRegisteredConsumerUrl());
        }
        ...
    }

registry.register(directory.getRegisteredConsumerUrl());会调用zookeeper的客户端框架Curator将consumser信息发布到zookeeper上,在zookeeper的 /dubbo/org.apache.dubbo.demo.DemoService/consumers目录下写入信息: 在这里插入图片描述

3.5 通过服务发现对象订阅并获取注册中心里的 服务提供方、配置、路由信息,更新服务发现对象的本地配置,构建invoker链

还是在doRefer方法,删除次要代码后如下:


private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
        RegistryDirectory<T> directory = new RegistryDirectory(type, url);
        directory.setRegistry(registry);
        directory.setProtocol(this.protocol);
        ...
        // 订阅节点信息,并根据注册中心的节点下的配置信息更新 服务发现directory对象里的配置
        directory.subscribe(subscribeUrl.addParameter("category", "providers,configurators,routers"));
        
        ...
}

public void subscribe(URL url) {
        this.setConsumerUrl(url);
        CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(this);
        this.serviceConfigurationListener = new RegistryDirectory.ReferenceConfigurationListener(this, url);
        // 将当前registryDirectory对象作为Listener监听节点变化
        this.registry.subscribe(url, this);
}

3.5.1 订阅注册中心的providers,configurators,routers节点

这个是服务发现的核心流程,通过zookeeper的客户端框架Curator来watch zookeeper上providers,configurators,routers的节点信息,并在zookeeper节点信息发生变化时,会通知到监听器,RegistryDirectory类实现了NotifyListener类,会作为监听器监听服务提供方节点的变化,当注册中心的providers,configurators,routers这三个节点发生变化时,会回调这个监听器的NotifyListener类的notify(List urls)方法。

3.5.2 获取节点配置,更新本地缓存的provider列表,与provider建立长连接

服务提供发生变化时,zookeeper的providers节点内容就会发生变化,会调用ZookeeperRegistrynotify(List urls) 方法来更新privider列表,由于这里是初次订阅注册中心,所以也会主动触发一次notify(List)方法,主干代码如下:

public synchronized void notify(List<URL> urls) {
        ...
        // 通过回调中的urls获取服务提供方信息
        List<URL> providerURLs = (List)urls.getOrDefault("providers", Collections.emptyList());
        // 更新本地服务提供方列表
        this.refreshOverrideAndInvoker(providerURLs);
    }

url信息里包含了服务提供方的元数据信息,debug截图如下: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8GSaWVyR-1617207225262)(quiver-image-url/763A6034B1F510D4BFC30456997D0C74.jpg =1862x631)] 这里主要有两步:

  1. 将URL信息解析成一个被层层包装的Invoker链,最里层是DubboInvoker.

每一层包装都是为了添加新特性,其中重要的有增加了异步支持的AsyncToSyncInvoker,consumer就是在AsyncToSyncInvoker的invoke方法里阻塞等待provider返回结果,在这一步里形成的Invoker包装链如下: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkF52hWn-1617207225279)(quiver-image-url/261433A85D0D9CA4D4B9408679A5AC82.jpg =1000x262)]

  1. 创建NettyClient,与provider建立长连接,通过DubboInvoker构造方法可以看出需要封装一个ExchangeClient,该对象负责网络通信,默认使用Netty框架。

构造方法代码如下:

DubboInvoker<T> invoker = new DubboInvoker(serviceType, url, this.getClients(url), this.invokers);

时序图如下: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-18PRXzMP-1617207225281)(quiver-image-url/30A108116061A9E1BC10B8BF7D53627E.jpg =1188x750)]

注册中心回调完整时序图如下: 在这里插入图片描述

3.6、通过Cluster对象对Invoker进行再次包装,添加集群特性,包括故障处理、负载均衡、路由逻辑

Invoker invoker = cluster.join(directory);

这里的cluster也是通过SPI来扩展的,我们看下dubbo里对cluster的分类就能明白抽象出这一层的意义: 在这里插入图片描述 从名字上可以看出有failover(故障转移)、failfast(快速失败)等,所以解Cluster可以理是抽象了对于失败情况的不同处理机制。默认是failover机制,最终调用的是FailoverCluster,debug截图如下: 在这里插入图片描述

在3.6里的invoker链的基础上经过这一层包装后,invoker链图如下: 在这里插入图片描述

3.7 将invoker链包装成代理类,返回给getObject()方法

private static final ProxyFactory PROXY_FACTORY = (ProxyFactory)ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

PROXY_FACTORY.getProxy(this.invoker);

从代码可以看出ProxyFactory也是通过SPI来扩展,默认使用JavassistProxyFactoryJavassistProxyFactory.getProxy代码如下:

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

这里将上面形成的invoker链包装成InvokerInvocationHandler.class,我们先看下生成的代理类被反编译后的源码:

public class proxy0
implements ClassGenerator.DC,
Destroyable,
EchoService,
DemoService {
    public static Method[] methods;
    private InvocationHandler handler;

    @Override
    public Object $echo(Object object) {
        Object[] objectArray = new Object[]{object};
        Object object2 = this.handler.invoke(this, methods[0], objectArray);
        return object2;
    }

    public CompletableFuture sayHelloAsync(String string) {
        Object[] objectArray = new Object[]{string};
        Object object = this.handler.invoke(this, methods[1], objectArray);
        return (CompletableFuture)object;
    }

    public String sayHello(String string) {
        Object[] objectArray = new Object[]{string};
        Object object = this.handler.invoke(this, methods[2], objectArray);
        return (String)object;
    }

    @Override
    public void $destroy() {
        Object[] objectArray = new Object[]{};
        Object object = this.handler.invoke(this, methods[3], objectArray);
    }

    public proxy0() {
    }

    public proxy0(InvocationHandler invocationHandler) {
        this.handler = invocationHandler;
    }
}

可以看出我们在调用demoService.sayHello(String string)方法时,实际就是调用InvokerInvocationHandler的invoke方法,而该类的invoke方法如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(this.invoker, args);
        } else {
            String methodName = method.getName();
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 0) {
                if ("toString".equals(methodName)) {
                    return this.invoker.toString();
                }

                if ("$destroy".equals(methodName)) {
                    this.invoker.destroy();
                    return null;
                }

                if ("hashCode".equals(methodName)) {
                    return this.invoker.hashCode();
                }
            } else if (parameterTypes.length == 1 && "equals".equals(methodName)) {
                return this.invoker.equals(args[0]);
            }
            // 构造RpcInvocation信息,分装了接口、方法、参数等元数据信息
            RpcInvocation rpcInvocation = new RpcInvocation(method, this.invoker.getInterface().getName(), args);
            rpcInvocation.setTargetServiceUniqueName(this.invoker.getUrl().getServiceKey());
            // 调用invoker链
            return this.invoker.invoke(rpcInvocation).recreate();
        }
    }

可以看出主要逻辑就是将被调用的类、方法、参数这些元数据信息封装成RpcInvocation对象,调用之前生成的invoker链的invoke方法,这样就串起来整个调用。 到这里代理类就已经创建完毕,最终返回给getObject()方法的就是一个代理类。