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生命周期的运用十分值得学习和借鉴。

相关推荐
Jasonakeke29 分钟前
Flask 处理响应
后端·python·flask
宋发元1 小时前
使用Go语言绘制水平柱状图教程
开发语言·后端·golang
九局下半1 小时前
【SkyWalking】如何在业务系统中控制SkyWalkingAgent的生命周期
后端
xcLeigh1 小时前
html实现好看的塔罗牌、十二星座运势网站源码
前端·html·源码·星座·占卜
一然明月2 小时前
ASP.NET Core 基础 - 入门实例
后端·asp.net
一只懒鱼a3 小时前
SpringBoot之外部化配置
java·spring boot·后端·spring
小码王科技3 小时前
免费【2024】springboot 二手家电管理平台的设计与实现
java·spring boot·后端·毕业设计
Slow菜鸟3 小时前
SpringBoot教程(二十) | SpringBoot整合异步任务
java·spring boot·后端
猿究院-张睿泽4 小时前
Spring的基本概念和结构
java·开发语言·后端·mysql·spring
llovew.4 小时前
Django异步请求和后台管理实战
后端·python·ajax·django