更新properties文件后保持配置项注释和顺序不变

2,564 阅读18分钟

最近遇到一个小需求:更新properties文件中的配置项。当然,首先想到的就是java.util.Properties工具类。但是后来发现,利用properties load配置项后,不会加载配置项的注释,且配置项顺序也发生了改变,基于这个数据再更新,则导致更新后的配置文件注释丢失,配置项的顺序也不是我们希望的。

首先我们了解下java.util.Properties工具类源码,可以发现该类继承自HashTable, 自然也就导致读取出来的配置项是无序的;继续深入,我们发现当Properties从一个字节流中加载属性时,会通过一个properites的内部类行读取器java.util.Properties.LineReader来实现的,下面是这个类的注释

从上图的类注释可以看出,利用LineReader读取properties文件时,会跳过注释。

原因找到了,接下来就是想办法解决问题。很显然,现有的java.util.Properties无法满足我们需要保留配置项注释和顺序的要求,我们需要做些扩展。

废话不多说,上代码

/**
 * 扩展java.util.Properties工具类:
 * 1. 保存原配置文件中的注释
 * 2. 保存原配置文件中的配置项顺序
 */
@Slf4j
public class OrderedProperties {

    // 原始属性键值对(无序)
    private final Properties props;
    // 保存key与comment的映射,同时利用这个映射来保证key的顺序。
    private final LinkedHashMap<String, String> keyCommentMap = new LinkedHashMap<String, String>();

    public OrderedProperties() {
        super();
        props = new Properties();
    }

    /**
     * 设置属性,如果key已经存在,那么将其对应value值覆盖。
     * @param key 键
     * @param value 与键对应的值
     * @param comment 对键值对的说明
     * @return
     */
    public synchronized String setProperty(String key, String value, String comment){
        Object oldValue = props.setProperty(key, value);
        if(StringUtils.isEmpty(comment)){
            if(!keyCommentMap.containsKey(key)){
                keyCommentMap.put(key, comment);
            }
        }else{
            keyCommentMap.put(key, comment);
        }
        return (String)oldValue;
    }

    /**
     * 根据key获取属性表中相应的value。
     * @param key
     * @return
     */
    public String getProperty(String key) {
        return props.getProperty(key);
    }

    /**
     * 从一个字符流中读取属性到属性表中
     *
     * @param reader
     * @throws IOException
     */
    public synchronized void load(Reader reader) throws IOException {
        load0(new LineReader(reader));
    }

    /**
     * 将属性表中的属性写到字节流里面。
     * @param out
     * @throws IOException
     */
    public void store(OutputStream out) throws IOException {
        store0(new BufferedWriter(new OutputStreamWriter(out, "utf-8")),true);
    }

    /**
     * 获取属性表中所有key的集合
     */
    public Set<String> propertyNames() {
        return props.stringPropertyNames();
    }

    /**
     * 获取property封装到类属性props和keyCommentMap
     * @param lr
     * @throws IOException
     */
    private void load0(LineReader lr) throws IOException {
        int limit;
        int keyLen;
        int valueStart;
        char c;
        boolean hasSep;
        boolean precedingBackslash;
        StringBuffer buffer = new StringBuffer();
        StringBuilder outBuffer = new StringBuilder();

        while ((limit = lr.readLine()) >= 0) {
            keyLen = 0;
            valueStart = limit;
            hasSep = false;
            //获取注释
            c = lr.lineBuf[keyLen];
            if(c == '#' || c == '!'){
                String comment = loadConvert(lr.lineBuf, 1, limit - 1, outBuffer);
                if(buffer.length() > 0){
                    buffer.append("\n");
                }
                buffer.append(comment);
                continue;
            }
            precedingBackslash = false;
            ...
            // load property
            setProperty(key, value, buffer.toString());
            // reset buffer
            buffer = new StringBuffer();
        }
    }

    /**
     * 基于java.util.Properties.LineReader进行改造, 删除过滤comment相关代码
     */
    class LineReader {
        public LineReader(InputStream inStream) {
            this.inStream = inStream;
            inByteBuf = new byte[8192];
        }

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

        byte[] inByteBuf;
        char[] inCharBuf;
        char[] lineBuf = new char[1024];
        int inLimit = 0;
        int inOff = 0;
        InputStream inStream;
        Reader reader;

