dubbo:虽然Protocol的@SPI指定了‘dubbo’却执行先RegistryProtocol后DubboProtocol

9 篇文章 0 订阅
3 篇文章 0 订阅

目录

总结

Invoker

Protocol

@Adaptive修饰方法来指明需要动态生成代码类来增强指定方法

#export 服务发布过程

#loadRegistries 将协议从'dubbo' 改为'registry'

#doExportUrlsFor1Protocol 执行发布

 #exportLocal 提前生成 'Injvm' 协议发布Local服务供优先本地调用

Protocol$Adaptive#export 增强发布逻辑

RegistryProtocol#export 发布过程

 RegistryProtocol#doLocalExport: 从'registry'协议URL的‘export’部分拿到源'dubbo'协议URL,回调DubboProtocol

DubboProtocol#export 开启网络通信:以[serviceGroup + serviceName + serviceVersion + port] 缓存了Invoker, 以[host:post]为Key缓存了服务端HeaderExchangeServer


总结

  1.  提前默认'Injvm协议会发布Local服务,优先访问本地提供的服务;由URL显示申明'scope = remote'来关闭。
  2. 在DubboProtocol里
    1. Provider侧:
          以[host:post]为Key缓存了服务端HeaderExchangeServer(NettyServer) (这里只可能协议端口port会变,所以通常只有1个NettyServer);以[serviceGroup + serviceName + serviceVersion + port] 缓存Invoker(DubboExporter)。
    2. Consumer侧:单一长连接Rpc客户端
         实际的provider提供的服务地址URL
      1. 没有配置‘connections’ 或 (默认)是0:  将以[host:post]为Key创建并缓存唯一的共享连接客户端ReferenceCountExchangeClient(封装了HeaderExchangeClient,内部引用计数),相同的服务端Provider( host+port )下同此配置的所有服务接口都共用一个客户端连接!
      2. URL配置‘connections’ > 0 , 即使配置为1,该Provider提供的服务接口(service interface)单独有‘connections’个连接客户端HeaderExchangeClient,当dubbo消费者发送请求路由到该Provider时DubboInvoker#doInvoke将轮询使用。
      3. 客户端底层的NettyClient会设置keepAlive。URL有指定'lazy',则延迟创建及connect remote。
  • @SPI:注解指定名称匹配接口的使用实现类。在接口的配置文件里不仅有最终要使用的target对象类,还把【有该接口类型作为入参的构造器】的实现类在创建target对象时当做wrapper裹在外层。
  • @Adaptive: 运行时,对含有被该注解修饰方法的类,用javassist生成并编译增强代码。只有被修饰的方法才被增强! 

所以Protocol实际上是Protocol$Adaptive,在创建target实例时外层用wrapper包裹。最重要的是ProtocolFilterWrapper, 它对DubboProtocol订阅和发布过程的Invoker筛选出合适的Filter来创建过滤器链(以@Activate来指定group是provider | consumer),eg. MonitorFilter、TPSLimiter、ExecuteLimitFilter...

target按逻辑处理先后顺序如下:

  • InjvmProtocol:服务调用会优先走本地
  • RegistryProtocol管理与注册中心的相关操作。 如发布、订阅...   ZookeeperRegistry 向zookeeper写入path
  • DubboProtocol核心底层。开启Provider的NettyServer及Consumer端NettyClient双向通信...

Invoker

Provider侧: Invoker的底层是真实服务的业务处理者。

DubboProtocol#export:

 Consumer侧:Invoker底层是通信客户端 HeaderExchangeClient , 它的底层是由NettyTransporter创建的NettyClient。

 DubboProtocol#refer:

 // 这里的url是到接口层面的:dubbo://127.0.0.1:20880/cn.noob.service.RepayPlanService?anyhost=true&application=noob-core&check=false&default.check=false&dubbo=2.6.2&generic=false&group=noob-rest&interface=cn.noob.service.RepayPlanService&logger=slf4j&methods=listOverdueRepayPlan,checkRepayAmountBeforeSettle,listOverdueRepayPlanByChannelForReconcile,checkRepayPlanBeforeSettle,listRepayPlanByLoanNo,saveBatch,settleRepayPlan,getRepayPlanSizeByLoanNo&pid=12268&register.ip=127.0.0.1&remote.timestamp=1679367745765&retries=-1&revision=1.0&side=consumer&timeout=180000&timestamp=1679367784543&version=1.0
    public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
        optimizeSerialization(url);
        // create rpc invoker.
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);
        return invoker;
    }

