专栏首页BAT的乌托邦使用commons-configuration2读取properties、yaml、xml配置文件,实现配置文件的热更新【享学Java】

使用commons-configuration2读取properties、yaml、xml配置文件,实现配置文件的热更新【享学Java】

前言

Apache Commons Configuration是apache commons组件中的又一实用工具。它的官网介绍如下:

The Commons Configuration software library provides a generic configuration interface which enables a Java application to read configuration data from a variety of sources. 
Commons Configuration provides typed access to single, and multi-valued configuration parameters

大致意思为:

Commons Configuration软件库提供了一个**通用的**配置接口,它允许Java应用程序从**各种来源**读取配置数据。
Commons Configuration提供对单值和多值配置参数的**类型化**访问(getDouble、getInteger等等)

我从官网截图,展示它支持的资源类型

配置对象是使用配置生成器builder创建的。可以使用组合配置生成器(CombinedConfigurationCompositeConfiguration)和组合配置混合不同的配置源。 可以使用自定义配置对象创建其他配置参数源。这种定制可以通过扩展AbstractConfigurationAbstractHierarchicalConfiguration来实现。

注意:AbstractHierarchicalConfiguration是2.0版本才会有的。而它的2.x版本和1.x版本是不兼容的。官网也有如下说明:

以上表述都来自官网的说明,而我发现它是因为Spring Cloudfeign(feign-hystrix.jar)以及ribbon(ribbon-core.jar)中都有对它的依赖和使用,为了更好的研究好和自定义Spring Boot的配置管理,所以才有了本文,毕竟我认为设计思想都是相通的

备注:SpringBoot中并没有依赖此jar,需要注意的是Spring Cloud的feign等中引入的commons-configuration版本都是1.x的(哪怕到了最新的2.1.4版本都还是1.8版本),所以cloud还都是使用1.x的API

Commons Configuration的依赖jar问题

使用commons configuration的某些特殊功能的时候,需要依赖到另外一些jar需要你额外导入,截图官网的如下(此处以2.x版本为例,1.x版本的也差不太多~):

需要注意的是2.x版本后要求的JDK版本最低为JDK8~~~

需要注意的是,这些依赖jar都是只有你需要使用到对应功能的时候才需要,所以理论上都是optional可选的,按序插拔的即可

使用Demo(以2.x为例)

因为1.x确实已经"过期"很久了,所以本文的Demo都只会以2.x为例~ 它的一个核心入口是:org.apache.commons.configuration2.builder.fluent.Configurations,而它生成的org.apache.commons.configuration2.Configuration就代表着一个配置。

处理Properties属性文件

.properties是我们开发中建的最多的一种配置文件,没有之一。因此必须对它提供良好的支持:比如编码、占位符等功能我相信这是一个最基本的要求了。 导入依赖的jar:

<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.3</version>
</dependency>

在类路径下建立一个文件:my.properties

person.name=fsx
person.age=10

spring.application.name=mydemoshow

## 示范一个中文
country.zh=中国

写一个测试方法:

    public static void main(String[] args) throws ConfigurationException {
        Configurations configs = new Configurations();

        // 每个Configuration代表这一个配置文件~(依赖beanutils这个jar)
        Configuration config = configs.properties("my.properties");

        // 遍历
        Iterator<String> keys = config.getKeys();
        while (keys.hasNext()) {
            String key = keys.next();
            String value = config.getString(key);
            System.out.println(key + " = " + value);
        }
    }

输出结果为:

person.name = fsx
person.age = 10
spring.application.name = mydemoshow::fsx
country.zh = 中国

从结果可以看出: 1、它支持占位符 2、中文依旧还是乱码,怎么破呢??

解决中文乱码问题,只需要这么做:

    public static void main(String[] args) throws ConfigurationException {
        Configurations configs = new Configurations();

        // 设置编码,此处的实际是个PropertiesConfiguration,它默认采用的是`ISO-8859-1`所以中文乱码~
        // 注意:这个前提是你的properties文件是utf-8编码的~~~
        FileBasedConfigurationBuilder.setDefaultEncoding(PropertiesConfiguration.class, "UTF-8");
        // 每个Configuration代表这一个配置文件~(依赖beanutils这个jar)
        Configuration config = configs.properties("my.properties");
        ...
     }

