详解Dubbo(一):消费端初始化

2020-03-29  本文已影响0人  空挡

前言

前面的《白话Dubbo》系列从一个RPC框架的角度分解了Dubbo的各个组成部分,里面只涉及到了接口层。后续几篇文章会从源码层面分解下这些接口的实现以及各个模块是如何组合在一起的。跟白话系列一样,还是会从一次正常调用的角度来逐步分解,不会有大段的源码注释。如果是刚接触Dubbo,在看后续文章之前强烈建议先看下前面的白话Dubbo系列,要不然理解起来会有点困难。这部分的解析基于v2.7.6版本的源代码。

基础概念

这篇文章还是从一次正常的远程调用的消费端开始,首先有一些Dubbo的基础概念需要明确一下。

配置类

做为一个开放的开源框架,自然会给用户开放很多配置项,全局角度的比如进程所属的应用名称、版本、管理人等,接口角度比如调用超时、重试次数等。Dubbo把这些配置用一系列Config类来做了封装,这些类都是AbstractConfig的子类,关系图如下:


Config类

上图中不是全部,只是我认为经常用到的部分,从类名大致可以看出每个配置类的作用范围:

URL

URL类贯穿了一次Dubbo调用的整个过程,Dubbo中每个可配置单元都有唯一的URL,配置参数通过url的param来携带。URL是对标准的URL类的扩展,比如对于一个注册中心,它有一个url:zookeeper://224.5.6.7:2181,代表这个一个zookeeper的注册中心,它的地址是224.5.6.7,端口号2181。再比如一个服务的url是dubbo://10.5.6.7:28080/org.apache.dubbo.demo.DemoService?version=1.0.0&timeout=3000。代表这个服务是以dubbo协议暴露在10.5.6.7的28080端口,版本是1.0.0,调用超时是3秒。大部分时候我们不会直接接触到这些url的具体值,只要了解在一次dubbo调用过程中,所有的配置参数都是通过url的来传递的就可以了。

服务引用

回到一次RPC的调用的开始,首先看下dubbo官方的Demo:

@Component("demoServiceComponent")
public class DemoServiceComponent implements  DemoService{
    @Reference
    private DemoService demoService;

    @Override
    public String sayHello(String name) {
        return demoService.sayHello(name);
    }

    @Override
    public CompletableFuture<String> sayHelloAsync(String name) {
        return null;
    }
}

上面的例子中,DemoServiceComponent是一个Spring Bean,它有一个成员变量DemoService上面加了一个@Reference注解,这样在调用这个Bean的sayHello方法时,就可以调用这个引用的方法,完成一次远程调用。
在这个过程中,用户代码中除了加了一个@Reference注解外,其它部分跟一次普通的本地调用没有任何区别。所以对于Dubbo来说,它有两件事需要做,1) 需要实现一个DemoService的实现类,这个实现类能够调用远程的DemoService接口;2) 在Spring Bean初始化的时候将这个实现注入到Bean中。所以本质上@Reference注解的功能就类似于Spring的@Autowired注解。
为了做到上面一点,看下Demo程序在启动类里做了什么:

public class Application {
    /**
     * In order to make sure multicast registry works, need to specify '-Djava.net.preferIPv4Stack=true' before
     * launch the application
     */
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConsumerConfiguration.class);
        context.start();
        DemoService service = context.getBean("demoServiceComponent", DemoServiceComponent.class);
        String hello = service.sayHello("world");
        System.out.println("result :" + hello);
    }

    @Configuration
    @EnableDubbo(scanBasePackages = "org.apache.dubbo.demo.consumer.comp")
    @PropertySource("classpath:/spring/dubbo-consumer.properties")
    @ComponentScan(value = {"org.apache.dubbo.demo.consumer.comp"})
    static class ConsumerConfiguration {

    }
}

启动类里,在Spring的配置类上 加了一个@EnableDubbo的注解,同时配置了Dubbo的扫描路径。对Spring熟悉的应该可以估计出这个注解的作用了,Spring很多组件里都有类似Enable的注解。这里,@EnableDubbo的作用就是,在Spring在初始化的时候,dubbo就会到这个路径下扫描所有的@Reference注解。将生成的代理对象注入进去。

