【Dubbo】2.服务发布和引用

服务发布

SpringContainer启动之后就开始进入Spring的生命周期管理了,依赖Spring提供的各种扩展机制,比如spring会自动查找dubbo.jar!/META-INF/spring.handlers文件的内容,实现命名空间自定义扩展

DubboNamespaceHandler

Dubbo的配置文件中有很多xml节点比如:

  • <dubbo:application/>
  • <dubbo:registry/>
  • <dubbo:service/>
  • <dubbo:reference/>
  • <dubbo:protocol/>

如果我们要自己写标签同时整合到Spring,首先要查看官方文档找到Xml扩展接口,Dubbo就是通过DubboNamespaceHandler继承Spring提供的扩展接口NamespaceHandlerSupport,允许Dubbo自定义Xml标签向Spring容器注册BeanDefinition,而Xml文件经过DubboBeanDefinitionParser.parse()之后又具备了XMLJava代码转换的能力,具体的配置参数如下所示,后面有专题章节对配置以及转换过程做详细介绍,这里只需要了解下面的xml等同于手写Dubbo Api代码即可。

Consumer配置

<dubbo:application name="demo-consumer"/>
<dubbo:registry address="multicast://224.5.6.7:1234" />
<dubbo:reference id="demoService" interface="org.apache.dubbo.demo.DemoService" />

Provider配置

<dubbo:application name="demo-provider"  />
<dubbo:registry address="multicast://224.5.6.7:1234" />
<dubbo:service id="demoService" interface="org.apache.dubbo.demo.DemoService" />
<bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl" />

DubboNamespaceHandler只有一个init()方法,从中观察到,<service><reference>标签注册的分别是ServiceBeanReferenceBean对象,而其他的元素注册的都是Config对象,表明<service><reference>被作者赋予高于普通配置的行为和地位(皆继承AbstractConfig

ServiceBean和ReferenceBean是Dubbo与Spring容器整合的核心方法

每一个elementName都是xml中的标签,有其相应属性,具有可配置的能力

package org.apache.dubbo.config.spring.schema;
class DubboNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        // 代码太长放不下,为了格式漂亮定义下变量
        // register=registerBeanDefinitionParser
        // BeanDefParser=DubboBeanDefinitionParser
        register("application", new BeanDefParser(ApplicationConfig.class, true));
        register("module", new BeanDefParser(ModuleConfig.class, true));
        register("registry", new BeanDefParser(RegistryConfig.class, true));
        register("config-center", new BeanDefParser(ConfigCenterBean.class, true));
        register("metadata-report", new BeanDefParser(MetadataReportConfig.class, true));
        register("monitor", new BeanDefParser(MonitorConfig.class, true));
        register("metrics", new BeanDefParser(MetricsConfig.class, true));
        register("provider", new BeanDefParser(ProviderConfig.class, true));
        register("consumer", new BeanDefParser(ConsumerConfig.class, true));
        register("protocol", new BeanDefParser(ProtocolConfig.class, true));
        // 名字是以Bean结尾的区别于上面的Config
        register("service", new BeanDefParser(ServiceBean.class, true));
        register("reference", new BeanDefParser(ReferenceBean.class, false));
        register("annotation", new BeanDefParser());
    }
}

ServiceBean

Dubbo要求<service>标签由Provider端定义,下面开始介绍Provider的定义入口ServiceBean。其泛型由interface="org.apache.dubbo.demo.DemoService"字段指定。

package org.apache.dubbo.config.spring;
public class ServiceBean<T> extends ServiceConfig<T>
implements InitializingBean, DisposableBean, BeanNameAware,
      ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>,
      ApplicationEventPublisherAware 

ServiceBean实现了好多接口,看下各自的作用

