Java Properties类

1. 絮絮叨叨

  • 感觉自己就是犟拐拐,本来就想学习下Java的系统属性,发现系统属性有关的System类中,使用Properties对象存储属性
  • 于是,又将Properties类简单学了一下
  • 按照自己这样下去,Java学习永无出头之日😭😭😭

2. Properties类概述

  • 以往的编程实战中,经常使用哈表存储键值对。现在想想,某些场景下,键值对实际就是属性名及属性值

  • 除了常见的get()、put()外,有时还需要从文件中获取属性,或将现有的属性写入到文件中

  • 这时,若还使用哈希表存储属性就变得不是很方便了。因为,哈希表中没有对stream操作提供直接支持,属性的加载或持久化存储等,还需要单独编写stream操作代码

  • Properties类应运而生,用于表示一个属性列表。

    • 它继承了 Hashtable类,本质上是一个哈希表,可以用于存储属性集合,支持get()、put()操作
    • 同时,可以将属性保存到stream中,或从stream中加载属性
  • Properties类的定义如下,表面上看key(属性名)和value(属性值),都是Object对象,但是实际是按照String进行存储的

    public class Properties extends Hashtable<Object,Object>
    
  • 除此之外,Properties中还包含一个defaults字段,用于存储默认属性;当前属性列表中找不到对应的key时,会尝试从自身包含的defaults属性列表中进行查找

    protected Properties defaults;
    
  • Properties类的构造函数如下,若不传入defaults属性列表,则defaults将为null

    public Properties() {
        this(null);
    }
    
    public Properties(Properties defaults) {
        this.defaults = defaults;
    }
    
  • Properties的getProperty()充分展示了,Properties类对key的两级寻找策略,以及key、value都是String类型的事实

    public String getProperty(String key) {
        Object oval = super.get(key); // 从自身属性列表查找
        String sval = (oval instanceof String) ? (String)oval : null;
        // 若defaults不为空,则从defaults属性列表中查找
        return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval; 
    }
    

3. set、get操作

  • Properties类继承了Hashtable,原本就支持通过put()、get()方法操作键值对

  • 为了与属性的设置、获取操作匹配,Properties类在put()方法之上,创建了setProperty()方法,用于设置属性

  • 注意: setProperty()使用synchronized关键字修饰,是一个线程安全的方法

    public synchronized Object setProperty(String key, String value) {
        return put(key, value);
    }
    
  • 在get()方法的基础上,创建了getProperty()方法,用于获取属性

    // 指定了默认值的getProperty()方法
    public String getProperty(String key, String defaultValue) {
        String val = getProperty(key);
        return (val == null) ? defaultValue : val;
    }
    
  • 下面的代码,简单演示properties的set、get操作,并展示默认属性defaults的作用

    public static void main(String[] args) {
        // 先创建defaults属性
        Properties defaults = new Properties();
        defaults.setProperty("city", "广东深圳");
        defaults.setProperty("nation", "中国");
    
        // 创建带defaults属性的properties
        Properties lucy = new Properties(defaults);
        lucy.setProperty("name", "lucy");
        lucy.setProperty("age", "24");
    
        // 访问lucy自身存在的属性
        System.out.printf("name: %s\n", lucy.getProperty("name"));
        // 访问默认属性
        System.out.printf("nation: %s\n", lucy.getProperty("nation"));
        // 访问不存在的属性, 将返回null
        System.out.printf("sex: %s\n", lucy.getProperty("sex"));
    }
    

4. 基于stream的操作

4.1 store()方法

  • 比这认为:相对普通的哈希表,Properties类最大特色就是支持对stream的操作

  • store()方法, 可以将properties以字节流或字符流的形式写到输出流中,一般用于将properties持久化存储到文件中

  • 其中,comment是对properties的注释,写到文件中,就像代码注释一样

    // 字节流
    public void store(OutputStream out, String comments) throws IOException {
        store0(new BufferedWriter(new OutputStreamWriter(out, "8859_1")),
               comments,
               true);
    }
    // 字符流
    public void store(Writer writer, String comments) throws IOException {
        store0((writer instanceof BufferedWriter)?(BufferedWriter)writer
                                                 : new BufferedWriter(writer),
               comments,
               false);
    }
    
  • 细心的你可能会发现,以字节流输出时制定了编码为"8859_1",也就是大名鼎鼎的ISO 8859-1 。笔者亲测,以字符流输出时,产生的文件也是ISO 8859-1编码

  • 下面的代码,展示了如何将properties通过store()方法写入文件

    public static void main(String[] args) {
       try {
           Properties properties = new Properties();
           properties.setProperty("jdbc.driver", "com.mysql.jdbc.Driver");
           properties.setProperty("jdbc.url", "jdbc:mysql://localhost:3306/test");
           properties.setProperty("jdbc.user", "sunrise");
           properties.setProperty("jdbc.pass", "db630230");
           // 将属性以字节流的形式写入target/classes目录下
           String filePath = SystemProperty.class.getResource("/").getPath();
           properties.store(new FileOutputStream(filePath + "db_store1.properties"), "backup of database.properties");
    
           // 将属性以字符流的形式写入文件
           properties.store(new OutputStreamWriter(new FileOutputStream(filePath + "db_store2.properties")), "backup of database.properties");
       } catch (IOException e) {
           e.printStackTrace();
       }
    }
    
  • 最终,会在target/classes目录下产生两个ISO 8859-1 编码的properties文件