@EnableDubbo注解原理

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@EnableDubboConfig
@DubboComponentScan
public @interface EnableDubbo {
   ...
   ...
}

@EnableDubbo是一个派生注解,它组合了2个Dubbo相关的注解

@EnableDubboConfig注解原理

这个注解的作用类似于SpringBoot中的外部化配置功能,用于读取环境变量或者配置文件中的配置来赋值给配置类,同时将配置类注册成Spring Bean。Dubbo支持配置来自于多个来源,这些配置来自于多个层级,可以根据优先级互相覆盖,具体可以查看官方文档的配置加载流程部分。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(DubboConfigConfigurationRegistrar.class)
public @interface EnableDubboConfig {
    /**
     * It indicates whether binding to multiple Spring Beans.
     *
     * @return the default value is <code>false</code>
     * @revised 2.5.9
     */
    boolean multiple() default true;

}

这个注解的主要配置工作通过DubboConfigConfigurationRegistrar类来完成。在这个Registrar类中,会将类的属性和配置绑定。

public class DubboConfigConfiguration {

    /**
     * Single Dubbo {@link AbstractConfig Config} Bean Binding
     */
    @EnableConfigurationBeanBindings({
            @EnableConfigurationBeanBinding(prefix = "dubbo.application", type = ApplicationConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.module", type = ModuleConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.registry", type = RegistryConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.protocol", type = ProtocolConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.monitor", type = MonitorConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.provider", type = ProviderConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.consumer", type = ConsumerConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.config-center", type = ConfigCenterBean.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metadata-report", type = MetadataReportConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metrics", type = MetricsConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.ssl", type = SslConfig.class)
    })
    public static class Single {

    }

    /**
     * Multiple Dubbo {@link AbstractConfig Config} Bean Binding
     */
    @EnableConfigurationBeanBindings({
            @EnableConfigurationBeanBinding(prefix = "dubbo.applications", type = ApplicationConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.modules", type = ModuleConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.registries", type = RegistryConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.protocols", type = ProtocolConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.monitors", type = MonitorConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.providers", type = ProviderConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.consumers", type = ConsumerConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.config-centers", type = ConfigCenterBean.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metadata-reports", type = MetadataReportConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metricses", type = MetricsConfig.class, multiple = true)
    })
    public static class Multiple {

    }
}

从上面的配置可以看出dubbo.application开头的属性会绑定到ApplicationConfig配置类中。配置类有了,就可以使用这些配置初始化Dubbo的Reference对象了。

@DubboComponentScan 注解原理

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DubboComponentScanRegistrar.class)
public @interface DubboComponentScan {
     ...
     ...
}

从上面的注解定义可以看到,功能是由DubboComponentScanRegistrar类来实现的。

public class DubboComponentScanRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //解析需要扫描的路径
        Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
        //将带@Service注解的类暴露成服务接口
        registerServiceAnnotationBeanPostProcessor(packagesToScan, registry);

        // @since 2.7.6 Register the common beans
        registerCommonBeans(registry);
    }
    ....
    ....
}
public interface DubboBeanUtils {
    static void registerCommonBeans(BeanDefinitionRegistry registry) {

        // Since 2.5.7 Register @Reference Annotation Bean Processor as an infrastructure Bean
        registerInfrastructureBean(registry, ReferenceAnnotationBeanPostProcessor.BEAN_NAME,
                ReferenceAnnotationBeanPostProcessor.class);
DubboConfigAliasPostProcessor.BEAN_NAME,
                DubboConfigAliasPostProcessor.class);

        // Since 2.7.5 Register DubboLifecycleComponentApplicationListener as an infrastructure Bean
        registerInfrastructureBean(registry, DubboLifecycleComponentApplicationListener.BEAN_NAME,
                DubboLifecycleComponentApplicationListener.class);

        // Since 2.7.4 Register DubboBootstrapApplicationListener as an infrastructure Bean
        registerInfrastructureBean(registry, DubboBootstrapApplicationListener.BEAN_NAME,
                DubboBootstrapApplicationListener.class);

        // Since 2.7.6 Register DubboConfigDefaultPropertyValueBeanPostProcessor as an infrastructure Bean
        registerInfrastructureBean(registry, DubboConfigDefaultPropertyValueBeanPostProcessor.BEAN_NAME,
                DubboConfigDefaultPropertyValueBeanPostProcessor.class);
    }
}

