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的自然行或逻辑行中,第一个未转义的
-
键:从第一个空白字符开始,到键值对分隔符之前的,都是键;键中可以包含转义的
=
、:
、空白字符 -
值:键值对分隔符之后的,第一个非空白字符到行结束,都是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-8
或UTF-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.