接口 方法 作用
InitializingBean afterPropertiesSet() bean启动扩展
DisposableBean destroy() bean销毁扩展
ApplicationContextAware setApplicationContext(ApplicationContext) 注入spring环境
ApplicationEventPublisherAware setApplicationEventPublisher(Publisher) 注入事件发布
BeanNameAware setBeanName(String name) 注入当前bean的名字
ServiceConfig export(),doExportUrls() 生成Protocol
  1. ServiceBean实现了Spring的InitializingBean扩展,容器启动时会自动执行afterPropertiesSet()

  2. ServiceBean实现了Spring的DisposableBean扩展,Bean销毁时会自动执行destroy()

  3. ServiceBean实现了Spring的ApplicationContextAware扩展,应用启动完毕会自动注入ApplicationContext上下文,方便Dubbo从全局获取Bean对象

  4. ServiceBean实现了Spring的ApplicationEventPublisherAware扩展,具有发布事件的能力

  5. ServiceBean实现了Spring的BeanNameAware扩展,知道自己的BeanName

  6. ServiceBean继承了ServiceConfig,主要目的是根据Config生成dubbo协议

    dubbo://host/com.foo.FooService?route=route&cluster=&version=1.0.0

Spring容器启动自动调用afterPropertiesSet(),时序上先于onApplicationEvent()

ServiceBean.afterPropertiesSet():

afterPropertiesSet():代码很长分两部分说明

1.标签优先级设定:根据BeanDefinition标签设定优先级

2.判断当前Spring版本是否支持ApplicationListener(),否则延迟导出直到Spring容器启动完毕

// 标签优先级设定伪代码
if exist(<provider/>) {find(<Protocol/>); setProviders(ProviderBean)}
if exist(<application/>) { setApplication(ApplicationBean)}
if exist(<registry/>) {super.setRegistries(ModuleBean)}
if exist(<protocol/>) {super.setProtocol(ProtocolBean)}
...
// 以上设置完毕判断spring是否支持事件监听,低版本兼容
if !supportedApplicationListener {export();}    

Spring容器启动完毕后监听ContextRefreshedEvent执行onApplicationEvent()

ServiceBean.onApplicationEvent():

onApplicationEvent():根据服务export发布状态,判断是否export()

    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (!isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            export();
        }
    }

ServiceBean.export():

export方法做了两件事

  1. 调用父类的export(),父类为ServiceConfig, ServiceBean#export()->ServiceConfig#export()
  2. 发布ExportEvent
    public void export() {
        // Step1
        super.export();
        // Step2
        publishExportEvent();
    }

super.export():

Step1:super.export() => ServiceConfig#export()

  1. 配置检查和更新事先由afterPropertiesSet()配置完毕
  2. 判断是否需要导出,不导出就是本地方法
  3. 若延迟导出则委托delayExecutor执行导出
  4. 否则调用 doExport()
package org.apache.dubbo.config;
super class ServiceConfig
public synchronized void export() {
        // 配置检查和更新 事先由afterPropertiesSet配置完毕
        checkAndUpdateSubConfigs();
        if (!shouldExport()) { // 标签属性export boolean
            return;
        }
        if (shouldDelay()) {// 标签属性delay boolean
            // 延时export
            delayExecutor.schedule(this::doExport, getDelay(), MILLISECONDS);
        } else {
            doExport();
        }
    }

super.doExportUrls():

doExport()在简单开关验证及设置path=interfaceName之后执行doExportUrls()

doExportUrls():

  1. 获取注册中心URL地址,由上面<registry/>标签定义,此处已知配置为multicast

  2. 获取协议列表,一般只配置一个protocol,由<protocol/>标签定义

  3. 开始循环协议列表

    获取pathKeycom.kaiyuan.qyd.topic.dubbo.basic.DemoService

    创建ProviderModel实体对象并加入缓存,interfaceClass是接口类,ref是接口实现类

    执行核心逻辑doExportUrlsFor1Protocol()