另外,若你需要处理一些较为复杂的场景,比如希望对配置文件进行监听、前置后置处理等,可以使用builder模式如下:

...
		// 采用Builder模式处理更为复杂的一些场景   比如把逗号分隔的字符串解析到数组、解析到list、前后拼接字符串等等操作
		// 其实你直接configs.properties(...)它的内部原理也是builder模式~
        FileBasedConfigurationBuilder<PropertiesConfiguration> builder = configs.propertiesBuilder("my.properties");
        //builder.addEventListener();
        //builder.getConfiguration();
        //builder.getFileHandler();
...

为了对比,此处使用Spring提供的PropertiesLoaderUtils工具来加载对比一下:

    public static void main(String[] args) throws ConfigurationException, IOException {
        // Spring也是支持很友好的编码解决的~~~
        Properties properties = PropertiesLoaderUtils.loadProperties(new EncodedResource(new ClassPathResource("my.properties"), StandardCharsets.UTF_8));
        System.out.println(properties);
    }

它也能很方便的解决中文乱码问题,但是,但是,但是它并不支持占位符且不能使用系统属性或者环境变量,这是它一大劣势。

关于properties文件中占位符的使用

上面我们知道了它支持强大的占位符,其实它还支持取值一些内置的变量。比如环境变量和环境属性。官方文档见:Variable Interpolation

个人建议以官方说明为准,此处大概的总结一下。 如果支持Ant或者Maven就知道,他们都是是支持读取环境变量的。 显然此处强大的它必须是也赋予了properties配置文件这个基本的功能特性。

内置三个属性前缀如下:

  1. sys:对应我们熟悉的systemProperties,不解释
  2. env:对应我们熟悉的environmentProperties,不解释
  3. const:The const prefix indicates that a variable is to be interpreted as a constant member field of a class (i.e. a field with the static final modifiers). The name of the variable must be of the form <full qualified class name>.<field name>. The specified class will be loaded and the value of this field will be obtained.

关于systemPropertiesenvironmentProperties可参考博文:【小家Java】Java环境变量(Env)和系统属性(Property)详解—工具文章

使用案例:

user.file = ${sys:user.home}/settings.xml
java.home = ${env:JAVA_HOME}

# 可以理解为直接引用某个类的静态常量~ 还是蛮强大的
action.key = ${const:java.awt.event.KeyEvent.VK_CANCEL}

占位符使用注意事项:

  1. 如果某个变量无法解析,例如,因为名称无效或使用了未知前缀,则不会替换该变量,而是按原样输出,包括美元符号和大括号。(so,不可能输出null)
  2. 对变量的引用可以嵌套;变量可以引用一个变量,而该变量又可以引用其他变量等等。
  3. 检测到循环引用。在这种情况下,直接报错:Infinite loop in property interpolation
  4. 变量插值发生在从配置中查询属性时,而不是在创建时(所以我们的使用变量可以达到很好的动态刷新属性值的效果)。因此此机制是动态的:一个属性上的更改会影响引用第一个属性的另一个属性的值。
  5. 只允许同一个文件的属性值引用,不能垮文件。另外,同一个文件内属性值引用,文件内可以无序(上面一行也可能引用下面的key,和顺序无关
关于properties文件的写

以上所有内容都是讲的读取properties文件,确实我们绝大部分情况下都只需要读取。 但是这里稍微介绍一下对它的写:向里面设置一个k-v,然后最终写进文件里(持久化)

...
		// 这里单独set,只是保存在了内存了,并还没有持久化的  需要注意~~~~
        Configuration config = configs.properties("my.properties");
        config.setProperty("dev", "dev");
...

关于它的具体写入逻辑以及中文乱码的处理,这里篇幅限制暂时略过,有兴趣的可以自己稍微研究一下即可。(因为写入确实用得太少了,时间原因所以这里就暂时忽略~)

处理XML文件:ExpressionEngine

相比properties文件格式 xml要复杂多了。虽然现在SpringBoot摒弃了xml配置,但是某些时候使用xml还是更加的适合些,比如日志框架(logback)的配置文件等。 xml的复杂在于:它不仅仅可以表示k-v,并且每个节点都可以有attribute以及子节点。所以对于xml格式的配置文件,需要一套规则来处理(比如你的attr叫什么名字,这个都是框架本身并不能知道的)。

commons configuration把这个称作ExpressionEngine引擎。它给我们提供了一套默认的规则引擎,但是这个显然你也是可以自定义的,也可以使用xpath的标准。

下面的代码分别示例如何使用:默认规则、xpath、自定义规则来访问Xml配置文件中的节点. 首先在类路径放置一个文件:my.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <token>
        <device>
            <validate>true</validate>
        </device>
        <person>
            <validate>true</validate>
            <expire description="人员令牌失效时间(分钟) ">60</expire>
        </person>
    </token>
</configuration>

以上:此处的description是节点属性(attribute)

默认规则引擎

使用提供给我们的默认规则来处理:

    public static void main(String[] args) throws ConfigurationException {
        Configurations configs = new Configurations();

        XMLConfiguration config = configs.xml("my.xml");

        // 使用默认的符号定义创建一个表达式引擎
        DefaultExpressionEngine engine = new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS);
        // 指定表达式引擎
        config.setExpressionEngine(engine);
        System.out.println(config.getBoolean("token.device.validate"));
        System.out.println(config.getInt("token.person.expire"));

        // 此处就是它的默认规则 [@xxx]表示取对应attr的值~  更多默认定义请参考DEFAULT_SYMBOLS常量
        System.out.println(config.getString("token.person.expire[@description]"));
    }

