最近遇到一个小需求:更新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 key, String 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);
}
}
}
}