上面的代码中注册@Service暴露服务这一步,我们讲服务提供方初始化的时候再说。registerCommonBeans()方法中,注册了如下几个BeanPostProcessor

@Reference注解处理
ReferenceAnnotationBeanPostProcessor主要代码如下

public class ReferenceAnnotationBeanPostProcessor extends AbstractAnnotationBeanPostProcessor implements
        ApplicationContextAware, ApplicationListener {
    @Override
    protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
                                       InjectionMetadata.InjectedElement injectedElement) throws Exception {
        /**
         * 获取引用的Service的Bean Name
         */
        String referencedBeanName = buildReferencedBeanName(attributes, injectedType);

        /**
         * 获取Reference Bean Name
         */
        String referenceBeanName = getReferenceBeanName(attributes, injectedType);
        // 构造一个ReferenceBean实例
        ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);
       // 判断引用的接口是否是这个进程暴露的
        boolean localServiceBean = isLocalServiceBean(referencedBeanName, referenceBean, attributes);
        //把上一步构造的ReferenceBean实例注册成Spring Bean
        registerReferenceBean(referencedBeanName, referenceBean, attributes, injectedType);

        cacheInjectedReferenceBean(referenceBean, injectedElement);
        // 初始化Proxy
        return getOrCreateProxy(referencedBeanName, referenceBeanName, referenceBean, injectedType);
    }
}

这个类继承了AbstractAnnotationBeanPostProcessor,来自于alibaba-spring。作用就是扫描classpath所有类中,包含某个注解的Field,找到后就会回调doGetInjectedBean()方法,获取到这个Field所应该注入的对象实例,然后注入到Field中。
在上面的代码中,首先根据注解和Field信息,构建Bean Name。然后根据@Reference注解的属性配置新建一个ReferenceBean类的实例,将这个实例注册成一个Spring Bean。

这一步其实有一种情况并不会注册ReferenceBean,就是添加@Reference注解的字段有本地实现。比如,服务提供方往外暴露的服务本地也有引用,这种情况如果用户不是用的@Autowird而是用的@Reference注解,Dubbo并不会使用远程调用,仍然会注入本地的Bean实现。

上面注册的ReferenceBean实现了Spring的FactoryBean接口,所以Spring在注入的时候,其实是调用它的getObject()方法。而这个方法返回的就是上面最后一步构建的Proxy。构建Proxy的代码如下:

private Object getOrCreateProxy(String referencedBeanName, String referenceBeanName, ReferenceBean referenceBean, Class<?> serviceInterfaceType) {
        //如果@Reference引用的对象有本地的Bean实现,则直接使用java的动态代理返回本地bean的代理
        if (existsServiceBean(referencedBeanName)) { // If the local @Service Bean exists, build a proxy of ReferenceBean
            return newProxyInstance(getClassLoader(), new Class[]{serviceInterfaceType},
                    newReferencedBeanInvocationHandler(referencedBeanName));
        } else { 
            exportServiceBeanIfNecessary(referencedBeanName);
            //调用referenceBean的方法获取远程访问的代理类
            return referenceBean.get();
        }
    }

构建代理分两种情况,一种是引用的接口本地有实现并注册成Spring Bean的情况(并且@Reference配置中要求的协议是injvm,即本地调用),直接走java的动态代理访问这个Bean实例就可以了,就是普通的本地调用。另外一种就是调用referenceBean.get()生成远程代理返回。

生成远程代理

代码中每个@Reference注解注入的都是referenceBean返回的一个代理,下面看下referenceBean.get()方法的实现。

    public synchronized T get() {
        if (destroyed) {
            throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
        }
        if (ref == null) {
            init();
        }
        return ref;
    }