        int readLine() throws IOException {
            int len = 0;
            char c = 0;

            boolean skipWhiteSpace = true;
            boolean isNewLine = true;
            boolean appendedLineBegin = false;
            boolean precedingBackslash = false;
            boolean skipLF = false;

            while (true) {
                if (inOff >= inLimit) {
                    inLimit = (inStream==null)?reader.read(inCharBuf)
                            :inStream.read(inByteBuf);
                    inOff = 0;
                    if (inLimit <= 0) {
                        if (len == 0) {
                            return -1;
                        }
                        return len;
                    }
                }
                if (inStream != null) {
                    // The line below is equivalent to calling a
                    // ISO8859-1 decoder.
                    c = (char) (0xff & inByteBuf[inOff++]);
                } else {
                    c = inCharBuf[inOff++];
                }
                if (skipLF) {
                    skipLF = false;
                    if (c == '\n') {
                        continue;
                    }
                }
                if (skipWhiteSpace) {
                    if (c == ' ' || c == '\t' || c == '\f') {
                        continue;
                    }
                    if (!appendedLineBegin && (c == '\r' || c == '\n')) {
                        continue;
                    }
                    skipWhiteSpace = false;
                    appendedLineBegin = false;
                }
                if (isNewLine) {
                    isNewLine = false;
                }

                if (c != '\n' && c != '\r') {
                    lineBuf[len++] = c;
                    if (len == lineBuf.length) {
                        int newLength = lineBuf.length * 2;
                        if (newLength < 0) {
                            newLength = Integer.MAX_VALUE;
                        }
                        char[] buf = new char[newLength];
                        System.arraycopy(lineBuf, 0, buf, 0, lineBuf.length);
                        lineBuf = buf;
                    }
                    // flip the preceding backslash flag
                    if (c == '\\') {
                        precedingBackslash = !precedingBackslash;
                    } else {
                        precedingBackslash = false;
                    }
                }
                else {
                    // reached EOL
                    if (len == 0) {
                        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;
                        }
                    }
                    if (precedingBackslash) {
                        len -= 1;
                        //skip the leading whitespace characters in following line
                        skipWhiteSpace = true;
                        appendedLineBegin = true;
                        precedingBackslash = false;
                        if (c == '\r') {
                            skipLF = true;
                        }
                    } else {
                        return len;
                    }
                }
            }
        }
    }

    /**
     * 更新property文件,基于keyCommentMap就行循环,确保顺序
     * @param bw
     * @param escUnicode
     * @throws IOException
     */
    private void store0(BufferedWriter bw, boolean escUnicode)
            throws IOException{
        synchronized (this) {
            Iterator<Map.Entry<String, String>> kvIter = keyCommentMap.entrySet().iterator();
            while(kvIter.hasNext()){
                Map.Entry<String, String> entry = kvIter.next();
                String key = entry.getKey();
                String val = getProperty(key);
                String comment = entry.getValue();
                key = saveConvert(key, true, escUnicode);
                /* No need to escape embedded and trailing spaces for value, hence
                 * pass false to flag.
                 */
                val = saveConvert(val, false, escUnicode);
                if(StringUtils.isNotEmpty(comment)) {
                    writeComments(bw, comment);
                }
                if(StringUtils.isNotEmpty(val)) {
                    bw.write(key + "=" + val);
                } else {
                    bw.write(key);
                }
                bw.newLine();
            }
        }
        bw.flush();
    }
}

以上是关键代码段,不要看着代码量很大哦,其实大部分都是java.util.Properties的相关源码,文中部分省略代码也皆是源码中对应的代码或方法(基于JDK 1.8)

工具类具体使用方法与原生java.util.Properties无异

public class OrderedPropertiesUtil {

    /**
     * load配置文件  
     */
    public static OrderedProperties loadProperties(String fileName) {
        OrderedProperties properties = new OrderedProperties();
        FileInputStream is = null;

        try {
            is = new FileInputStream(fileName);
            BufferedReader bf = new BufferedReader(new InputStreamReader(is));
            properties.load(bf);
        } catch (Exception e) {
            log.error e.getMessage(), e);
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
        }

        return properties;
    }

    /**
     * 更新配置文件  
     */
    public static void updateProperties(String fileName, String keyString value) {
        OrderedProperties properties = loadProperties(fileName);
        FileOutputStream os = null;
        try {
            os = new FileOutputStream(fileName);
            properties.setProperty(key, value)
            properties.store(os);
        } catch (Exception e) {
            log.error e.getMessage(), e);
        } finally {
            try {
                os.close();
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
        }
    }
}

参考 www.iteye.com/blog/broken…