java读写properties配置文件不改变属性的顺序和注释
先贴代码
1 import java.io.BufferedWriter; 2 import java.io.File; 3 import java.io.FileInputStream; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.InputStreamReader; 7 import java.io.OutputStream; 8 import java.io.OutputStreamWriter; 9 import java.io.Reader; 10 import java.io.Writer; 11 import java.util.Iterator; 12 import java.util.LinkedHashMap; 13 import java.util.Map; 14 import java.util.Properties; 15 import java.util.Set; 16 17 /** 18 * 扩展properties工具类 19 * 20 * @author tangming 21 * @date 2017-11-10 22 */ 23 public class SafeProperties { 24 25 /** 26 * 内部属性表 27 */ 28 private final Properties props; 29 30 /** 31 * 保存key与comment的映射, 同时利用这个映射来保证key的顺序。 32 */ 33 private final LinkedHashMap<String, String> keyCommentMap = new LinkedHashMap<String, String>(); 34 35 private static final String BLANK = ""; 36 37 public SafeProperties() { 38 super(); 39 props = new Properties(); 40 } 41 42 public SafeProperties(Properties defaults) { 43 super(); 44 props = new Properties(defaults); 45 } 46 47 /** 48 * 设置一个属性,如果key已经存在,那么将其对应value值覆盖。 49 * 50 * @param key 51 * @param value 52 * @return 53 */ 54 public String setProperty(String key, String value) { 55 return setProperty(key, value, BLANK); 56 } 57 58 /** 59 * 设置一个属性,如果key已经存在,那么将其对应value值覆盖。 60 * 61 * @param key 键 62 * @param value 与键对应的值 63 * @param comment 对键值对的说明 64 * @return 65 */ 66 public synchronized String setProperty(String key, String value, String comment) { 67 Object oldValue = props.setProperty(key, value); 68 if (BLANK.equals(comment)) { 69 if (!keyCommentMap.containsKey(key)) { 70 keyCommentMap.put(key, comment); 71 } 72 } else { 73 keyCommentMap.put(key, comment); 74 } 75 return (String) oldValue; 76 } 77 78 /** 79 * 根据key获取属性表中相应的value。 80 * 81 * @param key 82 * @return 83 */ 84 public String getProperty(String key) { 85 return props.getProperty(key); 86 } 87 88 /** 89 * 根据key获取属性表中相应的value。 如果没找到相应的value,返回defaultValue。 90 * 91 * @param key 92 * @param defaultValue 93 * @return 94 */ 95 public String getProperty(String key, String defaultValue) { 96 return props.getProperty(key, defaultValue); 97 } 98 99 /** 100 * 从一个字符流中读取属性到属性表中 101 * 102 * @param reader 103 * @throws IOException 104 */ 105 public synchronized void load(Reader reader) throws IOException { 106 load0(new LineReader(reader)); 107 } 108 109 /** 110 * 从一个字节流中读取属性到属性表中 111 * 112 * @param inStream 113 * @throws IOException 114 */ 115 public synchronized void load(InputStream inStream) throws IOException { 116 load0(new LineReader(inStream)); 117 } 118 119 /** 120 * 从一个字节流中读取属性到属性表中 121 * 122 * @param inStream 123 * @param charset 124 * @throws IOException 125 */ 126 public synchronized void load(InputStream inStream, String charset) throws IOException { 127 InputStreamReader reader = new InputStreamReader(inStream, charset); 128 load0(new LineReader(reader)); 129 } 130 131 /** 132 * 从一个文件中读取属性到属性表中 133 * 134 * @param file 属性文件 135 * @param charset 字符集 136 * @throws IOException 137 */ 138 public synchronized void load(File file, String charset) throws IOException { 139 FileInputStream inputStream = new FileInputStream(file); 140 InputStreamReader reader = new InputStreamReader(inputStream, charset); 141 load0(new LineReader(reader)); 142 } 143 144 /** 145 * 从一个文件中读取属性到属性表中 默认字符集为utf-8 146 * 147 * @param file 属性文件 148 * @throws IOException 149 */ 150 public synchronized void load(File file) throws IOException { 151 FileInputStream inputStream = new FileInputStream(file); 152 InputStreamReader reader = new InputStreamReader(inputStream, "utf-8"); 153 load0(new LineReader(reader)); 154 } 155 156 /** 157 * 将属性表中的属性写到字符流里面。 158 * 159 * @param writer 160 * @throws IOException 161 */ 162 public void store(Writer writer) throws IOException { 163 store0((writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer), false); 164 } 165 166 /** 167 * 将属性表中的属性写到字节流里面。 168 * 169 * @param out 170 * @throws IOException 171 */ 172 public void store(OutputStream out) throws IOException { 173 store0(new BufferedWriter(new OutputStreamWriter(out, "utf-8")), true); 174 } 175 176 /** 177 * 如果属性表中某个key对应的value值和参数value相同 那么返回true,否则返回false。 178 * 179 * @param value 180 * @return 181 */ 182 public boolean containsValue(String value) { 183 return props.containsValue(value); 184 } 185 186 /** 187 * 如果属性表中存在参数key,返回true,否则返回false。 188 * 189 * @param key 190 * @return 191 */ 192 public boolean containsKey(String key) { 193 return props.containsKey(key); 194 } 195 196 /** 197 * 获取属性表中键值对数量 198 * 199 * @return 200 */ 201 public int size() { 202 return props.size(); 203 } 204 205 /** 206 * 检查属性表是否为空 207 * 208 * @return 209 */ 210 public boolean isEmpty() { 211 return props.isEmpty(); 212 } 213 214 /** 215 * 清空属性表 216 */ 217 public synchronized void clear() { 218 props.clear(); 219 keyCommentMap.clear(); 220 } 221 222 /** 223 * 获取属性表中所有key的集合。 224 * 225 * @return 226 */ 227 public Set<String> propertyNames() { 228 return props.stringPropertyNames(); 229 } 230 231 public synchronized String toString() { 232 StringBuffer buffer = new StringBuffer(); 233 Iterator<Map.Entry<String, String>> kvIter = keyCommentMap.entrySet().iterator(); 234 buffer.append("["); 235 while (kvIter.hasNext()) { 236 buffer.append("{"); 237 Map.Entry<String, String> entry = kvIter.next(); 238 String key = entry.getKey(); 239 String val = getProperty(key); 240 String comment = entry.getValue(); 241 buffer.append("key=" + key + ",value=" + val + ",comment=" + comment); 242 buffer.append("}"); 243 } 244 buffer.append("]"); 245 return buffer.toString(); 246 } 247 248 public boolean equals(Object o) { 249 // 不考虑注释说明是否相同 250 return props.equals(o); 251 } 252 253 public int hashCode() { 254 return props.hashCode(); 255 } 256 257 private void load0(LineReader lr) throws IOException { 258 char[] convtBuf = new char[1024]; 259 int limit; 260 int keyLen; 261 int valueStart; 262 char c; 263 boolean hasSep; 264 boolean precedingBackslash; 265 StringBuffer buffer = new StringBuffer(); 266 267 while ((limit = lr.readLine()) >= 0) { 268 c = 0; 269 keyLen = 0; 270 valueStart = limit; 271 hasSep = false; 272 // 获取注释 273 c = lr.lineBuf[keyLen]; 274 if (c == '#' || c == '!') { 275 String comment = loadConvert(lr.lineBuf, 1, limit - 1, convtBuf); 276 if (buffer.length() > 0) { 277 buffer.append("\n"); 278 } 279 buffer.append(comment); 280 continue; 281 } 282 precedingBackslash = false; 283 while (keyLen < limit) { 284 c = lr.lineBuf[keyLen]; 285 // need check if escaped. 286 if ((c == '=' || c == ':') && !precedingBackslash) { 287 valueStart = keyLen + 1; 288 hasSep = true; 289 break; 290 } else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) { 291 valueStart = keyLen + 1; 292 break; 293 } 294 if (c == '\\') { 295 precedingBackslash = !precedingBackslash; 296 } else { 297 precedingBackslash = false; 298 } 299 keyLen++; 300 } 301 while (valueStart < limit) { 302 c = lr.lineBuf[valueStart]; 303 if (c != ' ' && c != '\t' && c != '\f') { 304 if (!hasSep && (c == '=' || c == ':')) { 305 hasSep = true; 306 } else { 307 break; 308 } 309 } 310 valueStart++; 311 } 312 String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf); 313 String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf); 314 setProperty(key, value, buffer.toString()); 315 // reset buffer 316 buffer = new StringBuffer(); 317 } 318 } 319 320 /* 321 * 基于java.util.Properties.LineReader进行改造 322 * 323 * Read in a "logical line" from an InputStream/Reader, skip all comment and blank lines and filter out those leading whitespace characters ( , and ) from the beginning of a "natural line". Method 324 * returns the char length of the "logical line" and stores the line in "lineBuf". 325 */ 326 class LineReader { 327 public LineReader(InputStream inStream) { 328 this.inStream = inStream; 329 inByteBuf = new byte[8192]; 330 } 331 332 public LineReader(Reader reader) { 333 this.reader = reader; 334 inCharBuf = new char[8192]; 335 } 336 337 byte[] inByteBuf; 338 char[] inCharBuf; 339 char[] lineBuf = new char[1024]; 340 int inLimit = 0; 341 int inOff = 0; 342 InputStream inStream; 343 Reader reader; 344 345 int readLine() throws IOException { 346 int len = 0; 347 char c = 0; 348 349 boolean skipWhiteSpace = true; 350 boolean isNewLine = true; 351 boolean appendedLineBegin = false; 352 boolean precedingBackslash = false; 353 boolean skipLF = false; 354 355 while (true) { 356 if (inOff >= inLimit) { 357 inLimit = (inStream == null) ? reader.read(inCharBuf) : inStream.read(inByteBuf); 358 inOff = 0; 359 if (inLimit <= 0) { 360 if (len == 0) { 361 return -1; 362 } 363 return len; 364 } 365 } 366 if (inStream != null) { 367 // The line below is equivalent to calling a 368 // ISO8859-1 decoder. 369 c = (char) (0xff & inByteBuf[inOff++]); 370 } else { 371 c = inCharBuf[inOff++]; 372 } 373 if (skipLF) { 374 skipLF = false; 375 if (c == '\n') { 376 continue; 377 } 378 } 379 if (skipWhiteSpace) { 380 if (c == ' ' || c == '\t' || c == '\f') { 381 continue; 382 } 383 if (!appendedLineBegin && (c == '\r' || c == '\n')) { 384 continue; 385 } 386 skipWhiteSpace = false; 387 appendedLineBegin = false; 388 } 389 if (isNewLine) { 390 isNewLine = false; 391 } 392 393 if (c != '\n' && c != '\r') { 394 lineBuf[len++] = c; 395 if (len == lineBuf.length) { 396 int newLength = lineBuf.length * 2; 397 if (newLength < 0) { 398 newLength = Integer.MAX_VALUE; 399 } 400 char[] buf = new char[newLength]; 401 System.arraycopy(lineBuf, 0, buf, 0, lineBuf.length); 402 lineBuf = buf; 403 } 404 // flip the preceding backslash flag 405 if (c == '\\') { 406 precedingBackslash = !precedingBackslash; 407 } else { 408 precedingBackslash = false; 409 } 410 } else { 411 // reached EOL 412 if (len == 0) { 413 isNewLine = true; 414 skipWhiteSpace = true; 415 len = 0; 416 continue; 417 } 418 if (inOff >= inLimit) { 419 inLimit = (inStream == null) ? reader.read(inCharBuf) : inStream.read(inByteBuf); 420 inOff = 0; 421 if (inLimit <= 0) { 422 return len; 423 } 424 } 425 if (precedingBackslash) { 426 len -= 1; 427 // skip the leading whitespace characters in following line 428 skipWhiteSpace = true; 429 appendedLineBegin = true; 430 precedingBackslash = false; 431 if (c == '\r') { 432 skipLF = true; 433 } 434 } else { 435 return len; 436 } 437 } 438 } 439 } 440 } 441 442 /* 443 * Converts encoded \uxxxx to unicode chars and changes special saved chars to their original forms 444 */ 445 private String loadConvert(char[] in, int off, int len, char[] convtBuf) { 446 if (convtBuf.length < len) { 447 int newLen = len * 2; 448 if (newLen < 0) { 449 newLen = Integer.MAX_VALUE; 450 } 451 convtBuf = new char[newLen]; 452 } 453 char aChar; 454 char[] out = convtBuf; 455 int outLen = 0; 456 int end = off + len; 457 458 while (off < end) { 459 aChar = in[off++]; 460 if (aChar == '\\') { 461 aChar = in[off++]; 462 if (aChar == 'u') { 463 // Read the xxxx 464 int value = 0; 465 for (int i = 0; i < 4; i++) { 466 aChar = in[off++]; 467 switch (aChar) { 468 case '0': 469 case '1': 470 case '2': 471 case '3': 472 case '4': 473 case '5': 474 case '6': 475 case '7': 476 case '8': 477 case '9': 478 value = (value << 4) + aChar - '0'; 479 break; 480 case 'a': 481 case 'b': 482 case 'c': 483 case 'd': 484 case 'e': 485 case 'f': 486 value = (value << 4) + 10 + aChar - 'a'; 487 break; 488 case 'A': 489 case 'B': 490 case 'C': 491 case 'D': 492 case 'E': 493 case 'F': 494 value = (value << 4) + 10 + aChar - 'A'; 495 break; 496 default: 497 throw new IllegalArgumentException("Malformed \\uxxxx encoding."); 498 } 499 } 500 out[outLen++] = (char) value; 501 } else { 502 if (aChar == 't') 503 aChar = '\t'; 504 else if (aChar == 'r') 505 aChar = '\r'; 506 else if (aChar == 'n') 507 aChar = '\n'; 508 else if (aChar == 'f') 509 aChar = '\f'; 510 out[outLen++] = aChar; 511 } 512 } else { 513 out[outLen++] = (char) aChar; 514 } 515 } 516 return new String(out, 0, outLen); 517 } 518 519 private void store0(BufferedWriter bw, boolean escUnicode) throws IOException { 520 synchronized (this) { 521 Iterator<Map.Entry<String, String>> kvIter = keyCommentMap.entrySet().iterator(); 522 while (kvIter.hasNext()) { 523 Map.Entry<String, String> entry = kvIter.next(); 524 String key = entry.getKey(); 525 String val = getProperty(key); 526 String comment = entry.getValue(); 527 key = saveConvert(key, true, escUnicode); 528 /* 529 * No need to escape embedded and trailing spaces for value, hence pass false to flag. 530 */ 531 val = saveConvert(val, false, escUnicode); 532 if (!comment.equals(BLANK)) 533 writeComments(bw, comment); 534 bw.write(key + "=" + val); 535 bw.newLine(); 536 } 537 } 538 bw.flush(); 539 } 540 541 private static void writeComments(BufferedWriter bw, String comments) throws IOException { 542 bw.write("#"); 543 int len = comments.length(); 544 int current = 0; 545 int last = 0; 546 while (current < len) { 547 char c = comments.charAt(current); 548 if (c > '\u00ff' || c == '\n' || c == '\r') { 549 if (last != current) 550 bw.write(comments.substring(last, current)); 551 if (c > '\u00ff') { 552 bw.write(c); 553 } else { 554 bw.newLine(); 555 if (c == '\r' && current != len - 1 && comments.charAt(current + 1) == '\n') { 556 current++; 557 } 558 if (current == len - 1 559 || (comments.charAt(current + 1) != '#' && comments.charAt(current + 1) != '!')) 560 bw.write("#"); 561 } 562 last = current + 1; 563 } 564 current++; 565 } 566 if (last != current) 567 bw.write(comments.substring(last, current)); 568 bw.newLine(); 569 } 570 571 /* 572 * Converts unicodes to encoded \uxxxx and escapes special characters with a preceding slash 573 */ 574 private String saveConvert(String theString, boolean escapeSpace, boolean escapeUnicode) { 575 int len = theString.length(); 576 int bufLen = len * 2; 577 if (bufLen < 0) { 578 bufLen = Integer.MAX_VALUE; 579 } 580 StringBuffer outBuffer = new StringBuffer(bufLen); 581 582 for (int x = 0; x < len; x++) { 583 char aChar = theString.charAt(x); 584 // Handle common case first, selecting largest block that 585 // avoids the specials below 586 if ((aChar > 61) && (aChar < 127)) { 587 if (aChar == '\\') { 588 outBuffer.append('\\'); 589 outBuffer.append('\\'); 590 continue; 591 } 592 outBuffer.append(aChar); 593 continue; 594 } 595 switch (aChar) { 596 case ' ': 597 if (x == 0 || escapeSpace) 598 outBuffer.append('\\'); 599 outBuffer.append(' '); 600 break; 601 case '\t': 602 outBuffer.append('\\'); 603 outBuffer.append('t'); 604 break; 605 case '\n': 606 outBuffer.append('\\'); 607 outBuffer.append('n'); 608 break; 609 case '\r': 610 outBuffer.append('\\'); 611 outBuffer.append('r'); 612 break; 613 case '\f': 614 outBuffer.append('\\'); 615 outBuffer.append('f'); 616 break; 617 case '=': // Fall through 618 case ':': // Fall through 619 case '#': // Fall through 620 case '!': 621 outBuffer.append('\\'); 622 outBuffer.append(aChar); 623 break; 624 default: 625 if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode) { 626 outBuffer.append('\\'); 627 outBuffer.append('u'); 628 outBuffer.append(toHex((aChar >> 12) & 0xF)); 629 outBuffer.append(toHex((aChar >> 8) & 0xF)); 630 outBuffer.append(toHex((aChar >> 4) & 0xF)); 631 outBuffer.append(toHex(aChar & 0xF)); 632 } else { 633 outBuffer.append(aChar); 634 } 635 } 636 } 637 return outBuffer.toString(); 638 } 639 640 /** 641 * Convert a nibble to a hex character 642 * 643 * @param nibble the nibble to convert. 644 */ 645 private static char toHex(int nibble) { 646 return hexDigit[(nibble & 0xF)]; 647 } 648 649 /** A table of hex digits */ 650 private static final char[] hexDigit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 651 'F' }; 652 653 }