// 判定URL的connections > 0 , 则以具体provider的接口服务service为维度单独创建'connections'个连接客户端; 
// 未配置或配置为0则以[host:post]provider服务主机创建(或使用缓存中的)单一共享的连接客户端
private ExchangeClient[] getClients(URL url) {
        // whether to share connection
        boolean service_share_connect = false;
        int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
        // if not configured, connection is shared, otherwise, one connection for one service
        if (connections == 0) {
            service_share_connect = true;
            connections = 1;
        }

        ExchangeClient[] clients = new ExchangeClient[connections];
        for (int i = 0; i < clients.length; i++) {
            if (service_share_connect) {
                clients[i] = getSharedClient(url);
            } else {
                clients[i] = initClient(url);
            }
        }
        return clients;
    }

Protocol

Protocol提供了服务发布#export 及 服务消费订阅#refer两个操作入口。

以服务发布Protocol#export的过程实现为例,讲讲dubbo对 @SPI(载入的接口实现类由文件配置并指定名称使用)和@Adaptive(动态创建并编译增强代码,增强被修饰的方法)。

/** Protocol. (API/SPI, Singleton, ThreadSafe) */
@SPI("dubbo")
public interface Protocol {
  
    int getDefaultPort();

    //服务提供者发布服务, 这里的invoker是serviceImpl的封装代理
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    // 消费者的订阅服务, 这里会绑定通信Client
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    void destroy();
}

图里有2个重要的注解:

  1. 类定义上的@SPI("dubbo") : 结合dubbo对spi的扩展实现,由文件配置的方式来指定加载接口的实体类。 所以是DubboProtocol;
  2. 在#export、#refer方法上使用的@Adaptive : dubbo对有该注解修饰方法的类动态生成增强代码类,只增强被修饰的方法! 

在Provider发布服务的过程里,ServiceBean的父类ServiceConfig里有个静态Protocol对象:

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

配置文件 \META-INF\dubbo\internal\com.alibaba.dubbo.rpc.Protocol

按常规逻辑这里指定的就该是DubboProtocol,但实际上确如下图所示:Protocol$Adaptive!

这也就引出了本文的主题:dubbo会对有@Adaptive修饰方法的类动态生成代码,来增强方法。具体如下:

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

/** 协议自适应类,只增强重写了@Adaptive修饰的方法refer、export */
public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    /** 服务引用 */
    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        // 根据extName找到具体适应类,然后调用方法
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }

    /**  服务暴露 */
    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); // 默认是dubboProtocol!
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        // 根据extName找到具体适应类,然后调用方法
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }

@Adaptive修饰方法来指明需要动态生成代码类来增强指定方法

当执行进入 ExtensionLoader#getAdaptiveExtension()  -> #createAdaptiveExtension 动态创建并编译代码的过程里:

首先会从配置文件中装载实现Protocol接口的实现类。

 判定:  [有入参是Protocol.class的构造器] 的实现类将被集中到“cachedWrapperClasses”,在后面对真正使用的Protocol实例进行装饰!