输出结果为:

true
60
人员令牌失效时间(分钟) 

结果正确并且还没有乱码(不用处理乱码也是xml的一大优势)。

xpath规则引擎

要使用xpath引擎,请先导入jar:

<!-- https://mvnrepository.com/artifact/commons-jxpath/commons-jxpath -->
<dependency>
    <groupId>commons-jxpath</groupId>
    <artifactId>commons-jxpath</artifactId>
    <version>1.3</version>
</dependency>

示例代码:

    public static void main(String[] args) throws ConfigurationException {
        Configurations configs = new Configurations();


        XMLConfiguration config = configs.xml("my.xml");

        // 使用 XPath表达式引擎
        XPathExpressionEngine xpathEngine = new XPathExpressionEngine();
        config.setExpressionEngine(xpathEngine);
        System.out.println(config.getBoolean("token/device/validate"));
        System.out.println(config.getInt("token/person/expire"));

        // 请注意这里路径分隔符和attribute标签与上面使用DefaultExpressionEngine是不同的
        System.out.println(config.getString("token/person/expire/@description"));
    }

使用xpath的好处:xpath是一种通用的标准,大家遵守起来比较容易。 缺点:需要额外导入jar。 但总体来说大多数情况下,我觉得选择xpath还是一种不错的选择~~

解析xml一直都是一件非常头疼的事,现在有了commons-configuration简直不要太方便有木有~

ExpressionEngine接口内置的两个实现:一个DefaultExpressionEngine,另外一个就是XPathExpressionEngine

自定义规则引擎

如果上面两种表达式引擎都不合你的意,比如你很任性就是不喜欢/@description[@description]这样别扭的attr标签。你可以用自定义的符号规则生成一个ExpressionEngine表达式引擎对象。

此处举例:直接使用@作为属性符号取值:

    public static void main(String[] args) throws ConfigurationException {
        Configurations configs = new Configurations();
        DefaultExpressionEngineSymbols symbols = new DefaultExpressionEngineSymbols.Builder(
                DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS)
                // 指定属性分隔符(路径分隔符)
                .setPropertyDelimiter(".")
                .setIndexStart("{")
                .setIndexEnd("}")
                // 指定@开头就是attribute标志
                .setAttributeStart("@")
                // attribute结尾符为null  也就是木有结尾符
                .setAttributeEnd(null)
                // A Backslash is used for escaping property delimiters
                .setEscapedDelimiter("\\/")
                .create();
        // 用自定义的符号DefaultExpressionEngineSymbols对象创建一个表达式引擎
        // 此处还是使用的DefaultExpressionEngine~~~这个对象
        DefaultExpressionEngine engine = new DefaultExpressionEngine(symbols);
        Parameters params = new Parameters();


        FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class)
                .configure(params.xml()
                        .setFileName("my.xml")
                        // ~~~~~~~~~~~~~~~~~~~~~~~~~~使用自定义的表达式引擎~~~~~~~~~~~~~~~~~~~~~~~~~~
                        .setExpressionEngine(engine));


        // 使用builder生成配置~  而不用使用Configurations帮忙了~
        XMLConfiguration config = builder.getConfiguration();
        System.out.println(config.getBoolean("token.device.validate"));
        System.out.println(config.getInt("token.person.expire"));

        // 看此处 符号换成了我们自定义的~
        System.out.println(config.getProperty("token.person.expire@description"));
    }