get() 方法中只是检查一下ref有没有值,有说明之前已经初始化过了,直接返回,没有则初始化。

   public synchronized void init() {
        if (initialized) {
            return;
        }
        //1.初始化DubboBootstrap
        if (bootstrap == null) {
            bootstrap = DubboBootstrap.getInstance();
            bootstrap.init();
        }
        //2. 检查和更新Config
        checkAndUpdateSubConfigs();
        //3. 如果配置了Local和Stub则做检查
        checkStubAndLocal(interfaceClass);
        //4. 如果配置了mock则做检查
        ConfigValidationUtils.checkMock(interfaceClass, this);

        Map<String, String> map = new HashMap<String, String>();
        map.put(SIDE_KEY, CONSUMER_SIDE);

        ReferenceConfigBase.appendRuntimeParameters(map);
        // 5. 将所有配置参数做一次merge
        ...
        ...
        //6. 根据merge好的参数构建proxy
        ref = createProxy(map);

        serviceMetadata.setTarget(ref);
        serviceMetadata.addAttribute(PROXY_CLASS_REF, ref);
        ConsumerModel consumerModel = repository.lookupReferredService(serviceMetadata.getServiceKey());
        consumerModel.setProxyObject(ref);
        consumerModel.init(attributes);

        initialized = true;

        // 触发一个Reference初始化完成的event
        dispatch(new ReferenceConfigInitializedEvent(this, invoker));
    }

整个初始化过程逻辑比较复杂,但是主要就是参数检查和合并。第一步初始化DubboBootstrap,如果有配置中心则会获取最新配置。这里解释下为什么要合并参数,因为dubbo中同一个参数可以在不同的配置中都设置。比如接口调用超时,可以设置在方法级别、服务接口级别,也可以consumer级别设置。所以在初始化的时候需要将这些设置做合并然后去重,module的配置可以覆盖application的配置,consumer的配置可以覆盖module的,以此类推。这里就不解释这些参数具体意义了,感兴趣的可以对照文档看下源码。在参数准备好之后调用了createProxy()方法创建代理。
合并Invoker

private T createProxy(Map<String, String> map) {
        //1. 判断是否是进程内调用
        if (shouldJvmRefer(map)) {
            URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
            invoker = REF_PROTOCOL.refer(interfaceClass, url);
            if (logger.isInfoEnabled()) {
                logger.info("Using injvm service " + interfaceClass.getName());
            }
        } else {
            urls.clear();
            //2. 判断用户是否配置了对端地址
            if (url != null && url.length() > 0) { 
                String[] us = SEMICOLON_SPLIT_PATTERN.split(url);
                if (us != null && us.length > 0) {
                    for (String u : us) {
                        URL url = URL.valueOf(u);
                        if (StringUtils.isEmpty(url.getPath())) {
                            url = url.setPath(interfaceName);
                        }
                       //3. 地址是否是注册中心的地址
                        if (UrlUtils.isRegistry(url)) {
                            urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
                        } else {
                            urls.add(ClusterUtils.mergeUrl(url, map));
                        }
                    }
                }
            } else { 
                if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
                    // 4. 检查是否有注册中心配置,没有会抛出异常
                    checkRegistry();
                    // 5. 获取所有配置中心的url
                    List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
                    if (CollectionUtils.isNotEmpty(us)) {
                        for (URL u : us) {
                            URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
                            if (monitorUrl != null) {
                                map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
                            }
                            // 6. 是注册中心地址,则转换url 
                            urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
                        }
                    }
                    if (urls.isEmpty()) {
                        throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
                    }
                }
            }

            if (urls.size() == 1) {
                // 7. 如果只获取到一个url,则直接通过Protocol获取Invoker
                invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
            } else {
                // 8. 如果是多个地址,则生成多个invoker
                List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
                URL registryURL = null;
                for (URL url : urls) {
                    invokers.add(REF_PROTOCOL.refer(interfaceClass, url));
                    if (UrlUtils.isRegistry(url)) {
                        // 9. 声明了多个注册中心的地址,默认采用最后一个
                        registryURL = url; 
                    }
                }
                if (registryURL != null) {
                    // 10. 如果多个地址中有注册中心的地址,则使用ZoneAwareCluster
                    URL u = registryURL.addParameterIfAbsent(CLUSTER_KEY, ZoneAwareCluster.NAME);
                    invoker = CLUSTER.join(new StaticDirectory(u, invokers));
                } else { 
                    // 11. 所有的地址都是直连地址,直接用这些地址生成ClusterInvoker
                    invoker = CLUSTER.join(new StaticDirectory(invokers));
                }
            }
        }
        //检查服务是否在线
        if (shouldCheck() && !invoker.isAvailable()) {
            throw new IllegalStateException("Failed to check the status of the service "
                    + interfaceName
                    + ". No provider available for the service "
                    + (group == null ? "" : group + "/")
                    + interfaceName +
                    (version == null ? "" : ":" + version)
                    + " from the url "
                    + invoker.getUrl()
                    + " to the consumer "
                    + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
        }
        if (logger.isInfoEnabled()) {
            logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
        }
        /**
         * @since 2.7.0
         * ServiceData Store
         */
        String metadata = map.get(METADATA_KEY);
        WritableMetadataService metadataService = WritableMetadataService.getExtension(metadata == null ? DEFAULT_METADATA_STORAGE_TYPE : metadata);
        if (metadataService != null) {
            URL consumerURL = new URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), 0, map.get(INTERFACE_KEY), map);
            metadataService.publishServiceDefinition(consumerURL);
        }
        // 使用得到的Invoker生成接口代理
        return (T) PROXY_FACTORY.getProxy(invoker);
    }