4.2 load()方法

  • load()方法与store()方法相对应,用于从字节流或字符流中读取properties,一般用于从文件中加载properties
    public synchronized void load(Reader reader) throws IOException {
        load0(new LineReader(reader));
    }
    // 要求输入流使用ISO 8859-1编码
    public synchronized void load(InputStream inStream) throws IOException {
        load0(new LineReader(inStream));
    }
    

4.2.1 自然行 vs 逻辑行 vs 空白行 vs 注释行

  • load(Reader reader)方法的注释中,提到了很多关于行的术语:自然行、逻辑行、空白行、注释行

load(Reader reader)方法将按行读取properties,这个行既可以是自然行,也可以是逻辑行

  • 自然行:以行终止符\n\r\r\n,或stream默认的终止符EOF,定义的一行字符

  • 逻辑行:

    • 为了方便阅读,有时属性值需要占据多个自然行。
    • 这时,可以使用\转义行终止符,使属性可以可以占据多个相邻的自然行
    • 使用\转义后的多个自然行,形成了一个逻辑行
  • 例如,下面的server.properties文件中,就包含自然行和逻辑行

    scheduler.http-client.idle-timeout=1m
    query.client.timeout=5m
    # query.min-expire-age=30m
    
    plugin.bundles=../presto-blackhole/pom.xml,\
      ../presto-memory/pom.xml,\
      ../presto-jmx/pom.xml,\
      ../presto-raptor/pom.xml
    
    plugin.dir=../presto-server/target/presto-server-0.240/presto-server-0.240/plugin
    
  • load(Reader reader)方法读取server.properties文件

    public static void main(String[] args) {
        try {
            Properties properties = new Properties();
            String filePath = SystemProperty.class.getResource("/server.properties").getPath();
            properties.load(new FileReader(filePath));
            // 打印properties
            properties.forEach((key,value) -> System.out.printf("%s: %s\n", key, value));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
  • 最终结果如下,可以发现自然行、逻辑行表示的property被成功读取
    在这里插入图片描述

关于空白行:仅包含空白字符的自然行,被视为空白并在加载时被忽略

  • 这也解释了,为什么上面的server.properties文件中,存在空白行并未影响properties的加载
  • 关于空白字符:除了上面提到的行终止符,还包括空格字符(' ''\u0020')、制表符('\t''\u0009')和换页符('\f''\u000C')都可以视作空白字符

第一个非空白字符#!的行,是注释行;注释行不能像逻辑行一样,通过\转义实现多行注释

  • 上面的server.properties文件中,# query.min-expire-age=30m就是一个注释行,所以在加载时被忽略
  • 注意: 多行注释,必须通过多个单行注释实现,不能像逻辑行一样,通过\转义自然行后实现

广义的自然行

  • 该小节的最开始,笔者将自然行描述成了对应一个property的字符行
  • 但从自然行的定义可知,自然行可以是空白行、注释行、包含部分或全部键值对的行

4.3 properties文件的编码方式

  • load(InputStream inStream)方法,要求输入流使用ISO 8859-1编码

  • 基于grammar.properties文件文件亲测,使用load(Reader reader)方法读取properties文件时,是可以读取UTF-8编码的文件,而且支持中文

  • 但使用 load(InputStream inStream)方法读取grammar.properties文件,中文将会乱码

    public static void main(String[] args) {
        try {
            Properties properties = new Properties();
            String filePath = SystemProperty.class.getResource("/grammar.properties").getPath();
            properties.load(Files.newInputStream(Paths.get(filePath)));
            // 打印properties
            properties.forEach((key,value) -> System.out.printf("%s=%s\n", key, value));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
  • 乱码的中文

  • 摆烂地说一句:

    • 字符编码的弯弯绕绕,笔者到现在都是迷糊的。
    • 既然 load(InputStream inStream)方法要求输入ISO 8859-1编码,那我尽量避免在properties文件中使用中文,或者使用load(Reader reader)方法读取UTF-8编码的properties文件
    • 或者使用store(OutputStream out, String comments)方法,先创建属性包含中文的、ISO 8859-1编码的properties文件

4.4 property的键值对书写方法

  • 到目前为止,我们都是以key=value的形式来表示属性的

  • 键值对的分隔符:

    • 表示property的自然行或逻辑行中,第一个未转义=:和空白字符(除行终止符外),都是键值对的分隔符
    • 如果使用=:作为分隔符,则其之前和之后未转义的空白字符都将被忽略
    • 如果使用空白字符做分割符,可以是多个空白字符
  • 键:从第一个空白字符开始,到键值对分隔符之前的,都是键;键中可以包含转义的=:、空白字符

  • 值:键值对分隔符之后的,第一个非空白字符到行结束,都是property的值

  • grammar.properties文件如下,其定义的property都是有效的

    # =作为分隔符,=前后未转义白字符将被忽略
    name = sunrise
    # :作为分隔符,:后未转义的空白字符将被忽略
    tel: 010-6666888,010-668866
    # 以若干空格作为分割符
    fruits   banana, apple\
             watermelon, mongo\
             orange
    # key中包含转义的:
    city\:province=深圳,广东
    # value中可以包含分割符,且无需转义
    expression: a = 1 + 2
    # =作为分隔符,=前转义的分隔符将被是做key的一部分
    class\ \: =1024
    
  • 同样使用load(Reader reader)方法读取grammar.properties文件,只是修改了键值之间的分隔符,最后打印的键值对如下:

  • 注意: 虽然property键值对的书写方法很多,但一般都是用key=value的书写方法

4.5 针对xml的stream操作

  • 目前为止,Properties的stream操作基本都是基于properties文件
  • 除了properties文件,xml文件也可以用于存储属性
  • 与之前基于字节流的的load()、store()方法不一样,针对xml的load()、store()方法要求编码格式为UTF-8UTF-16

4.5.1 storeToXML()方法

  • Properties提供了两个storeToXML()方法,用于将属性持久化到xml文件中,且在不指定文件编码的情况下,默认使用UTF-8

    public void storeToXML(OutputStream os, String comment)
        throws IOException
    {
        storeToXML(os, comment, "UTF-8");
    }
    public void storeToXML(OutputStream os, String comment, String encoding)
        throws IOException
    {
        XmlSupport.save(this, Objects.requireNonNull(os), comment,
                        Objects.requireNonNull(encoding));
    }
    
  • 下面的代码,可以将于MySQL JDBC连接配置写入target/classes/目录下的db.xml文件中

    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        properties.setProperty("jdbc.driver", "com.mysql.jdbc.Driver");
        properties.setProperty("jdbc.url", "jdbc:mysql://localhost:3306/test");
        properties.setProperty("jdbc.user", "sunrise");
        properties.setProperty("jdbc.pass", "db630230");
        OutputStream outputStream = Files.newOutputStream(Paths.get(SystemProperty.class.getResource("/").getPath() + "db.xml"));
        properties.storeToXML(outputStream, "xml file from database configuration");
    }
    
  • 最终db.xml文件的内容如下:

4.5.2 loadFromXML()方法

  • loadFromXML()方法如下

    public synchronized void loadFromXML(InputStream in)
            throws IOException, InvalidPropertiesFormatException
        {
            XmlSupport.load(this, Objects.requireNonNull(in));
            in.close();
        }
    
  • loadFromXml()方法要求xml文件使用UTF-8或UTF-16编码,且DOCTYPE定义如下:

     <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    
  • 下面的代码,将从xml文件中读取property并打印

    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        InputStream inputStream = SystemProperty.class.getResourceAsStream("/db.xml");
        properties.loadFromXML(inputStream);
        properties.list(System.out);
    }
    
  • 执行结果如下:

5. list操作

  • 上面的代码,都是通过forEach语法实现的property打印

  • Properties类提供了list()方法,用于打印properties中的属性(不包含defaults中的默认属性)

    public void list(PrintStream out)
    public void list(PrintWriter out)
    
  • 下面的代码,将读取server.properties文件,并使用list(PrintStream out)方法进行打印

    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        String filePath = SystemProperty.class.getResource("/server.properties").getPath();
        properties.load(Files.newInputStream(Paths.get(filePath)));
        // 打印properties
        properties.list(System.out);
    }
    
  • 执行结果如下,value貌似因为过长被截取了 😂

  • 查看list()方法的源码,发现两个list()方法,都会在value长度超过40后,只打印前37个字符,后面的使用...表示

    if (val.length() > 40) {
        val = val.substring(0, 37) + "...";
    }
    
  • 这样的设定,使用list()方法打印property前,要先考虑下能否满足需求~

6. 总结

6.1 开源组件如何使用Properties?

  • 拿自己比较熟悉的开源组件Presto来说,会在etc/目录下配置大量的.properties文件

  • 基于Properties类自定义了PropertiesUtil,然后通过loadProperties(XXX_CONFIGURATION)将属性加载到内存

    public final class PropertiesUtil
    {
        private PropertiesUtil() {}
    
        public static Map<String, String> loadProperties(File file)
                throws IOException
        {
            Properties properties = new Properties();
            try (InputStream in = Files.newInputStream(file.toPath())) {
                properties.load(in);
            }
            return fromProperties(properties);
        }
    }
    

6.2 线程安全的Properties类

  • 从之前的学习可以看出,只要涉及到Properties的写入操作,都使用了synchronized关键字进行修饰

    public synchronized void load(Reader reader)
    public synchronized void load(InputStream inStream)
    public synchronized void loadFromXML(InputStream in)
    public synchronized Object setProperty(String key, String value)
    
  • 不难看出,Properties类是线程安全的,这与类注释一致:

    This class is thread-safe: multiple threads can share a single Properties object without the need for external synchronization.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值