dubbo.properties占位符源码分析

一、背景

我们团队的系统最近在进行商业化交付改造。为了能够达到快速、灵活、开放、稳定的目的,在架构层面进行了调整。其中在RPC通信层面,公司内部使用的是自研的JSF中间件。这次商业化改造决定采用DUBBO。

整体工程基于JDK17+Springboot3+DUBBO3

在商业化中,有些客户可能因为资源预算紧张,要求我们尽可能压缩资源。DUBBO本身是一款非常优秀的轻量级RPC开源框架,其3.x版本更稳定、更开放,很多组件都支持扩展,十分适合我们的诉求。我们可以采用直连模式从而减少注册中心的资源成本。

DUBBO支持丰富的配置方式,我们采用的是dubbo.properties配置文件的方式。配置示例如下:

properties 复制代码
dubbo.reference.com.jd.xxx.xxx.lite.api.org.OrgStaffInternalRpcService.url=dubbo://${DUBBO_ADDRESS}/com.jd.xxx.xxx.lite.api.org.OrgStaffInternalRpcService

注意看,这里采用了${DUBBO_ADDRESS}作为占位符,这样我们就可以通过环境变量实现配置化。假设我们是通过系统环境变量进行配置。在系统环境变量中添加:DUBBO_ADDRESS=127.0.0.1:20051,在启动过程中会自动解析。

二、这是Spring EL表达式么?

乍一看,${DUBBO_ADDRESS}和Spring EL表达式很像。就以为这是DUBBO在Spring生态下支持了Spring EL表达式。有一种理所当然的感觉。但本着有疑惑就要解决疑惑的原则,为了验证这个猜测,去官方文档寻找答案,结果发现官方没有这方面的资料。没办法,只能结合源码寻找答案了。

三、源码解析

Dubbo的初始化过程充分依赖Spring的启动过程。在Spring启动过程的每个阶段都做了很多事情。总体可分为三部分:

1)Reference的BeanDefinition初始化 2)DubboConfig初始化

3)DubboReferenceConfig填充

(一)ReferenceAnnotationBeanPostProcessor---Reference的BeanDefinition初始化

这个类是一个非常关键的类。分别继承了BeanFactoryPostProcessorApplicationContextAware等Spring的扩展类。做了很多关键的事前准备。其关键源码如下:

java 复制代码
public class ReferenceAnnotationBeanPostProcessor extends AbstractAnnotationBeanPostProcessor
        implements ApplicationContextAware, BeanFactoryPostProcessor {
        
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
    // 关键代码!!!通过主动触发getBean方法,实例化referenceBeanManager这个Bean,
    // ReferenceBeanManager是用于管理Reference的。包含了对Reference的详细解析等。
    this.referenceBeanManager =
            applicationContext.getBean(ReferenceBeanManager.BEAN_NAME, ReferenceBeanManager.class);
    this.beanDefinitionRegistry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();
}
        
