正确使用Java Properties
最近赋闲在家闲的蛋疼,找工作也不顺利,就安静下来学一些常用开源项目,在翻struts2的时候看到读取properties配置文件是自己定义的reader来读取,因为之前上班的时候常常使用到properties的读写,对于jdk本身的properties在保存的时候会把注释忽略掉这点深恶痛绝,一直想重新写一个properties文件读写的工具类,但是大致翻了一下properties的代码和文档,发现properties的规则挺多,没有几天时间怕是难以完成就一直搁下了。这次看到struts2的代码就想拿来借鉴一下,于是就把properties的东西读了一遍,发觉很多东西是之前忽略甚至不知道的,于是记下和兄弟们共享,如有错欢迎指正,概念颇多,容易晕头,建议找头脑清醒的时候看。
JDK Properties核心在读取配置文件的:
private void load0 (LineReader lr) throws IOException
方法上。其中传入的参数LineReader类是Properties的内部类,用来读取一个逻辑行(这儿就不详细介绍了,它会读取一个逻辑行并且忽略掉逻辑行行首的所有空白字符和换行字符)。
load0方法的JDK文档总结如下,这也是后续的几个重要的概念的出处:
1.注释符为:'#'或者'!'。空白字符为:' ', '\t', '\f'。key/value分隔符为:'='或者':'。行分隔符为:'\r','\n','\r\n'。
2.自然行是使用行分隔符或者流的结尾来分割的行。逻辑行可能分割到多个自然行中,使用反斜杠'\'来连接多个自然行。
3.注释行是使用注释符作为首个非空白字符的自然行。
4.空白字符的自然行会被认为是空行而被忽略。
5.properties文件的key为从首个非空白字符开始直到(但不包括)首个非转义的'=', ':'或者非行结束符的空白字符为止。
6.key后面的第一个非空白字符如果是”=”或者”:”,那么这个字符后面所有空白字符都会被忽略掉。
7.可以使用转义序列表示key和value(当然此处的字符转义序列和unicode的转义有一些差别,jdk文档都有列出来)。
properties是一个包含了key、value对的文本文档,key,value的界定是正确读取properties的关键,那么key、value是如何界定的呢?上面第5点是对key的不完全界定但是并未涉及到value,这些,都只有从源码当中来寻找答案。
load0源码和注解如下:
1 private void load0(LineReader lr) throws IOException { 2 char[] convtBuf = new char[1024]; 3 //行的长度 4 int limit; 5 //key的长度 6 int keyLen; 7 //value的开始点 8 int valueStart; 9 //当前读取的字符 10 char c; 11 //是否是key/value的分隔符 12 boolean hasSep; 13 //前一个字符是否是反斜杠 14 boolean precedingBackslash; 15 //把通过LineReader读取来的逻辑行进行遍历,一个个char的进行处理。 16 while ((limit = lr.readLine()) >= 0) { 17 c = 0; 18 keyLen = 0; 19 valueStart = limit; 20 hasSep = false; 21 precedingBackslash = false; 22 //循环获取key的长度 23 while (keyLen < limit) { 24 c = lr.lineBuf[keyLen]; 25 //当字符为key/value分隔符:'='或':'并且前一个字符不是反斜杠的时候,key长度读取结束,并且把hasSep设置为true,break。 26 if ((c == '=' || c == ':') && !precedingBackslash) { 27 valueStart = keyLen + 1; 28 hasSep = true; 29 break; 30 } 31 //当字符为空白字符' '或'\t'或'\f'并且前一个字符不是反斜杠的时候,key长度读取结束,break。 32 else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) { 33 valueStart = keyLen + 1; 34 break; 35 } 36 //当连续存在奇数个反斜杠的时候, precedingBackslash为true。 37 if (c == '\\') { 38 precedingBackslash = !precedingBackslash; 39 } else { 40 precedingBackslash = false; 41 } 42 keyLen++; 43 } 44 //循环获取value开始的位置 45 while (valueStart < limit) { 46 c = lr.lineBuf[valueStart]; 47 //如果字符不为所有的空白字符:' ', '\t', '\f'的时候 48 if (c != ' ' && c != '\t' && c != '\f') { 49 //如果前面不是key/value的分隔符,而是空白字符,而该字符是key/value分隔符 50 if (!hasSep && (c == '=' || c == ':')) { 51 hasSep = true; 52 } else { 53 //结束读取 54 break; 55 } 56 } 57 valueStart++; 58 } 59 //loadConvert是进行字符串转义的方法,就不用关心了。 60 String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf); 61 String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf); 62 put(key, value); 63 } 64 }
通过如上的代码可以看出,key/value分割符'=', ':'与空白字符:' ', '\t', '\f'是区分key、value的关键:
key的界定为:逻辑行中,从首个非空白字符开始直到(但不包括)首个非转义的'=', ':'或者非行结束符的空白字符为止。(和前面第5点基本一致)
value的界定为:逻辑行中,非转义的key/value分隔符(此处不仅仅包括'=',':',还包括' ', '\t', '\f')后面的第一个非空白字符(非' ', '\t', '\f'字符)开始到逻辑行结束的所有字符。
另外key、value还有如下特征:
1.因为LineReader是读取的逻辑行,所以key、value中可以包含多个自然行。
2.在“循环获取key的长度”的代码中可以看到处理key/value分隔符的方式和处理空白字符的方式很相似(除了在发现处理的字符为key/value分隔符的时候会把 hasSep变量设置为true)。而这表明:
如果空白字符后续没有key/value分隔符(“=”或者“:”),那么该空白字符会被当作key/value分隔符,从分隔符后的第一个非空白字符起到逻辑行结束所有的字符都当作是value。也就是说:“key1 value1”,读取出来之后的key和value分别为”key1”, “value1”。
如果空白字符后续有key/value分隔符(“=”或者“:”),那么该空白字符会被忽略,key/value分隔符后的第一个非空白字符起到逻辑行结束所有的字符都当作是value。也就是说:”key1 :value1”,读取出来之后的key和value分别为”key1”和”value1”,而不是”key1”和”:value1”。
另外,在读xwork的com.opensymphony.xwork2.util.PropertiesReader类的时候发现,它的实现和JDK的Properties实现有出入,也就是说,如果JDK的Properties是规范的话,那么xwork的properties读取类是有bug的。测试类如下(注释掉的Assert才能通过junit):
1 public class PropertiesTest { 2 @Test 3 public void testLoad() throws IOException { 4 File f = new File(getClass().getResource(".").getPath(), "test.properties"); 5 6 InputStream in = null; 7 try { 8 //java properties 9 in = new FileInputStream(f); 10 Properties props = new Properties(); 11 props.load(in); 12 String s1 = props.getProperty("key"); 13 Assert.assertEquals("value#with", s1); 14 String s2 = props.getProperty("comments"); 15 Assert.assertEquals("", s2); 16 } finally { 17 if (in != null) 18 try { 19 in.close(); 20 } catch (IOException e) { 21 e.printStackTrace(); 22 } 23 } 24 25 try { 26 //xwork properties 27 in = new FileInputStream(f); 28 Reader reader = new InputStreamReader(in); 29 PropertiesReader pr = new PropertiesReader(reader); 30 while (pr.nextProperty()) { 31 String name = pr.getPropertyName(); 32 String val = pr.getPropertyValue(); 33 if ("key".equals(name)) { 34 Assert.assertEquals("value#with", val); 35 //Assert.assertEquals("valuecomments", val); 36 } 37 if ("comments".equals(name)) { 38 Assert.assertEquals("", val); 39 //Assert.assertEquals(null, val); 40 } 41 } 42 } finally { 43 if (in != null) 44 try { 45 in.close(); 46 } catch (IOException e) { 47 e.printStackTrace(); 48 } 49 } 50 } 51 }
test.properties的内容如下:
1 key=value\ 2 #with 3 comments
好了,清楚properties的使用规则了,如果我们需要自己写一个实现在保存properties的时候注释不被忽略掉,而且按照原来的行数来保存的工具类的话,就会清晰很多了。本来想把这个工具写一下,但是写代码加调试实在太费时间,等到用的时候再来写吧。
---原来写的一篇老文章,移过来。

浙公网安备 33010602011771号