2018-10-22 · Develop

Java 配置文件-Disconf

上篇文章 如何在 CentOS 7 上安装 Disconf 我们将 disconf-web 搭建成功。就该重构项目,使用上 disconf 了。

本文不是对 disconf 的使用教程,具体的使用方式请参考 官方文档

这篇文章主要针对项目已经使用 ConfigUtil 和 Spring 管理 properties 的情况下,进行小幅度的重构。

现在的项目使用 Spring 4.x 版本,里面一堆 xml 文件通过 <context:property-placeholder/> 进行读取,然后替换 xml 中的占位符 ${key}

然后大量的 JavaBean 中使用 ConfigUtil 通过文件流读取 properties 文件,然后通过 ConfigUtil.getInstance().getString(key) 进行访问。

为了减少重构量,保证系统能够正常的运行,采用了文档中 7. Tutorial 8 基于XML的分布式配置文件管理,自动reload 的方式进行重构。具体重构过程如下:

Step 1 - 上传配置文件

将需要进行管理的配置上传到 disconf-web 里。

Step 2 - 添加基本的 disconf 支持

在所有的 spring 配置中,我们保证下面添加的要 Spring 容器启动时最早扫描。

<!-- 使用disconf必须添加以下配置 -->
<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean" destroy-method="destroy">
    <property name="scanPackage" value="com.example.disconf.demo"/>
</bean>

<bean id="disconfMgrBean2" class="com.baidu.disconf.client.DisconfMgrBeanSecond" init-method="init" destroy-method="destroy" />

Step 3 - 添加需要进行托管的配置文件

<!-- 使用托管方式的disconf配置(无代码侵入, 配置更改会自动reload)-->
<bean id="configproperties_disconf" class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">
    <property name="locations">
        <list>
            <value>classpath:/autoconfig.properties</value>
            <value>classpath:/autoconfig2.properties</value>
        </list>
    </property>
</bean>

<bean id="propertyConfigurer" class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer">
    <property name="ignoreResourceNotFound" value="true" />
    <property name="ignoreUnresolvablePlaceholders" value="true" />
    <property name="propertiesArray">
        <list>
            <ref bean="configproperties_disconf"/>
        </list>
    </property>
</bean>

在第一步重构中不需要达到动态的切换数据源等功能。所以到此,在后面进行扫描的 xml 文件就可以使用 <property name="auto" value="${auto=100}"/> 进行获取配置了。
但是对于使用 ConfigUtil 进行读取配置的并不能获得 reload 的配置。那我们就来解决这个问题,上面 Step 3 注册的 ReloadingPropertyPlaceholderConfigurer 里面有这么两段源码:

