欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Properties源码理解

程序员文章站 2022-04-30 15:19:18
...

Properties用来读配置文件的对象,用的很多。

 

使用方法

    // 新建一个对象
    Properties pro = new Properties();
    // 加载字节流
    pro.load(new FileInputStream("abc.txt"));
    // 取值
    System.out.println(pro.getProperty("key"));
    // 修改
    pro.setProperty("key", "value");

 

0.几个问题

1.a=b=c 会被拆分成什么样?
                答:key="a",value="b=c"

2.字符中有 = 该怎么办?
                答:办法一:\=     办法二:\u003D

3.a<空格><空格>d  =  2<空格>44  可以作为一个正常的配置么?
                答:不能,空格会作为key的结束

4.a=2<空格>34<空格>5 最后的值value为多少?
                答: "2 34 5"

5.在配置文件中最后的空格会被去掉吗?
                答:不会被去掉,程序中只会去掉前面和等号左右的空格。

6.想在配置文件里换行改怎么办
                答:在回车前输入一个\
7.pro.load(new FileInputStream("abc.txt")); 这样打开abc.txt的文件流关闭没有?
                答:流未关闭,需要手动关闭,不建议这样写。但是虚拟机在回收这个对象的时候会关闭流。

 

1.构造方法

类继承了hashTable类

 

public class Properties extends Hashtable<Object,Object> 

 

本身是一个Map ,这个对象主要用来储存key-value的键值对,所以可以使用另一个Properties来初始化自己。

 

    protected Properties defaults;

    public Properties() {
        this(null);
    }

    public Properties(Properties defaults) {
        this.defaults = defaults;
    }

 

2.使用的第一步,load()。

先简单的说一下load的执行过程:

1.读一行。

2.找到key 和 value 。

3.存入map中<String,String>

 

    //下面是Properties类中方法
    public synchronized void load(InputStream inStream) throws IOException {
        //将输出流转成LineReader,LineReader是Properties的内部类,功能是可以一行一行的读数据。
        load0(new LineReader(inStream));
    }
    //私有
    private void load0 (LineReader lr) throws IOException {
        //由于key和value中会有转义字符串,所以用这个数组来存储key或者value的转义后的数据。
        char[] convtBuf = new char[1024];
        int limit;//一行字符数(除一行前面空格,到末尾的回车符)
        int keyLen;//key值长度
        int valueStart;//value起始
        char c;//在循环遍历时暂存一个字符,临时使用,命名也很不规范。
        boolean hasSep;//是否有分割符 = 或者:
        boolean precedingBackslash; //当前读取是否为转义字符
        /*循环读取每一行,每一行分析出key和value。limit为每一行字符数长度(除前面空格),-1表示流读完了 */
        while ((limit = lr.readLine()) >= 0) {
            c = 0;
            keyLen = 0;
            valueStart = limit;
            hasSep = false;
            //下面的system是原作者写的,不认真。
	    //System.out.println("line=<" + new String(lineBuf, 0, limit) + ">");
            //字面上是反斜杠的意思,表示碰到了转义的反斜杠"\"。
            precedingBackslash = false;
            /*这个循环主要找出key结束的地方,也简单的找了一个value大概开始部分。*/
            while (keyLen < limit) {
                //这个lineBuf是LineReader里的一个字符数组,存了一行字符。
                c = lr.lineBuf[keyLen];
                //找到分隔符
                if ((c == '=' ||  c == ':') && !precedingBackslash) {
                    //value的起始点
                    valueStart = keyLen + 1;
                    //找到key和value的分隔符
                    hasSep = true;
                    //找到key结束的位置,跳出循环
                    break;
                /*同样空格\t \f 也是key结束的地方*/
                } else if ((c == ' ' || c == '\t' ||  c == '\f') && !precedingBackslash) {
                    valueStart = keyLen + 1;
                    break;
                } 
                //如果是反斜杠,则跳过下一个字符
                if (c == '\\') {
                    //连续两个反斜杠的话又变为false了。
                    precedingBackslash = !precedingBackslash;
                } else {
                    //未碰到变为false。
                    precedingBackslash = false;
                }
                //这个就是key的长度。
                keyLen++;
            }
            /*计算value的开始点*/
            while (valueStart < limit) {
                c = lr.lineBuf[valueStart];
                /*这个if是为了去掉value前的空格字符*/
                if (c != ' ' && c != '\t' &&  c != '\f') {
                    /*这个if是为了在key值中因为空格跳出,然后空格后接着是“="或":"的情况下*/
                    if (!hasSep && (c == '=' ||  c == ':')) {
                        //有了分割key和value的标识
                        hasSep = true;
                    } else {
                        //结束value开始的检查
                        break;
                    }
                }
                valueStart++;
            }
            //取key和value。
            String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf);
            String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf);
            //这个类继承了HashTable
	    put(key, value);
	}
    }