package org.apache.dubbo.config;
super class ServiceConfig
private void doExportUrls() {
        // registry://host/org.xxx.registry.RegistryService?application=demo-provider&registry=multicast
        List<URL> registryURLs = loadRegistries(true);
        // <dubbo:protocol name="dubbo" port="20880" id="dubbo"/>
        for (ProtocolConfig protocolConfig : protocols) {
            // pathKey = com.kaiyuan.qyd.topic.dubbo.basic.DemoService
            String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
            // interfaceClass是接口类,ref是接口实现类
            ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
            // 放入缓存PROVIDED_SERVICES
            ApplicationModel.initProviderModel(pathKey, providerModel);
            // 核心代码
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

AbstractInterfaceConfig.loadRegistries():

这个方法来自抽象父类,主要逻辑为:

1.参数验证,代码中省略

2.根据BeanDefinition配置,获取parameters放到map

3.根据map中的参数生成注册中心URL地址,翻译如下:

registry://host/org.xxx.RegistryService?application=demo-provider&registry=multicast

   protected List<URL> loadRegistries(boolean provider) {
      List<URL> registryList = new ArrayList<URL>();
      // 省略参数验证
            if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
             Map<String, String> map = new HashMap<String, String>();
         // 把<application>中的parameter放到map中,比如name
             appendParameters(map, application);
         // 把<registry>中的parameter反射放到map中,比如host
             appendParameters(map, config);
             map.put(PATH_KEY, RegistryService.class.getName());
         // 追加一些运行时参数比如时间戳,版本号,pid
             appendRuntimeParameters(map);
             List<URL> urls = UrlUtils.parseURLs(address, map);
             for (URL url : urls) {
                 url = URLBuilder.from(url)
                         .addParameter(REGISTRY_KEY, url.getProtocol())
                         .setProtocol(REGISTRY_PROTOCOL)
                         .build();
             // registry://host/org.xxx.RegistryService?application=demo-provider&registry=multicast
                 if ((provider && url.getParameter(REGISTER_KEY, true))
                         || (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
                     registryList.add(url);
                 }
             }
            }
      return registryList;
    }

appendParameters()

看下参数追加的代码逻辑

1.获取当前BeanDefinition所配置的所有Getter方法,本例是applicationgetApplication()

2.反射获取getApplication()的注解修饰类,判断是否被@Parameter(excluded = true)修饰跳出设置,否则追加到parameters

    protected static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
        Method[] methods = config.getClass().getMethods();
        for (Method method : methods) {
            try {
                String name = method.getName();
                // 获取ApplicationConfig下所有的Getter方法:getApplication()
                if (MethodUtils.isGetter(method)) {//getName()
                    Parameter parameter = method.getAnnotation(Parameter.class);
                    if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
                        continue;
                    }
                } 
                // put parameters key:prefix.xxx ....
            } catch (Exception e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
    }

super.doExportUrlsFor1Protocol():

服务发布核心代码,比较长,不适合阅读,所以拆分成四块分析

1.根据BeanDefinition配置组装URL

    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
        String name = protocolConfig.getName();
        if (StringUtils.isEmpty(name)) {
            name = DUBBO;
        }
        Map<String, String> map = new HashMap<String, String>();
        // 标记为Provider端
        map.put(SIDE_KEY, PROVIDER_SIDE);
        // 追加一些运行时参数比如时间戳,版本号,pid
        appendRuntimeParameters(map);
        // 把BeanDefinition中的parameter反射放到map中
        appendParameters(map, metrics);
        appendParameters(map, application);
        appendParameters(map, module);
        appendParameters(map, provider);
        appendParameters(map, protocolConfig);
        appendParameters(map, this);
        // 子标签配置<dubbo:method/>
    }

此时的map是这样的:

{
    "side": "provider", // 标记为生产者
    "application": "demo-provider",
    "release": "2.7.2", // 从jar包截取的版本号
    "deprecated": "false",
    "dubbo": "2.0.2", // Dubbo RPC protocol version
    "pid": "48180",
    "dynamic": "true",
    "interface": "com.kaiyuan.qyd.topic.dubbo.basic.DemoService",
    "generic": "false", // 是否是泛化调用
    "timestamp": "1563865286121", // 时间戳
    "register": "true",
    "bean.name": "com.kaiyuan.qyd.topic.dubbo.basic.DemoService"
}

2.子标签配置<dubbo:method/>,配置粒度可以到<argument>级别,跟踪这段代码需要补充配置标签:

<dubbo:service interface="com.kaiyuan.qyd.topic.dubbo.basic.DemoService" ref="demoService" >
    <dubbo:method name="sayHello" timeout="2000">
        <dubbo:argument type="java.lang.String" callback="true"></dubbo:argument>
    </dubbo:method>
</dubbo:service>

细粒度的配置主要用来为泛化调用,区分参数类型,参数下标,正常业务很少对它配置

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
      // 判断是否配置<method>
            if (CollectionUtils.isNotEmpty(methods)) {
            for (MethodConfig method : methods) {
                // 追加timeout参数到map中,key:sayHello.timeout value:2000
                appendParameters(map, method, method.getName());
                // 判断是否设置sayHello.retry或重写retries
                String retryKey = method.getName() + ".retry";
                if (map.containsKey(retryKey)) {
                    String retryValue = map.remove(retryKey);
                    if ("false".equals(retryValue)) {
                        map.put(method.getName() + ".retries", "0");
                    }
                }
                List<ArgumentConfig> arguments = method.getArguments();
                // 是否配置了<argument>,列表判断是因为参数可能多参
                if (CollectionUtils.isNotEmpty(arguments)) {
                    // 省略的这部分代码主要是配置<argument>和上面的<method>逻辑相同
                }
            } // end of methods for
        }
}