在之前白话dubbo系列中讲过,Dubbo生成的代理类是通过Invoker来发送和接收请求的。所以,生成代理之前首先是初始化Invoker。本文的开头讲到,Dubbo的所有配置都是通过URL来传递的,所以在方法得开头是获取所有得url。这个url有可能是@Reference注解上直接配置得,也有可能是注册中心。下面按步骤分解一下:

  1. 判断是否是进程内调用,比如配置的@Reference(url="injvm://localhost:28080"),这种方式会直接走进程内调用
  2. 判断用户是否在@Reference注解上直接配置了url参数,如果用户配置了则会忽略注册中心的配置
  3. 如果配置的是注册中心的地址,比如@Reference(url="registry://localhost:28080"),在会将配置参数加在refer参数中,比如registry://localhost:28080?refer=version%3f1.0.0
  4. 如果没有配置url,则检查是否有注册中心,没有的话会抛出异常
  5. 获取所有注册中心的url
  6. 注册中心url添加refer参数,同第3步
  7. 如果最终只有一个url,则使用Protocol的refer方法获取到invoker
  8. 如果最终由多个url,就生成多个Invoker。
  9. 这里还会判断这些url里面有没有注册中心的地址
  10. 如果有注册中心,就会在最后一个注册中心url中加一个cluster=zone-aware的参数
  11. 最终会将多个invoker合并成一个ClusterInvoker

这里多提一句cluster=zone-aware参数的作用,当dubbo发现一个ReferenceBean关联到多个注册中心的时候,在发送请求时需要一个优先策略。加了这个参数就表明,优先选择同一个可用区的的注册中心。

创建代理
获取到Invoker后,就可以使用Invoker生成代理了,最后一步是调用ProxyFactory.getProxy()获取到接口代理。而生成的代理类里通过传入的Invoker参数来发起请求和处理response。

注解方式初始化总结

Dubbo + Spring的组合是当前最流行的使用方式,应用通过@EnableDubbo注解来激活Dubbo。Dubbo首先读取外部配置生成配置类并将配置类注册为SpringBean,然后扫描classpath下所有类,找到添加@Reference注解的字段,从而得出有哪些接口需要生成远程代理。对于每个接口的每个配置都会注册一个ReferenceBean的Spring Bean,同时调用它的get()方法根据配置生成URL,进而创建Proxy。生成的Proxy通过Spring注入到业务类中。Proxy生成将在下一篇文章解析。

其它初始化方式

Spring xml配置

Dubbo不光可以通过Spring注解的方式来初始化,在早期用的更多的是通过xml配置文件的方式。将dubbo相关配置写到Spring的application-context.xml配置文件中,利用Spring的扩展机制来初始化Dubbo。由于这种方式的使用越来越少,详细原理就不解释了,感兴趣的可以看下org.apache.dubbo.config.spring.schema.DubboNamespaceHandler的实现

非Spring方式

Dubbo并不是和Spring紧耦合,它可以脱离Spring单独使用,可以通过org.apache.dubbo.config.bootstrap.DubboBootstrap直接初始化和启动Dubbo运行环境。

上一篇下一篇

猜你喜欢

热点阅读