// 实现BeanFactoryPostProcessor的回调方法。用于识别含有@DubboReference注解的SpringBeanDefinition
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    String[] beanNames = beanFactory.getBeanDefinitionNames();
    for (String beanName : beanNames) {
        Class<?> beanType;
        // 是否是FactoryBean,不需要过多关注。
        if (beanFactory.isFactoryBean(beanName)) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
            if (isReferenceBean(beanDefinition)) {
                continue;
            }
            if (isAnnotatedReferenceBean(beanDefinition)) {
                processReferenceAnnotatedBeanDefinition(beanName, (AnnotatedBeanDefinition) beanDefinition);
                continue;
            }

            String beanClassName = beanDefinition.getBeanClassName();
            beanType = ClassUtils.resolveClass(beanClassName, getClassLoader());
        } else {
            beanType = beanFactory.getType(beanName);
        }
        if (beanType != null) {
            AnnotatedInjectionMetadata metadata = findInjectionMetadata(beanName, beanType, null);
            try {
                // 关键代码!!!在这个方法里面寻找当前的BeanDefination中源码是否含有@DubboReference/@Reference注解。
                // 如果含有,则将@DubboReference或@Reference注解的成员属性注册为一个BeanDefinition。同时将Reference加入到referenceBeanManager中,用于后续流程的进一步处理。
                // tips:referenceBeanManager是在上面通过getBean手动触发Bean的实例化产生的。
                prepareInjection(metadata);
            } catch (BeansException e) {
                throw e;
            } catch (Exception e) {
                //...
            }
        }
    }

    if (beanFactory instanceof AbstractBeanFactory) {
        List<BeanPostProcessor> beanPostProcessors = ((AbstractBeanFactory) beanFactory).getBeanPostProcessors();
        for (BeanPostProcessor beanPostProcessor : beanPostProcessors) {
            if (beanPostProcessor == this) {
                beanDefinitionRegistry.removeBeanDefinition(BEAN_NAME);
                break;
            }
        }
    }

    try {
        // 关键代码!!!发布DubboConfigInitEvent事件。触发DubboConfigApplicationListener监听器
        applicationContext.publishEvent(new DubboConfigInitEvent(applicationContext));
    } catch (Exception e) {
        // ...
    }
}
}

通过继承BeanFactoryPostprocessor成功识别出Reference这是关键的第一步

(二)DubboConfigApplicationListener

DubboConfigApplicationListener监听DubboConfigInitEvent事件。它主要干的事情就是实例化DubboConfigBeanInitializer以及加载基础的Config。核心代码如下:

java 复制代码
/**
 * An ApplicationListener to load config beans. 用于加载ConfigBean
 */
public class DubboConfigApplicationListener
        implements ApplicationListener<DubboConfigInitEvent>, ApplicationContextAware {
        @Override
    public void onApplicationEvent(DubboConfigInitEvent event) {
        if (nullSafeEquals(applicationContext, event.getSource())) {
            init(); // 委托
        }
    }

    public void init() {
        if (initialized.compareAndSet(false, true)) {
            initDubboConfigBeans(); // 委托
        }
    }

    private void initDubboConfigBeans() {
        // 在这里if条件必然会命中,因为此时spring生命周期中已经开始了所有的Bean的实例化。
        // 这个地方再通过spring加载bean的机制,通过getBean方法主动触发 DubboConfigBeanInitializer的实例化。
        if (applicationContext.containsBean(DubboConfigBeanInitializer.BEAN_NAME)) {
            applicationContext.getBean(DubboConfigBeanInitializer.BEAN_NAME, DubboConfigBeanInitializer.class);
        } else {
            // ...
        }

        // 在这里,调用Deployer的prepare()方法,先进行一部分初始化的动作。
        // 后文中会重点分析Deployer类。
        moduleModel.getDeployer().prepare();
    }
        
        }

在上面源码中分析到:方法中通过主动调用applicationContext.getBean方法,触发DubboConfigBeanInitializer的实例化;

DubboConfigBeanInitializer实际上是一个FactoryBean,也干了很关键的事情。

Tips:Spring生命周期中,Bean的实例化和Bean的初始化完全是两个不同的阶段。实例化在先,初始化在后。

(三)DubboConfigBeanInitializer

它继承了InitializingBeanInitializingBean是Spring提供的一个扩展类。可以在Bean初始化完成之后回调afterPropertiesSet()方法。目的是实例化Reference对应的ReferenceConfig为后续解析ReferenceConfig做准备。源码如下:

java 复制代码
public class DubboConfigBeanInitializer implements BeanFactoryAware, InitializingBean {
@Override
    public void afterPropertiesSet() throws Exception {
        init(); // 委托
    }