配置完<method> <argument>map变成这样:

{
    ... // 省略未变化参数
    "sayHello.timeout": "2000", // 方法的超时时间
    "sayHello.0.callback": "true" // 标记下标为0的参数是callback函数(举例而已)
}

3.判断是否是泛化调用,并追加一些参数到map中,生成URL对象

这里有个特殊的Wrapperjavassist处理后自动生成getMethodNames(),留在Invoke章节详细介绍

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
      // 是否泛化调用,本例是用户业务接口,不是泛化调用接口
            if (ProtocolUtils.isGeneric(generic)) {
            map.put(GENERIC_KEY, generic);
            map.put(METHODS_KEY, ANY_VALUE);
        } else {
            String revision = Version.getVersion(interfaceClass, version);
            if (revision != null && revision.length() > 0) {
                map.put(REVISION_KEY, revision);
            }
                // 包装类,javassist处理之后自动生成getMethodNames(),Invoker章节详细介绍
            String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
            if (methods.length == 0) {
                logger.warn("No method found in service interface " + interfaceClass.getName());
                map.put(METHODS_KEY, ANY_VALUE);
            } else {
                map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
            }
        }
        // 是否配置token
        if (!ConfigUtils.isEmpty(token)) {
            if (ConfigUtils.isDefault(token)) {
                map.put(TOKEN_KEY, UUID.randomUUID().toString());
            } else {
                map.put(TOKEN_KEY, token);
            }
        }
            // 获取本地ip
        String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
        // port:20880
        Integer port = this.findConfigedPorts(protocolConfig, name, map);
            // 由map对象转换生成url
        URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
        // export service ...
}

urlmap对象转换而成:

dubbo://172.16.69.49:20880/com.kaiyuan.qyd.topic.dubbo.basic.DemoService?anyhost=true&application=demo-provider&bean.name=com.kaiyuan.qyd.topic.dubbo.basic.DemoService&bind.ip=172.16.69.49&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.kaiyuan.qyd.topic.dubbo.basic.DemoService&methods=sayHello&pid=46800&register=true&release=2.7.2&sayHello.0.callback=true&sayHello.timeout=2000&side=provider&timestamp=1563870489213

4.服务发布,涉及RPC模块的内容,将在第三章Invoker详细介绍,这里需要了解Dubbo的领域模型

  • 实体域:Invoker,是RPC调用的核心

  • 会话域:Invocation,一次RPC调用的上下文

  • 服务域:Protocol,实体域会话域生命周期管理,发布或引用入口

package org.apache.dubbo.rpc;
@SPI("dubbo")
// 服务域
public interface Protocol { 
    int getDefaultPort();
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}
// 实体域
public interface Invoker<T> extends Node {
    Class<T> getInterface();
    Result invoke(Invocation invocation) throws RpcException;
}
// 会话域
public interface Invocation {
    String getMethodName();
    Class<?>[] getParameterTypes();
    Object[] getArguments();
    Map<String, String> getAttachments();
    void setAttachment(String key, String value);
    void setAttachmentIfAbsent(String key, String value);
    String getAttachment(String key);
    String getAttachment(String key, String defaultValue);
    Invoker<?> getInvoker();
}

服务发布逻辑:

1.判断协议是否被重新定义

2.本地服务发布:url重写,创建Invoker动态代理类,缓存Exporter等待export()

3.远程服务发布:和本地调用一样先生成Exporter后执行export(),开启远程端口,注册dubbo协议到注册中心等待调用