紧接着,我们看一下怎么取出key和value,并且将其内部的转义符都去掉。

 

    /* 加载转化,
     * in 传入的字符数组
     * off 要取值开始的地方
     * len 要取的长度
     * convtbuf 输出的字符数组
     */
    private String loadConvert (char[] in, int off, int len, char[] convtBuf) {
        //如果数组的长度小于要取长度
        if (convtBuf.length < len) {
            int newLen = len * 2;
            //如果传入的len为负的,性能就非常差了,少个判断!
            if (newLen < 0) {
	        newLen = Integer.MAX_VALUE;
	    } 
	    convtBuf = new char[newLen];
        }
        char aChar;
        char[] out = convtBuf; //换个名字
        int outLen = 0; //输出的长度
        int end = off + len;//要取字符结束的地方
        //循环从要取字符开始到结束。
        while (off < end) {
            aChar = in[off++];
            //如果是转义字符
            if (aChar == '\\') {
                aChar = in[off++];   
                //如果是unicode
                if(aChar == 'u') {
                    //读4位,4位表示unicode的编码集.
                    int value=0;
                    //下面的步骤可以理解成Integer.paseInt("7EA2",16)
		    for (int i=0; i<4; i++) {
		        aChar = in[off++];  
		        switch (aChar) {
		          case '0': case '1': case '2': case '3': case '4':
		          case '5': case '6': case '7': case '8': case '9':
		             value = (value << 4) + aChar - '0';
			     break;
			  case 'a': case 'b': case 'c':
                          case 'd': case 'e': case 'f':
			     value = (value << 4) + 10 + aChar - 'a';
			     break;
			  case 'A': case 'B': case 'C':
                          case 'D': case 'E': case 'F':
			     value = (value << 4) + 10 + aChar - 'A';
			     break;
			  default:
                              //抛出的这个异常不需要捕获。
                              throw new IllegalArgumentException(
                                           "Malformed \\uxxxx encoding.");
                        }
                     }
                    //写入输出的数组
                    out[outLen++] = (char)value;
                } else {
                    //这样就其它转义字符处理了
                    if (aChar == 't') aChar = '\t'; 
                    else if (aChar == 'r') aChar = '\r';
                    else if (aChar == 'n') aChar = '\n';
                    else if (aChar == 'f') aChar = '\f'; 
                    //这一步非常关键,它直接忽略转义符号\ ,举例:\= 会存为 = ,  \\ 会存为\ , \?会存为 ?,\<空格> 会存 为 <空格>
                    out[outLen++] = aChar;
                }
            } else {
                //无转义
	        out[outLen++] = (char)aChar;
            }
        }
        return new String (out, 0, outLen);
    }

它是怎么读取一行数据的?下面需要看一下Properties的内部类LineReader

3.Properties的内部类LineReader

3.1首先是无任何继承

  class LineReader {}

3.2构造方法

字符流和字节流通吃。

        
        public LineReader(InputStream inStream) {
            this.inStream = inStream;
            inByteBuf = new byte[8192]; 
	}

        public LineReader(Reader reader) {
            this.reader = reader;
            inCharBuf = new char[8192]; 
	}

3.3 readLine方法

简单的看一下吧,这个不是重点

文字介绍:读取每一个字符,空白的跳过,碰到一行第一个字符检查是否为#或者!,继续循环直到碰到\r 或者\n ,然后结束进入一个新一行的循环。末尾的空格不去掉。

 

        int readLine() throws IOException {
            int len = 0;         //数组长度
            //另外一些变量
            ...
            while (true) {
                //从字节流中读取 8192个字节,或者从字符流中读取8192个字符
                if (inOff >= inLimit) {
                    inLimit = (inStream==null)?reader.read(inCharBuf)
		                              :inStream.read(inByteBuf);
                    //如果流读到末尾
		    if (inLimit <= 0) {
                        //流的长度是0,或者是注释,直接返回-1,结束。
			if (len == 0 || isCommentLine) { 
			    return -1; 
			}
			return len;
		    }
		}     
                //取出一个字符
                ...
                //碰到\\r的情况,意思是\\r\n 或者 \\r \\n都不会进行换行,同样\\r \\r\n \\n 也不加入字符数组中。
                if (skipLF) {
                    skipLF = false;
		    if (c == '\n') {
		        continue;
		    }
		}
                //跳过空白
		if (skipWhiteSpace) {
		    if (c == ' ' || c == '\t' || c == '\f') {
			continue;
		    }
                    //如果没有开始,并且字符时\r \n 的,同样跳过。
		    if (!appendedLineBegin && (c == '\r' || c == '\n')) {
			continue;
		    }
                    //如果不是上述情况,则代表已经不是空格字符了。
		    skipWhiteSpace = false;
		    appendedLineBegin = false;
		}
                //如果是新的一行
		if (isNewLine) {
		    isNewLine = false;
                    //如果是注释
		    if (c == '#' || c == '!') {
			isCommentLine = true;
			continue;
		    }
		}
		//检查字符是不是换行
		if (c != '\n' && c != '\r') {//不是换行
		    lineBuf[len++] = c;
                    //下面是检查是否越界了
                    ...
		    //同样处理转义字符
                    ...
		}
		else {// 这里是找到了换行的字符了


                    //注释或者空行
		    if (isCommentLine || len == 0) {
			isCommentLine = false;
			isNewLine = true;
			skipWhiteSpace = true;
			len = 0;
			continue;
		    }
                    //如果当前字符超过了读取到的字符数。
		    if (inOff >= inLimit) {
                        inLimit = (inStream==null)
                                  ?reader.read(inCharBuf)
			          :inStream.read(inByteBuf);
			inOff = 0;
			if (inLimit <= 0) {
			    return len;
			}
		    }
                    //下面还有回车前碰到转义符的情况
                    ...
		}
	    }
	}


 

4.取值和修改

在load里面使用put存储了数据,这里取值很简单。

    public String getProperty(String key) {
	Object oval = super.get(key);
	String sval = (oval instanceof String) ? (String)oval : null;
	return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
    }

    public String getProperty(String key) {
	Object oval = super.get(key);
	String sval = (oval instanceof String) ? (String)oval : null;
	return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
    }

 

5.方法中也包含着将数据存成文本的功能。

在存储xml的时候,用到了 java.util.XMLUtils 来解析xml。

storeToXML()
loadFromXML()

6.读取时候去除绝对路径

props.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("config.properties"));

7.结束