    private void init() {
        if (initialized.compareAndSet(false, true)) {
            // 从Spring容器中获��referenceBeanManager
            referenceBeanManager = beanFactory.getBean(ReferenceBeanManager.BEAN_NAME, ReferenceBeanManager.class);
            try {
                // 只是用于设置Dubbo支持的基础的Config类型
                prepareDubboConfigBeans();
                //关键代码!!!触发referenceBean的解析
                referenceBeanManager.prepareReferenceBeans();
            } catch (Throwable e) {
                // ...
            }
        }
    }

}

(四)referenceBeanManager.prepareReferenceBeans()

在上文中最为关键的是referenceBeanManager.prepareReferenceBeans();。这个方法主要作用就是初始化referenceBeans.源码如下:

java 复制代码
public void prepareReferenceBeans() throws Exception {
    initialized = true;
    // getReferences()方法获取referenceBean集合。在ReferenceAnnotationBeanPostProcessor中初始化ReferenceBean并添加到this.referenceBeanMap容器中的。
    for (ReferenceBean referenceBean : getReferences()) {
        initReferenceBean(referenceBean); // 委托
    }
}


public Collection<ReferenceBean> getReferences() {
    return new HashSet<>(referenceBeanMap.values());
}


public synchronized void initReferenceBean(ReferenceBean referenceBean) throws Exception {

    if (referenceBean.getReferenceConfig() != null) {
        return;
    }

    // reference key
    String referenceKey = getReferenceKeyByBeanName(referenceBean.getId());
    if (StringUtils.isEmpty(referenceKey)) {
        referenceKey = ReferenceBeanSupport.generateReferenceKey(referenceBean, applicationContext);
    }

    ReferenceConfig referenceConfig = referenceConfigMap.get(referenceKey);
    if (referenceConfig == null) {
        // 关键代码!!!实例化referenceBean对应的ReferenceConfig。
        // dubbo中ReferenceBean的属性都是通过ReferenceConfig进行配置的。
        // 在这里,先实例化处ReferenceConfig,后续会进一步解析并完善ReferenceConfig里面的属性值。
        Map<String, Object> referenceAttributes = ReferenceBeanSupport.getReferenceAttributes(referenceBean);
        referenceConfig = ReferenceCreator.create(referenceAttributes, applicationContext)
                .defaultInterfaceClass(referenceBean.getObjectType())
                .build();

        // set id if it is not a generated name
        if (referenceBean.getId() != null && !referenceBean.getId().contains("#")) {
            referenceConfig.setId(referenceBean.getId());
        }

        // cache referenceConfig
        referenceConfigMap.put(referenceKey, referenceConfig);

        // register ReferenceConfig
        moduleModel.getConfigManager().addReference(referenceConfig);
        moduleModel.getDeployer().setPending();
    }

    // 将 referenceConfig 绑定到对应的 referenceBean
    referenceBean.setKeyAndReferenceConfig(referenceKey, referenceConfig);
}

到这里,完成了ReferenceBean对应的Config的实例化。Reference和ReferenceConfig也有了映射关系。

(五)DubboDeployApplicationListener

这又是一个监听器。它监听的是ApplicationContextEvent。Spring在完成所有Bean的初始化以及相关扩展接口执行完成之后,会发布ContextRefreshedEvent,它继承了ApplicationContextEvent。源码地址在:AbstraceApplicationContext#finishRefresh()方法中。

Dubbo在这个监听器中完成reference最终的加载步骤,其中就包括填充属性。核心源码如下:

java 复制代码
public class DubboDeployApplicationListener
        implements ApplicationListener<ApplicationContextEvent>, ApplicationContextAware, Ordered {
        ```
@Override
public int getOrder() { // 所有监听器中,执行优先级最低,确保SpringBean的正确性。
    return LOWEST_PRECEDENCE;
}

@Override
public void onApplicationEvent(ApplicationContextEvent event) {
    if (nullSafeEquals(applicationContext, event.getSource())) {
        if (event instanceof ContextRefreshedEvent) {
            // 关键代码!!!委托
            onContextRefreshedEvent((ContextRefreshedEvent) event);
        } else if (event instanceof ContextClosedEvent) {
            onContextClosedEvent((ContextClosedEvent) event);
        }
    }
}
}

private void onContextRefreshedEvent(ContextRefreshedEvent event) {
        ModuleDeployer deployer = moduleModel.getDeployer();
        Assert.notNull(deployer, "Module deployer is null");
        Object singletonMutex = LockUtils.getSingletonMutex(applicationContext);
        // start module
        Future future = null;
        synchronized (singletonMutex) {
       
            // 关键代码!!!核心入口。
            future = deployer.start();
        }

        // if the module does not start in background, await finish
        if (!deployer.isBackground()) {
            try {
                future.get();
            } catch (InterruptedException e) {
                // ...
            } catch (Exception e) {
                // ...
            }
        }
    }

通过源码分析,最终是委托给deployer完成最终的装配。

(六)DefaultModuleDeployer

在Dubbo中,DefaultModuleDeployer是一个非常核心的组件。可以完成加载配置、初始化组件、注册服务等功能。start()方法是入口。

java 复制代码
public class DefaultModuleDeployer extends AbstractDeployer<ModuleModel> implements ModuleDeployer {

    @Override
    public Future start() throws IllegalStateException {
        // 调用初始化方法。
        applicationDeployer.initialize();
        // 调用startSync方法。
        return startSync();
    }

    @Override
    public void initialize() { 
        // 在DubboApplicationListener中,执行了preper方法,就已经进行了初始化。
        // 所以在DubboDeployApplicationListener中执行初始化方法时,不会再执行下面的代码逻辑了
        if (initialized) {
            return;
        }
        // Ensure that the initialization is completed when concurrent calls
        synchronized (startLock) {//双重校验锁保证原子操作。
            if (initialized) {
                return;
            }
            onInitialize();

            // register shutdown hook
            registerShutdownHook();

            startConfigCenter();

            // 重要。在这里面完成核心Dubbo的config的加载。比如dubbo.application/dubbo.registries等。但不包括dubbo.reference
            loadApplicationConfigs();

            initModuleDeployers();

            initMetricsReporter();

            initMetricsService();

            // @since 2.7.8
            startMetadataCenter();
            
            // 变更初始化状态
            initialized = true;
        }
    }
    
    private synchronized Future startSync() throws IllegalStateException {
    if (isStopping() || isStopped() || isFailed()) {
        throw new IllegalStateException(getIdentifier() + " is stopping or stopped, can not start again");
    }

    try {
        if (isStarting() || isStarted()) {
            return startFuture;
        }

        onModuleStarting();

        // 实际上不会再执行初始化方法了,原因在上面说了
        initialize();

        // export services
        exportServices();

        // prepare application instance
        // exclude internal module to avoid wait itself
        if (moduleModel != moduleModel.getApplicationModel().getInternalModule()) {
            applicationDeployer.prepareInternalModule();
        }

        // 关键代码!!! 对服务进行配置处理。
        referServices();

        // 省略...

    return startFuture;
}

private void referServices() {
    // 从ReferenceConfigManager中获取referenceConfigs并进行刷新。
    configManager.getReferences().forEach(rc -> {
        try {
            ReferenceConfig<?> referenceConfig = (ReferenceConfig<?>) rc;
            if (!referenceConfig.isRefreshed()) {
                // 关键代码!!!调用刷新方法。
                referenceConfig.refresh();
            }
            // ...
        } catch (Throwable t) {
            // ...
            referenceCache.destroy(rc);
            throw t;
        }
    });
}
}

通过上面的源码解析,最终来到了配置解析的最后一个关键源码处。

(七)AbstruseConfig#refresh

在这个方法里面,进行了最终的解析。源码如下:

java 复制代码
public abstract class AbstractConfig implements Serializable {

public void refresh() {
    if (needRefresh) {
        try {
            // 关键代码!!!在刷新之前做一些事情。
            // reference场景下,其子类实现方法是:ReferenceConfigBase#preProcessRefresh。
            // 在子类里面也调用了refreshWithPrefixes(getPrefixes(), getConfigMode());方法
            // 不同的是,在ReferenceConfigBase#preProcessRefresh中,getPrefixes()获取到的是dubbo.reference的配置前缀。
            preProcessRefresh();
            // 而在当前方法中,getPrefixes()获取到的前缀是:
            // dubbo.reference.com.jd.xxx.xxx.lite.api.org.OrgStaffInternalRpcService
            // 这正是我们在dubbo.properties中的配置。
            refreshWithPrefixes(getPrefixes(), getConfigMode());
        } catch (Exception e) {
            // ...
        }

        postProcessRefresh();
    }
    refreshed.set(true);
}

/**
 *根据配置前缀刷新
*/
protected void refreshWithPrefixes(List<String> prefixes, ConfigMode configMode) {
    // 获取环境对象。
    // 这里的环境对象是Dubbo的自己封装的环境对象,持有系统环境变量/JVM环境变量以及Dubbo自身的环境变量配置。
    // 这就是为什么Dubbo支持丰富的配置方式的原因。
    Environment environment = getScopeModel().modelEnvironment();
    // 获取所有的环境变量。
    // key->环境变量类型。比如:系统环境变量/JVM环境变量等。
    // value-> 对应的变量配置。
    List<Map<String, String>> configurationMaps = environment.getConfigurationMaps();

    // 确认最终的前缀。
    // 以Reference场景为例,如果是this.refresh()方法中调用的话,那么这里的Prefix就是dubbo.properties中的配置前缀
    String preferredPrefix = null;
    for (String prefix : prefixes) {
        if (ConfigurationUtils.hasSubProperties(configurationMaps, prefix)) {
            preferredPrefix = prefix;
            break;
        }
    }
    if (preferredPrefix == null) {
        preferredPrefix = prefixes.get(0);
    }
    // Extract sub props (which key was starts with preferredPrefix)
    Collection<Map<String, String>> instanceConfigMaps = environment.getConfigurationMaps(this, preferredPrefix);
    // 关键代码!!!获取指定前缀的属性配置。
    // exp:
    // config->dubbo.reference.com.jd.xxx.xxx.lite.api.org.OrgStaffInternalRpcService.url
    // prefix->dubbo.reference.com.jd.xxx.xxx.lite.api.org.OrgStaffInternalRpcService
    // subProperty->url
    // 这样我们就可以解析url的配置了
    Map<String, String> subProperties = ConfigurationUtils.getSubProperties(instanceConfigMaps, preferredPrefix);
    InmemoryConfiguration subPropsConfiguration = new InmemoryConfiguration(subProperties);

    // 关键代码!!!填充配置
    // 我们的配置是dubbo://${DUBBO_ADDRESS}/com.jd.xxx.xxx.lite.api.org.OrgStaffInternalRpcService
    // 通过填充配置对${DUBBO_ADDRESS}占位符进行替换。
    assignProperties(this, environment, subProperties, subPropsConfiguration, configMode);

    // process extra refresh of subclass, e.g. refresh method configs
    processExtraRefresh(preferredPrefix, subPropsConfiguration);
}

private void assignProperties(
        Object obj,
        Environment environment,
        Map<String, String> properties,
        InmemoryConfiguration configuration,
        ConfigMode configMode) {
    // if old one (this) contains non-null value, do not override
    // 覆盖模式。即根据配置优先级确认最终有效的配置。本示例中只有dubbo.properties配置
    boolean overrideIfAbsent = configMode == ConfigMode.OVERRIDE_IF_ABSENT;

    // even if old one (this) contains non-null value, do override
    boolean overrideAll = configMode == ConfigMode.OVERRIDE_ALL;

    // loop methods, get override value and set the new value back to method
    // 关键代码!!!通过反射机制获取到ReferenceConfig所有的method方法,其中就包括setUrl
    List<Method> methods =
            MethodUtils.getMethods(obj.getClass(), method -> method.getDeclaringClass() != Object.class);
    for (Method method : methods) {
        // 关键代码!!!判断是否是setXXX
        if (MethodUtils.isSetter(method)) {
            String propertyName = extractPropertyName(method.getName());

            // if config mode is OVERRIDE_IF_ABSENT and property has set, skip
            if (overrideIfAbsent && isPropertySet(methods, propertyName)) {
                continue;
            }

            // 关键代码!!!格式化属性名称。
            // 本示例中,这里获取到的属性名称为url
            String kebabPropertyName = StringUtils.convertToSplitName(propertyName, "-");

            try {
            // 关键代码!!!获取属性对应的配置。
            // 本示例中,这里的value为:dubbo://${DUBBO_ADDRESS}/com.jd.xxx.xxx.lite.api.org.OrgStaffInternalRpcService
                String value = StringUtils.trim(configuration.getString(kebabPropertyName));

                if (StringUtils.hasText(value)
                        && ClassUtils.isTypeMatch(method.getParameterTypes()[0], value)
                        && !isIgnoredAttribute(obj.getClass(), propertyName)) {
                    // 关键代码!!!庐山真面目在此!
                    // 在这里解析占位符。
                    // 通过上文介绍道,这里的environment是Dubbo自己封装的。
                    // 也就意味着,解析也是Dubbo框架自己解析的。因为Dubbo支持丰富的参数配置,包括Dubbo自身提供的方式。所以必然不可能是Spring EL表达式的解析方式。
                    value = environment.resolvePlaceholders(value);
                    if (StringUtils.hasText(value)) {
                        Object arg = ClassUtils.convertPrimitive(
                                ScopeModelUtil.getFrameworkModel(getScopeModel()),
                                method.getParameterTypes()[0],
                                value);
                        if (arg != null) {
                            // 反射调用。
                            // 本示例中就是,反射调用setUrl()方法。
                            method.invoke(obj, arg);
                        }
                    }
                }
            } catch (Exception e) {
                // ...
        } else if (isParametersSetter(method)) { // 非setter方法的解析方式,原理一样的。
            // ...
        } else if (isNestedSetter(obj, method)) { 
            // ...
        }
    }
}

}

总结

通过以上源码分析,知道了dubbo.properties中占位符的解析原理。

此外,对于Dubbo作为一个开源框架的灵活性有了更深的感悟。

单纯从技术角度出发,Dubbo对于Spring生命周期的运用十分值得学习和借鉴。

相关推荐
李歘歘5 分钟前
Golang笔记——切片与数组
开发语言·笔记·后端·golang·go
raoxiaoya31 分钟前
golang中的eval,goeval,govaluate
开发语言·后端·golang
CyberScriptor1 小时前
PHP语言的软件工程
开发语言·后端·golang
GGBondlctrl1 小时前
【SpringAOP】Spring AOP 底层逻辑:切点表达式与原理简明阐述
java·后端·代理模式·spring aop·切点表达式
代码驿站5201 小时前
Scala语言的软件开发工具
开发语言·后端·golang
wlyang6661 小时前
2. Scala 高阶语法之集合与元组
开发语言·后端·scala
喜欢猪猪1 小时前
大厂架构之极致缓存策略实战与原理剖析
java·后端·spring
JINGWHALE11 小时前
设计模式 行为型 责任链模式(Chain of Responsibility Pattern)与 常见技术框架应用 解析
前端·人工智能·后端·设计模式·性能优化·系统架构·责任链模式
AI向前看2 小时前
PHP语言的函数实现
开发语言·后端·golang
Harry技术2 小时前
Harry技术添加存储(minio、aliyun oss)、短信sms(aliyun、模拟)、邮件发送等功能
vue.js·spring boot·后端