package org.apache.dubbo.config;

public class ServiceConfig<T> extends AbstractServiceConfig {
        private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
            // 判断dubbo协议是否被用户自定义,此处使用默认dubbo协议
            if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                    .hasExtension(url.getProtocol())) {
                url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                        .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
            }
            // url中没有配置scope,此处为null
            String scope = url.getParameter(SCOPE_KEY);
            // don't export when none is configured
            if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
                if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
                    // 先从本地export
                    exportLocal(url);
                }
                // 如果配置了注册中心则进行远程export 
                // 1.createExporter(ref,interface,register)
                // 2.register("dubbo://xxx")
                // 3.openServer(url);
            }
    }
  
    private void exportLocal(URL url) {
        // url host port重写变成injvm://127.0.0.1,其他参数未变
        URL local = URLBuilder.from(url)
                .setProtocol(LOCAL_PROTOCOL)
                .setHost(LOCALHOST_VALUE)
                .setPort(0)
                .build();
        // 包装ref引用为Invoker接口对象,执行protocol.export()
        Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        // 加入缓存
        exporters.add(exporter);
    }
}

小结:方法export()概括的说做了两件事

  1. 配置URL,生成Protocol服务域。dubbo协议是register协议的一个export参数同步到注册中心

    registry://host/xxx.RegistryService?export=dubbo%3A%2F%2F172.16.69.49%3A20880%2F/xxx

  2. 生成远程代理类Invoker,执行Protocol.export(Invoker),注册服务,开启端口监听,等待Protocol.refer()


ServiceBean.export()方法Step2执行publishExportEvent(),发布一个事件,下面看下具体逻辑

Step2:publishExportEvent()

  1. 创建ServiceBeanExportedEvent事件,此对象结构比较简单,只有一个构造参数source
  2. source对象代表发生事件的主体,继承自Spring的ApplicationEvent,source=ServiceBean(this)
  3. 调用spring注入applicationEventPublisher发布事件等待监听器响应
public void export() {
   // Step1
   super.export();
   // Step2
   publishExportEvent();
}
  
private void publishExportEvent() {
   // 创建事件实体对象,source对象为ServiceBean
   ServiceBeanExportedEvent exportEvent = new ServiceBeanExportedEvent(this);
   // ApplicationEventPublisherAware方法注入的Publisher
   applicationEventPublisher.publishEvent(exportEvent);
}

ReferenceAnnotationBeanPostProcessor

ServiceBeanExportedEvent事件只有一个监听Listener即ReferenceAnnotationBeanPostProcessor,该类结构如下:

package org.apache.dubbo.config.spring.beans.factory.annotation;
public class ReferenceAnnotationBeanPostProcessor
    extends AnnotationInjectedBeanPostProcessor<Reference>
    implements ApplicationContextAware, ApplicationListener
接口 方法 作用
ApplicationContextAware setApplicationContext() Spring上下文
ApplicationListener onApplicationEvent(E event) 监听逻辑
AnnotationInjectedBeanPostProcessor doGetInjectedBean(...) 注解依赖注入
  1. ApplicationListener,监听ServiceBeanExportedEvent,执行onApplicationEvent()
  2. AnnotationInjectedBeanPostProcessor,dubbo的依赖注入工具与spring注入对接

先看ApplicationListener唯一方法onApplicationEvent(),事件的执行者

onApplicationEvent()

  1. 判断instanceof ServiceBeanExportedEvent
  2. 执行onServiceBeanExportEvent(event)
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ServiceBeanExportedEvent) {
            // 判断类型执行相应操作 内部直接调用了initReferenceBeanInvocationHandler方法
            onServiceBeanExportEvent((ServiceBeanExportedEvent) event);
        } else if (event instanceof ContextRefreshedEvent) {
            onContextRefreshedEvent((ContextRefreshedEvent) event);
        }
  }

onServiceBeanExportEvent()内部直接调用了initReferenceBeanInvocationHandler()