// 构造函数的入参是指定的接口   
 private boolean isWrapperClass(Class<?> clazz) {
        try {
            clazz.getConstructor(type);
            return true;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

ExtensionLoader#createAdaptiveExtensionClass使用 javassist 字节码编辑技术动态创建并编译了 Protocol$Adaptive !  在文章开头有贴它源码。

#export 服务发布过程

回到入口处:  ServiceBean#export() -> #doExport() ->#doExportUrls

  private void doExportUrls() {
        List<URL> registryURLs = loadRegistries(true); //判定如果有注册中心,重写url协议为register
        for (ProtocolConfig protocolConfig : protocols) {
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);//发布服务
        }
    }

#loadRegistries 将协议从'dubbo' 改为'registry'

原始的URL信息配置如下图:

经过处理后得到'registry协议'的URL:

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=noob-core&dubbo=2.6.2&logger=slf4j&pid=27468&registry=zookeeper&timestamp=1654680258624

这里如果由多个注册中心地址,会一一匹配生成多个。 

#doExportUrlsFor1Protocol 执行发布

这里会根据原始的配置参数组装dubbo协议头的URL,并在之后创建Invoker代理时,将它拼入registry协议头URL末尾的export部分!

dubbo://139.139.37.184:20880/cn.noob.interf.BuyBackInterface?anyhost=true&application=noob-core&bind.ip=139.139.37.184&bind.port=20880&dubbo=2.6.2&generic=false&group=noob-rest&interface=cn.noob.interf.BuyBackInterface&logger=slf4j&methods=buyBackSendRepayToYcloans,buyBack&pid=25416&retries=-1&revision=1.0&side=provider&threads=1024&timeout=180000&timestamp=1654683368355&version=1.0

 转换成了:

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=noob-core&dubbo=2.6.2&export=dubbo%3A%2F%2F139.139.37.184%3A20880%2Fcn.noob.interf.BuyBackInterface%3Fanyhost%3Dtrue%26application%3Dnoob-core%26bind.ip%3D139.139.37.184%26bind.port%3D20880%26dubbo%3D2.6.2%26generic%3Dfalse%26group%3Dnoob-rest%26interface%3Dcn.noob.interf.BuyBackInterface%26logger%3Dslf4j%26methods%3DbuyBackSendRepayToYcloans%2CbuyBack%26pid%3D25416%26retries%3D-1%26revision%3D1.0%26side%3Dprovider%26threads%3D1024%26timeout%3D180000%26timestamp%3D1654683368355%26version%3D1.0&logger=slf4j&pid=25416&registry=zookeeper&timestamp=1654683267651

 #exportLocal 提前生成 'Injvm协议发布Local服务供优先本地调用

injvm://127.0.0.1/cn.noob.interf.BuyBackInterface?anyhost=true&application=noob-core&bind.ip=139.139.37.184&bind.port=20880&dubbo=2.6.2&generic=false&group=noob-rest&interface=cn.noob.interf.BuyBackInterface&logger=slf4j&methods=buyBackSendRepayToYcloans,buyBack&pid=25708&retries=-1&revision=1.0&side=provider&threads=1024&timeout=180000&timestamp=1655083273825&version=1.0

当应用既是服务的提供者,同时也是这个服务的消费者,可以直接对本机提供的服务发起本地调用。

 如果URL上没有指定'scope = remote',会先于RegistryProtocol过程使用InjvmProtocol暴露本地服务,不会对外开启端口。

在同一个进程里对该服务的调用会优先走本地,它首先会进入Protocol$Adaptive#export, 所以也会构建invoke的Filter链!

Protocol$Adaptive#export 增强发布逻辑

// Protocol$Adaptive源码
public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); // 如果url的protocol为空, 默认走 dubbo
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        // 根据extName找到具体适应类,然后调用方法
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }

从上述Protocol$Adaptive源码可知, 这里关键的是: url#getProtocol()决定了下一个要执行的Protocol接口实现类!

 所以此时使用ExtensionLoader#getExtension('registry')去拿registryProtocol的实例:如果没有历史缓存,则#createExtension创建新的实例

 从上图中可知道: 新建真实对象实例时会使用多个wrapperClass来封装。这几个wrapperClass就是在读配置文件时归集而来[有Protocol为入参构造器]的实现类。

  1. ProtocolFilterWrapper : 对非‘registry’协议头的URL在export、refer时给Invoker绑定过滤器链FilterChain!Filter通过@Activate来指定支持的group:  provider | consumer 及在URL特别指定匹配相应Filter的操作指令:executes、tps、tracing...。 
  2. QosProtocolWrapper : Quality of Service,服务质量。 用来开启一个默认22222端口的服务,用来后台telnet上来online和offline service。
  3. ProtocolListenerWrapper : 通过 ExporterListener、InvokerListener 接口对服务的订阅和消费进行监听。默认看起来没具体实现。