一般情况下个人不太建议自定义,毕竟xpath形成一个标准也不容易,直接使用反倒更加省心,建议遵守。

处理Yaml文件

在现在Yaml越来越流行,也确实它比properties文件更加的直观和简洁,所以解析它成为一个基础能力。

在类路径下准备一个my.yaml

spring:
 application:
   name: mydemoshow::${person.name}

person:
 name: fsx
 age: 10

示例代码如下:

    public static void main(String[] args) throws Exception {
        YAMLConfiguration config = new YAMLConfiguration();
        config.read(new ClassPathResource("my.yaml").getInputStream());

        Iterator<String> keys = config.getKeys();
        while (keys.hasNext()) {
            String key = keys.next();
            String value = config.getString(key);
            System.out.println(key + " = " + value);
        }
    }

输出:

spring.application.name = mydemoshow::fsx
person.name = fsx
person.age = 10

结果正确,并且它也能使用和properties一样的占位符~

文件扫描策略

从上面的例子可以看到,我们的文件放在classpath下,就能够被加载进来。那么这里面到底有怎么样的规则呢?

从2.x版本开始,对于文件扫描策略,用接口FileLocationStrategy来实现,该接口只有一个单一的方法locate(),

// @since 2.0
public interface FileLocationStrategy {
    URL locate(FileSystem fileSystem, FileLocator locator);
}

它内置的实现如下:

  1. ClasspathLocationStrategy:从classpath下去加载文件**(常用)**
  2. AbsoluteNameLocationStrategy:绝对路径。所以直接使用new File(locator.getFileName())去加载
  3. HomeDirectoryLocationStrategy:从user.home里去查找
  4. BasePathLocationStrategy:使用basePath+fileName的方式new File()
  5. FileSystemLocationStrategy:使用文件系统定位。比如windows是带盘符的
  6. ProvidedURLLocationStrategy:直接是URL的方式。所以它也可以存在于网络上~
  7. CombinedLocationStrategy:真正使用的。它是一个聚合,这些实现类可以构成一个扫描链来进行按照其顺序进行组合扫描,之前讲过很多类似的设计模式了

默认使用的策略:

public class FileHandler {
    public FileLocationStrategy getLocationStrategy() {
        return FileLocatorUtils.obtainLocationStrategy(getFileLocator());
    }
}

... // 最终调用如下
public final class FileLocatorUtils {
	
    private static FileLocationStrategy initDefaultLocationStrategy() {
        final FileLocationStrategy[] subStrategies = new FileLocationStrategy[] {
                        new ProvidedURLLocationStrategy(),
                        new FileSystemLocationStrategy(),
                        new AbsoluteNameLocationStrategy(),
                        new BasePathLocationStrategy(),
                        new HomeDirectoryLocationStrategy(true),
                        new HomeDirectoryLocationStrategy(false),
                        new ClasspathLocationStrategy()
                };
        return new CombinedLocationStrategy(Arrays.asList(subStrategies));
    }
}

可以看到它内置的策略,能满足我们几乎所有的case。 其中,我们最常用的ClasspathLocationStrategy放在了末尾,前面的优先级更高。若你想自定义这种扫描顺序,你可以重写对应方法即可~

事件/监听

它也有它自己的一套事件监听的API: org.apache.commons.configuration2.event.Event:事件。它继承自java.util.EventObject org.apache.commons.configuration2.event.EventListener:监听器。只有一个方法void onEvent(T event); T extends Event 注意和java.util.EventListener以及 org.apache.commons.configuration2.event.EventSource:事件源。提供注册监听器、移除监听器的两个方法