/**
    * copy & paste, just so we can insert our own visitor.
    * 启动时 进行配置的解析
    */
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {

    BeanDefinitionVisitor visitor = new ReloadingPropertyPlaceholderConfigurer.PlaceholderResolvingBeanDefinitionVisitor(props);
    String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
    for (int i = 0; i < beanNames.length; i++) {
        // Check that we're not parsing our own bean definition,
        // to avoid failing on unresolvable placeholders in net.unicon.iamlabs.spring.properties.example.net
        // .unicon.iamlabs.spring.properties file locations.
        if (!(beanNames[i].equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
            this.currentBeanName = beanNames[i];
            try {
                BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(beanNames[i]);
                try {
                    visitor.visitBeanDefinition(bd);
                } catch (BeanDefinitionStoreException ex) {
                    throw new BeanDefinitionStoreException(bd.getResourceDescription(), beanNames[i],
                            ex.getMessage());
                }
            } finally {
                currentBeanName = null;
            }
        }
    }

    StringValueResolver stringValueResolver = new PlaceholderResolvingStringValueResolver(props);

    // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
    beanFactoryToProcess.resolveAliases(stringValueResolver);

    // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
    beanFactoryToProcess.addEmbeddedValueResolver(stringValueResolver);
}

这个方法重写了 PropertyPlaceholderConfigurer 的方法,进行了扩展。传进来的 Properties props 就是管理的所以配置,我们可以将其保存起来,这样就完成了初始化部分,还需完成的是 reload 功能。在看下面这段代码

private Properties lastMergedProperties;

/**
    * merge property and record last merge
    *
    * @return
    *
    * @throws IOException
    */
protected Properties mergeProperties() throws IOException {
    Properties properties = super.mergeProperties();
    this.lastMergedProperties = properties;
    return properties;
}

/**
    * 当配置更新时,被调用
    *
    * @param event
    */
public void propertiesReloaded(PropertiesReloadedEvent event) {

    Properties oldProperties = lastMergedProperties;

    try {
        //
        Properties newProperties = mergeProperties();

        //
        // 获取哪些 dynamic property 被影响
        //
        Set<String> placeholders = placeholderToDynamics.keySet();
        Set<DynamicProperty> allDynamics = new HashSet<DynamicProperty>();
        for (String placeholder : placeholders) {
            String newValue = newProperties.getProperty(placeholder);
            String oldValue = oldProperties.getProperty(placeholder);
            if (newValue != null && !newValue.equals(oldValue) || newValue == null && oldValue != null) {
                if (logger.isInfoEnabled()) {
                    logger.info("Property changed detected: " + placeholder +
                            (newValue != null ? "=" + newValue : " removed"));
                }
                List<DynamicProperty> affectedDynamics = placeholderToDynamics.get(placeholder);
                allDynamics.addAll(affectedDynamics);
            }
        }

        ...

    } catch (IOException e) {
        logger.error("Error trying to reload net.unicon.iamlabs.spring.properties.example.net.unicon.iamlabs" +
                ".spring" + ".properties: " + e.getMessage(), e);
    }
}

通过上面的代码知道,在收到 disconf-web 端配置变化时,调用 propertiesReloaded 方法,在里面进行合并配置操作,即调用 mergeProperties 方法。
那我们就可以将合并后的 properties 替换我们上面保存的 properties ,这样就实现了 reload 功能。具体实现如下:

public class PropertyConfigurer extends ReloadingPropertyPlaceholderConfigurer {

    private static final Logger log = LoggerFactory.getLogger(PropertyConfigurer.class);

    /**
     * 存取properties配置文件key-value结果
     */
    private Properties props;

    @Override
    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {
        super.processProperties(beanFactoryToProcess, props);
        this.props = props;
    }

    public String getProperty(String key) {
        return props.getProperty(key);
    }

    public String getProperty(String key, String defaultValue) {
        return props.getProperty(key, defaultValue);
    }

    @Override
    public void propertiesReloaded(PropertiesReloadedEvent event) {
        super.propertiesReloaded(event);
        try {
            props = super.mergeProperties();
        } catch (IOException e) {
            log.error("ReLoad 配置文件失败。", e);
        }
    }
}

我们自定义一个继承 ReloadingPropertyPlaceholderConfigurer 的类,在 Step 3 中初始化自定义的类。

<!--<bean id="propertyConfigurer" class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer">-->
<bean id="propertyConfigurer" class="com.example.disconf.demo.PropertyConfigurer">
    <property name="ignoreResourceNotFound" value="true" />
    <property name="ignoreUnresolvablePlaceholders" value="true" />
    <property name="propertiesArray">
        <list>
            <ref bean="configproperties_disconf"/>
        </list>
    </property>
</bean>

现在的问题是 ConfigUtil 是一个单例模式的类,那如何将 bean propertyConfigurer 注册为 ConfigUtil 的属性呢?看下面的重构:

public class ConfigUtil {

    private ConfigUtil() {
        propertyConfigurer = SpringContextUtil.getBean(PropertyConfigurer.class);
    }

    private PropertyConfigurer propertyConfigurer;

    private static class ConfigBuilder {
        private static ConfigUtil instance = new ConfigUtil();
    }

    public static ConfigUtil getInstance() {
        return ConfigBuilder.instance;
    }

    public String getString(String key) {
        return propertyConfigurer.getProperty(key);
    }
    
    ...
}

这样我们就可以正常使用了,那 SpringContextUtil 是个什么鬼?

public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    ...
}

说白了就是将单例的属性经过静态方法获取,在静态成员变量是可以进行注入的,如果你的 ConfigUtil 是通过静态方法对位提供服务的话,就不需要进行这一圈的饶了。