initReferenceBeanInvocationHandler()

  1. 获取cache key
  2. 从localReferenceBeanInvocationHandlerCache中移除了key对象
  3. 执行了init()
 private void initReferenceBeanInvocationHandler(ServiceBean serviceBean) {
        String beanName = serviceBean.getBeanName();
        // Remove ServiceBean when it's exported
        // 为了调整格式修改了变量名称lrbhc=localReferenceBeanHandlerCache
        ReferenceBeanInvocationHandler handler = lrbhc.remove(beanName);
        // Initialize
        if (handler != null) {
            handler.init();
        }
 }

问题:为什么要用缓存?

因为<service><reference>都是重量级对象,不能频繁创建,缓存和双重锁保证单例
dubbo依托spring生命周期做缓存证明了dubbo使用api启动的方式要考虑程序健壮性,需要自己做内存管理

问题: 为什么要从localReferenceBeanHandlerCache中移除对象?

带着这个问题继续分析,cache可以被remove说明cache里面有值,如果我们知道缓存存放的是什么值,以及存放的逻辑,那么也就知道问题所在了。

按照这个思路定位到方法doGetInjectedBean(),是来自父类AnnotationInjectedBeanPostProcessor的抽象方法,该方法有个关键的参数

 Object doGetInjectedBean(Reference reference, Object bean, String beanName,Class<?> injectedType,InjectedElement injectedElement)

Reference reference: 这是我们@Reference 的注解类,通过它可以注入SpringBean,通常这么写

package com.kaiyuan.qyd.business.tx;
@Reference FooService fooService;

Dubbo依托Spring包扫描机制把@Service/@Reference注解修饰的对象注册/注入到Spring容器中

@Reference使用范围:可以用在FieldMethod上,说明是用来依赖注入

package org.apache.dubbo.config.annotation;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface Reference {}

@Service的使用范围:只能用在Class上为了注册到spring容器

package org.apache.dubbo.config.annotation;
@Target({ElementType.TYPE})
public @interface Service {}

DubboComponentScanRegistrar:

上面说到Dubbo依托Spring包扫描机制把@Service/@Reference注解修饰的对象注册/注入到Spring容器中,下面简单介绍下是怎么实现的

父类ImportBeanDefinitionRegistrar是Spring接口,用来实现包扫描注册功能

  1. @DubboComponentScan包扫描获取路径
  2. 注册@Service注解修饰的对象到spring,会触发BeanDefinition拦截器自定义注入逻辑
  3. 注入@Reference注解修饰的对象到spring,最终会触发依赖注入doGetInjectedBean逻辑
package org.apache.dubbo.config.spring.context.annotation;
// 本节只做介绍后面会有spring和dubbo整合的源码分析
public class DubboComponentScanRegistrar implements ImportBeanDefinitionRegistrar {
@Override
void registerBeanDefinitions(AnnotationMetadata meta, BeanDefinitionRegistry registry) {
       // @DubboComponentScan包扫描
     Set<String> packagesToScan = getPackagesToScan(meta);
     // 注册@Service注解修饰的对象到spring,使用了BeanDefinitionRegistry注册工厂
     // 核心逻辑在ServiceAnnotationBeanPostProcessor.registerServiceBean方法内部
     registerServiceAnnotationBeanPostProcessor(packagesToScan, registry);
     // 注入@Reference注解修饰的对象到spring,使用了BeanDefinitionRegistry注册工厂
     registerReferenceAnnotationBeanPostProcessor(registry);
 }
}

Spring所使用的注册工厂如下,registerBeanDefinition()执行注册逻辑时会触发Dubbo实现的BeanDefinitionRegistryPostProcessor子类ServiceAnnotationBeanPostProcessor实现自定义注册

package org.springframework.beans.factory.support;
public interface BeanDefinitionRegistry extends AliasRegistry {
    // spring注册方法
    void registerBeanDefinition(String beanName, BeanDefinition beanDefinition);

doGetInjectedBean
上面说到使用@Reference依赖注入最终会执行本方法,下面开始分析方法逻辑

doGetInjectedBean

  1. 获取referencedBeanName
  2. 包装业务接口成ReferenceBean
  3. 缓存ReferenceBean到BeanCache
  4. 创建代理类
 @Override
 Object doGetInjectedBean(Reference reference, Object bean, String beanName,Class<?> injectedType,InjectedElement injectedElement){
     // 构建beanname
     String referencedBeanName = buildReferencedBeanName(reference, injectedType);
     // 包装业务接口成ReferenceBean
     ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referencedBeanName,reference, injectedType, getClassLoader());
     cacheInjectedReferenceBean(referenceBean, injectedElement);
     // 创建代理类 
     return buildProxy(referencedBeanName, referenceBean, injectedType);
 }