这里还需要关注一下方法ExtensionLoader#injectExtension:

通过反射setter方式执行方法RegistryProtocoL#setProtocol,又把AdaptiveExtensionFactory里缓存的Protocol$Adaptive写回了RegistryProtocol里!

RegistryProtocol#export 发布过程

  public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        //export invoker   处理dubboProtocol
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

        URL registryUrl = getRegistryUrl(originInvoker);

        //registry provider
        final Registry registry = getRegistry(originInvoker);
        final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);

        //to judge to delay publish whether or not
        boolean register = registedProviderUrl.getParameter("register", true);

        ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registedProviderUrl);

        if (register) {
            // 在zookeeper上发布服务URL
            register(registryUrl, registedProviderUrl);
            ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
        }

        // Subscribe the override data
        // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call the same service. Because the subscribed is cached key with the name of the service, it causes the subscription information to cover.
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); 
        //Ensure that a new exporter instance is returned every time export
        return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registedProviderUrl);
    }

这里发布核心过程按先后顺序:

  1. RegistryProtocol#doLocalExport:从'registry'协议URL的‘export’部分拿到源'dubbo'协议URL,进入Protocol$Adaptive#export方法体内后,由此选择了回调DubboProtocol。
  2. ZookeeperRegistry#register:在zookeeper上写入服务地址节点。ZookeeperRegistry的抽象父类是FailbackRegistry,它定义了在发布#register、订阅#subscribe、变化产生的事件通知#notify(eg. AbstractRegistry#notify->#saveProperties缓存文件更新、更新RegistryDirectory里服务提供者Invoker视图) 的 处理逻辑及异常时的补差任务。

 RegistryProtocol#doLocalExport: 从'registry'协议URL的‘export’部分拿到源'dubbo'协议URL,回调DubboProtocol

如下图所示,这里会从入参的'registry'协议URL里拿到‘export’部分,它就是源头的dubbo协议头URL。

从下图中可以看出,又要回到Protocol$Adaptive#export方法体内,只是这次选择执行了DubboProtocol#export

DubboProtocol#export 开启网络通信:以[serviceGroup + serviceName + serviceVersion + port] 缓存了Invoker, 以[host:post]为Key缓存了服务端HeaderExchangeServer

以DubboExporter封装了Invoker,并缓存在'AbstractProtocol.exporterMap'里。key的构建依赖于: 见AbstractProtocol#serviceKey :serviceGroup + serviceName + serviceVersion + port

noob-rest/cn.noob.interf.BuyBackInterface:1.0:20880

[host:post] 为Key,ConcurrentHashMap缓存了HeaderExchangeServer,它实际上是NettyServer, 这里端口默认是20880; 所以一般情况下只有1个Server

 创建过程如下:

最终的URL: 

dubbo://139.139.37.184:20880/cn.noob.interf.BuyBackInterface?anyhost=true&application=noob-core&bind.ip=139.139.37.184&bind.port=20880&channel.readonly.sent=true&codec=dubbo&dubbo=2.6.2&generic=false&group=noob-rest&heartbeat=60000&interface=cn.noob.interf.BuyBackInterface&logger=slf4j&methods=buyBackSendRepayToYcloans,buyBack&pid=25708&retries=-1&revision=1.0&side=provider&threads=1024&timeout=180000&timestamp=1655083273825&version=1.0
  1. 指定数据报文的(反)序列化方式DubboCodec,实际是Hessian2Serialization
  2. HeaderExchangeServer里开启消费端Client与服务端Server的心跳检查任务HeartBeatTask的调度器。默认每隔60s检测一次,3分钟内没响应则发起重连(先AbstractClient#disconnect()关闭channel后AbstractClient#connect()再连接)。它的底层核心是通信的NettyServer。
  3. DubboProtocol内自定义实现的ExchangeHandlerAdapter会当作ChannelHandler绑定到NettySever上;NettyHandler定义了各种通信事件的处理方法:messageReceived、writeRequested、channelConnected、channelDisconnected
  4. 当传递的消息RpcInvocation被接收时,ExchangeHandlerAdapter#reply方法解析URL找到缓存的DubboExporter,执行方法Invoker#invoke。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值