事件监听的三大要素都有了,这种模式其实我们也已经非常熟悉了。下面主要列出重要的事件、监听器等,然后写个Demo即可 ConfigurationEvent:配置文件的时间。 ConfigurationErrorEvent:配置文件出错的事件 ConfigurationBuilderEvent:builder的事件。 ReloadingEvent:冲洗加载的事件。

常见监听器有: AutoSaveListener:监听事件为ConfigurationEvent PropertiesConfigurationLayout:监听事件为ConfigurationEvent CombinedConfiguration(DynamicCombinedConfiguration):监听事件为ConfigurationEvent

常见事件源EventSource: ConfigurationBuilderReloadingControllerBaseEventSource

Demo:使用事件/监听模式记录配置文件的修改记录

关于Commons Configuration的1.x和2.x的简单说明

commons-configuration2是在已经广泛使用commons-configuration 1.x版本基础上的一个升级版本,与1.x版本并保持兼容。

从官网的Release History我们可以看到如下:

可以看到1.10版本在2013年10月份就已经寿终正寝了,2.x版本目前还保持着非常活跃的状态。

所以个人建议:若你自己的业务或者框架中需要使用它来管理配置,建议直接使用2.x版本。 但是呢?因为你是直接使用Spring Cloud自带的版本,从而做到jar的最小依赖,你就也有必要了解1.x版本的使用和API了

总结

本文只介绍了commons-configuration2的一个基本使用,更多详情请参见它的官网的doc:http://commons.apache.org/proper/commons-configuration/userguide/user_guide.html

Apache出品,必属精品。它号称Java的第二API,自然涵盖有非常多且优秀的jar包工具供我们使用,很大程度上避免了我们重复造轮子,提高工作效率。 比如:BeanUtils、Collections、Codec、Compress、Configuration、DbUtils

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 美团大规模微服务通信框架及治理体系OCTO核心组件开源

    微服务通信框架及治理平台OCTO作为美团基础架构设施的重要组成部分,目前已广泛应用于公司技术线,稳定承载上万应用、日均支撑千亿级的调用。业务基于OCTO提供的标...

    美团技术团队
  • 再也不敢使用集合默认初始化值了

    集合初始化通常进行分配容量、设定特定参数等相关工作。我们以使用频率相对较高的ArrayList和HashMap为例,简要说明初始化的相关工作,并解释为什么在任何...

    用户4143945
  • Java性能优化的50个细节,我必须分享给你!

    来源:blog.csdn.net/dongnan591172113/article/details/51790428

    用户5224393
  • 如何用Python编写一个Lisp解释器

    这篇文章有两个目的:一是展示如何实现一个计算机语言的解释器,二是演示如何使用 Python 3 构造 Lisp 的一种方言 Schema,作者把自己的这个语言解...

    AI科技大本营
  • Elasticsearch BulkProcessor 的具体实现

    本文示例使用的是 Spring Boot 框架,由于该框架有默认的 Elasticsearch 版本,为了避免版本混乱或冲突,我在 pom.xml 文件内添加了...

    create17
  • Java 12 骚操作, String居然还能这样玩!

    栈长之前在Java技术栈微信公众号分享过《Java 11 已发布,String 还能这样玩!》这篇文章,介绍了 Java 11 的 String 新玩法,让大家...

    Java技术栈
  • JAVAAPI中SortedMap解释

    A Map进一步提供其键上的总排序 。地图根据其键的natural ordering或通过在分类地图创建时提供的Comparator进行排序。当迭代排序的地图的...

    技术从心
  • 怎样成为全栈开发工程师[每日前端夜话0xAA]

    在 LinkedIn 和 Facebook 上,有很多人将当前的工作标记为全栈工开发程师。在 Medium 上关于这个问题的文章也收到了很多读者的好评。一些人认...

    疯狂的技术宅
  • Iterator 、Generator(一)

    调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和don...

    用户3258338
  • JavaScript的工作原理:引擎,运行时和调用堆栈的概述

    随着 JavaScript 变得越来越流行,各团队正在多个领域栈中使用它们,其中包括 — 前端,后端,混合应用,嵌入式等等。

    奋飛

扫码关注云+社区

领取腾讯云代金券