动态代理在这里的作用是动态生成代理类,对原始方法进行控制,对使用者透明,本章只阐述下直观作用

未代理之前:
调用方->业务接口->业务接口实现类
动态代理后:
调用方->业务接口->动态代理类->业务接口实现类

动态代理类InvocationHandler.invoke()方法示例:

begin()
 try 
  action1
        srcMethod.invoke();// 原始方法调用
    action2
 catch e {}
 finally {}
end()

真实的代理比这个要复杂,本代理类其实就是远程调用的Invoker对象(包含五元组,协议,序列化等等),比如consumer端执行远程调用的同时也要生成代理类,provider端接收亦然,在后面章节Invoker和Proxy会详细介绍

buildProxy:

介绍完动态代理,回过头来看下代码逻辑

buildProxy目的是针对业务接口生成代理方法,对调用方无感知

Object buildProxy(String referencedBeanName, ReferenceBean referenceBean, Class<?> injectedType) {
                // 生成动态代理handler,对业务接口进行控制代理
        InvocationHandler handler = buildInvocationHandler(referencedBeanName, referenceBean);
        // dubbo注解管理的bean对象都是动态代理类,会执行handler.invoke()
        return Proxy.newProxyInstance(getClassLoader(), new Class[]{injectedType}, handler);
}

问题的线索就在buildInvocationHandler()内部

缓存存放的值ReferenceBeanInvocationHandler,该对象是上面的动态代理类
缓存存放逻辑@Reference注入的是不是本地@Service注册的Spring对象

InvocationHandler buildInvocationHandler(String referencedBeanName, ReferenceBean referenceBean) {
        ReferenceBeanInvocationHandler handler = lcrih.get(referencedBeanName);
        if (handler == null) {
            handler = new ReferenceBeanInvocationHandler(referenceBean);
        }
        // 从这能看到缓存添加的是本地spring管理的bean对象,用referencedBeanName做区分
        if (applicationContext.containsBean(referencedBeanName)) { 
            // lcrih=localReferenceBeanInvocationHandlerCache
            lcrih.put(referencedBeanName, handler);
        } else {
            handler.init();
        }
        return handler;
}

ReferenceBeanInvocationHandler

ReferenceBeanInvocationHandler继承了InvocationHandler动态代理类,代理对象是业务定义的接口:

<dubbo:reference id="demoService" interface="org.apache.dubbo.demo.DemoService" />

方法逻辑很简单,只是从缓存中获取管理的ReferenceBean调用执行,同时解决了一个单元测试的bug:

package org.apache.dubbo.config.spring.beans.factory.annotation;
public class ReferenceAnnotationBeanPostProcessor {}
// static内部类
private static class ReferenceBeanInvocationHandler implements InvocationHandler {
        private final ReferenceBean referenceBean;
        private Object bean;
        private ReferenceBeanInvocationHandler(ReferenceBean referenceBean) {
            this.referenceBean = referenceBean;
        }

        @Override // 动态代理方法执行入口
        public Object invoke(Object proxy, Method method, Object[] args) {
            Object result;
            try {
                if (bean == null) { 
                    // issue: https://github.com/apache/dubbo/issues/3429
                    init();
                }
                result = method.invoke(bean, args);
            } catch (InvocationTargetException e) {
                // re-throws the actual Exception.
                throw e.getTargetException();
            }
            return result;
        }
    
        private void init() {
            this.bean = referenceBean.get();
        }
    }
}

小结

  1. ServiceBean执行export方法时发布ServiceBeanExportedEvent事件
  2. ReferenceAnnotationBeanPostProcessor监听到该事件,从本地localReferenceBeanInvocationHandlerCache缓存中移除bean对象
  3. 缓存中的数据是依托DubboComponentScan包扫描机制根据@Service注解注入到spring容器中,同时对@Reference进行依赖注入
  4. 如果ReferenceBean所代理的bean其实是本地spring容器管理的,也就是本地调用,则存放到localReferenceBeanInvocationHandlerCache
  5. 因为有延迟导出的存在,所以采用事件通知机制及时更新本地缓存,动态代理生成InvocationHandler对象
  6. localReferenceBeanInvocationHandlerCache移除的只是本地代理对象,真正的远程代理类都在referenceBeanCache里面保存着

服务引用

服务发布完成后,生成RPC代理,同步Invoker信息到注册中心,等待Consumer端引用,配置如下:

<dubbo:application name="demo-consumer"/>
<dubbo:registry address="multicast://224.5.6.7:1234" />
<dubbo:reference id="demoService" interface="org.xxx.DemoService" />

ReferenceBean

Dubbo要求<reference>标签由Consumer端定义,下面开始介绍Consumer的定义入口ReferenceBean

public class ReferenceBean<T> extends ReferenceConfig<T> 
    implements FactoryBean, ApplicationContextAware, 
                         InitializingBean, DisposableBean {}
接口 方法 作用
FactoryBean getObject() 获取spring对象
ApplicationContextAware setApplicationContext(ApplicationContext) 注入spring环境
InitializingBean afterPropertiesSet() bean启动扩展
DisposableBean destroy() bean销毁扩展

这个类实现的接口与ServiceBean类似,相同的地方无需赘述,区别在于FactoryBean.getObject()

Spring容器启动自动调用afterPropertiesSet(),进行一系列的BeanDefinition设置之后执行FactoryBean.getObject()

package org.apache.dubbo.config.spring;

public class ReferenceBean<T> extends ReferenceConfig<T>
implements FactoryBean, ApplicationContextAware,
                     InitializingBean, DisposableBean {}

public void afterPropertiesSet() throws Exception {
    // 执行一系列BeanDefinition配置
    // setConsumer()
    // setApplication()
    // setRegistries()
    if (shouldInit()) {// 是否延迟加载
        getObject();
    }
}

public Object getObject() {
    // 父类ReferenceConfig中的方法
    return get();
}

ReferenceConfig.get():

同步方法get()和非空判断保证了单例,内部逻辑先是做了内部的验证和默认配置重写,之后执行ReferenceConfig.init()

package org.apache.dubbo.config;
public class ReferenceConfig<T> {
  
  public synchronized T get() {
      // 内部验证比如注册中心,还有一些默认参数的重写
      checkAndUpdateSubConfigs();

      if (destroyed) {
          throw new IllegalStateException("The invoker has already destroyed!");
      }
      if (ref == null) {
          // 单例初始化
          init();
      }
      return ref;
  }
}

ReferenceConfig.init():

核心是从注册中心获取元数据创建代理类,细节会在Invoker里面详细介绍

1.例行参数检查

2.map对象参数设置

2.创建远程代理类

private void init() {
    if (initialized) {
        return;
    }
    // 例行检查
    checkStubAndLocal(interfaceClass);
    checkMock(interfaceClass);
    Map<String, String> map = new HashMap<String, String>();
    map.put(SIDE_KEY, CONSUMER_SIDE);
    appendRuntimeParameters(map);
    // 省略map参数准备...
    String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY);
    if (StringUtils.isEmpty(hostToRegistry)) {
        hostToRegistry = NetUtils.getLocalHost();
    } else if (isInvalidLocalHost(hostToRegistry)) {
        throw new IllegalArgumentException("Specified invalid registry ip from property:");
    }
    map.put(REGISTER_IP_KEY, hostToRegistry);
        // 从注册中心获取元数据创建远程调用代理类
    ref = createProxy(map);
    String serviceKey = URL.buildKey(interfaceName, group, version);
    // 创建ConsumerModel并放到ApplicationModel缓存里
    ApplicationModel.initConsumerModel(serviceKey, buildConsumerModel(serviceKey, attributes));
    initialized = true;
}

小结:

ServiceConfigReferenceConfig最终都指向了Invoker,也说明了实体域Invoker的重要性,在这里对两个重要方法做个标记,也是后面第三章的引子。

1.服务发布创建远程代理类:ServiceConfig.doExportUrlsFor1Protocol()

2.服务导出引用远程代理类:ReferenceConfig.init()

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352