iT邦幫忙

0

應用系統建置前準備工具 - PropUtils Properties 檔案工具

  • 分享至 

  • xImage
  •  

PropUtils Properties 檔案工具類別

概述

PropUtils 是一個用於操作 Properties 檔案的工具類別,提供讀取、更新及查詢 properties 檔案內容的相關方法。此類別設計為靜態工具類別,所有方法皆可直接呼叫使用。

專案相關程式

  • FileUtils
  • CommUtils
  • GlobalConstants

第三方元件(Dependency)

  • lombok.extern.slf4j.Slf4j;

主要功能

1. Properties 檔案驗證

檢查檔案格式

// 檢查檔案是否為有效的 properties 格式
boolean isValid = PropUtils.isPropertiesFile("config.properties");  // true
boolean notValid = PropUtils.isPropertiesFile("invalid.txt");      // false

2. Properties 檔案更新

更新預設檔案

// 更新預設 properties 檔案中的變數值
PropUtils.updateProperty("database.url", "jdbc:mysql://localhost:3306/mydb");

更新指定檔案

// 更新指定 properties 檔案中的變數值
PropUtils.updateProperty("custom.properties", "server.port", "8080");

3. Properties 檔案讀取

讀取全部內容

// 讀取整個 properties 檔案內容
Map<String, String> allProps = PropUtils.loadProps("config.properties");

讀取指定變數

// 從預設 properties 檔案讀取變數值
String value = PropUtils.getProps("app.name");

// 從指定 properties 檔案讀取變數值
String value = PropUtils.getProps("custom.properties", "app.version");

重要注意事項

  1. 檔案處理

    • 更新或讀取時,若檔案不存在會自動建立新檔案
    • 空檔案或僅包含註解的檔案仍視為有效的 properties 檔案
    • 讀取和更新操作都會忽略註解行(以 # 開頭)
  2. 格式驗證

    • 每行必須包含 "=" 符號(除了空行和註解行)
    • 格式錯誤的行會被記錄在日誌中
    • 變數名稱和值會自動去除前後空白
  3. 更新操作

    • 更新不存在的變數時會自動新增
    • 更新時會保留原有的註解和空行
    • 新增的變數會加在檔案末尾
  4. 錯誤處理

    • 所有方法都可能拋出 IOException
    • getProps 方法在發生異常時會返回 null
    • 檔案操作錯誤會記錄在日誌中

單元測試範例

以下測試範例示範 PropUtils 的常見測試場景:

1. 讀取與更新 Properties

@Test
void testLoadAndUpdateProps(@TempDir Path tempDir) throws Exception {
   Path p = tempDir.resolve("app.properties");
   Files.write(p, Arrays.asList("app.name=demo"), StandardCharsets.UTF_8);

   Map<String,String> props = PropUtils.loadProps(p.toString());
   assertEquals("demo", props.get("app.name"));

   PropUtils.updateProperty(p.toString(), "app.name", "newdemo");
   assertEquals("newdemo", PropUtils.getProps(p.toString(), "app.name"));
}

2. 格式驗證與錯誤處理

@Test
void testIsPropertiesFileAndError(@TempDir Path tempDir) throws Exception {
   Path p = tempDir.resolve("notprop.txt");
   Files.write(p, Arrays.asList("just text"), StandardCharsets.UTF_8);
   assertFalse(PropUtils.isPropertiesFile(p.toString()));

   // 讀取不存在的檔案應建立並回傳空 map
   Map<String,String> created = PropUtils.loadProps(tempDir.resolve("new.properties").toString());
   assertNotNull(created);
}

測試說明

  • 測試讀取、更新與檔案建立行為
  • 驗證格式檢查與錯誤處理路徑

使用場景

  1. 配置管理

    • 應用程式設定檔讀取與更新
    • 環境變數管理
    • 系統參數配置
  2. 動態設定

    • 運行時更新配置
    • 批次處理參數管理
    • 多環境配置切換
  3. 系統整合

    • 外部系統連接設定
    • 資料庫連線參數管理
    • API 端點配置

程式碼 PropUtils.java

package tw.lewishome.webapp.base.utility.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.GlobalConstants;

/**
 *
 * PropUtils 是一個用於操作 properties 檔案的工具類別,提供讀取、更新及查詢 properties 檔案內容的相關方法。
 * 主要功能包含:
 * <ul>
 * <li>更新指定 properties 檔案中的某個變數值</li>
 * <li>載入 properties 檔案所有變數與內容至 Map</li>
 * <li>查詢指定變數的內容值</li>
 * </ul>
 * 所有方法皆為靜態方法,方便直接呼叫使用。
 *
 *
 *
 * 注意事項:
 * <ul>
 * <li>更新或讀取檔案時,若檔案不存在會自動建立新檔案。</li>
 * <li>載入 properties 時,會忽略註解行(以 # 開頭)及格式不正確的行。</li>
 * <li>更新變數時,若該變數不存在則會新增該變數。</li>
 * <li>所有方法皆可能拋出例外,請妥善處理例外狀況。</li>
 * </ul>
 *
 *
 *
 * 適用場景:
 * <ul>
 * <li>需要動態修改或查詢 Java properties 檔案內容時</li>
 * <li>批次處理或設定檔管理</li>
 * </ul>
 *
 *
 * @author Lewis
 * @version 1.0
 */
@Slf4j
public class PropUtils {
    /** Private constructor to prevent instantiation */
    private PropUtils() {
        throw new IllegalStateException("This is a utility class and cannot be instantiated");
    }

    /**
     * 判斷指定路徑是否為 properties 檔案。
     *
     * 此方法會嘗試以 {@link FileInputStream} 開啟並以 {@link Properties#load(InputStream)}
     * 載入檔案。若載入成功(即能被解析為 properties 格式),則回傳 {@code true}。
     * 若檔案不存在、不可讀或在載入時發生 IO/解析錯誤,則回傳 {@code false}。
     * 空檔案或僅有註解的檔案仍會視為可解析並回傳 {@code true}(因為 load() 成功)。
     *
     *
     * @param filePath 屬性檔案的路徑
     * @return 若能成功以 Properties 載入則回傳 {@code true},否則回傳 {@code false}
     */
    public static boolean isPropertiesFile(String filePath) {

        List<String> listFilelines = new ArrayList<>();
        try {
            listFilelines = FileUtils.fileToStringArrayList(filePath);
        } catch (IOException e) {
            log.debug("無法讀取檔案 [{}]: {}", filePath, e.getMessage());
            return false;
        }
        if (listFilelines.size() == 0) {
            log.debug("Properties 檔案 [{}] 為空檔案或不存在", filePath);
            return true; // 空檔案視為有效的 properties 檔案
        }
        for (int i = 0; i < listFilelines.size(); i++) {
            String oneLine = listFilelines.get(i).trim();
            if (oneLine.isEmpty() || oneLine.startsWith("#")) {
                continue; // 忽略空行和註解行
            }
            if (Boolean.FALSE.equals(oneLine.contains("="))) {
                log.debug("檔案 [{}] 第 {} 行格式錯誤: {}", filePath, i + 1, oneLine);
                return false; // 非 properties 格式
            }
        }
        return true; // 成功載入且格式正確
    }

    /**
     * 更新檔案 propertyFileName 的變數 propName 內容 value
     *
     * @param propNameKey a String object
     * @param value       對應 內容
     * @throws java.lang.Exception java.lang.Exception
     * 
     */
    public static void updateProperty(String propNameKey, String value) throws Exception {
        updateProperty(GlobalConstants.PROPERTIES_FILE, propNameKey, value);

    }

    /**
     * 更新檔案 propertyFileName 的變數 propName 內容 value
     *
     * @param propertyFileName properties File name
     * @param propertyValue    對應 內容
     * @param propertyKey      a String object
     * @throws IOException IOException
     */
    public static void updateProperty(String propertyFileName, String propertyKey, String propertyValue) throws IOException {

        // 1. Load existing properties
        List<String> listFilelines = new ArrayList<>();
        try {
            listFilelines = FileUtils.fileToStringArrayList(propertyFileName);
        } catch (IOException e) {
            log.debug("無法讀取檔案 [{}]: {}", propertyFileName, e.getMessage());
            return;
        }

        List<String> listNewFilelines = new ArrayList<>();
        // 2. update the property
        boolean hasKey = false;
        for (int i = 0; i < listFilelines.size(); i++) {
            String oneLine = listFilelines.get(i).trim();
            if (oneLine.isEmpty() || oneLine.startsWith("#")) {
                listNewFilelines.add(listFilelines.get(i));
                continue; // 忽略空行和註解行
            }
            List<String> splitLine = CommUtils.splitDelimiter(oneLine, "=");
            if (splitLine.size() == 2) {
                String key = splitLine.get(0).trim();
                String value = splitLine.get(1).trim();
                if (key.equals(propertyKey)) {
                    value = propertyValue;
                    listNewFilelines.add(key + "=" + value);
                    hasKey = true;
                    continue;
                } 
                listNewFilelines.add(oneLine);                               
            } else {
                listNewFilelines.add(oneLine);
            }
        }
        if (Boolean.FALSE.equals(hasKey)) {
            // Append new property if key not found
            listNewFilelines.add(propertyKey + "=" + propertyValue);
        }

        // 3. Save the updated properties
        FileUtils.stringArrayListToFile(listNewFilelines, propertyFileName);

    }

    /**
     * 取的檔案 propertyFileName 的所有變數與內容 (以File)
     *
     * @param propertyFile 對應 properties File
     * @return Map 對應 properties 變數
     * @throws IOException IOException
     */
    public static Map<String, String> loadProps(String propertyFile) throws IOException {
        Map<String, String> mapProperties = new HashMap<>();
         // 1. Load existing properties
        List<String> listFilelines = new ArrayList<>();
        try {
            listFilelines = FileUtils.fileToStringArrayList(propertyFile);
        } catch (IOException e) {
            log.debug("無法讀取檔案 [{}]: {}", propertyFile, e.getMessage());
            return mapProperties ;             
        }
        for (int i = 0; i < listFilelines.size(); i++) {
            String oneLine = listFilelines.get(i).trim();
            if (oneLine.isEmpty() || oneLine.startsWith("#")) {
                continue; // 忽略空行和註解行
            }
            List<String> splitLine = CommUtils.splitDelimiter(oneLine, "=");
            if (splitLine.size() == 2) {
                String key = splitLine.get(0).trim();
                String value = splitLine.get(1).trim();
                mapProperties.put(key, value);
            } else {
                log.debug("檔案 [{}] 第 {} 行格式錯誤: {}", propertyFile, i + 1, oneLine);
            }
        }
        return mapProperties;
    }

    /**
     * 取的檔案 propertyFileName 的所有變數與內容 (以File Name String)
     *
     * @param propertyFileName 對應 properties File Name
     * @return Map 對應 properties 變數
     * @throws IOException IOException
     */
    public static Map<String, String> loadProps(File propertyFileName) throws IOException {

        String propertyFile = propertyFileName.getAbsolutePath();

        return loadProps(propertyFile);
    }

    /**
     * 取的檔案 propertyFileName 的 propName 變數與內容 (以File Name String)
     *
     * @param propertyFileName 對應 properties File Name
     * @param propName         property 變數
     * @return String properties 變數內容
     */
    public static String getProps(String propertyFileName, String propName) {
        try {
            File propertyFile = new File(propertyFileName);
            Map<String, String> allProperties = loadProps(propertyFile);

            return allProperties.get(propName);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 取的檔案 default PROPERTIES_FILE 的 propName 變數與內容
     *
     * @param propName property 變數
     * @return String properties 變數內容
     */
    public static String getProps(String propName) {
        return getProps(GlobalConstants.PROPERTIES_FILE, propName);
    }
}

單元測試程式碼 PropUtilsTest.java

package tw.lewishome.webapp.base.utility.common;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;

/**
 * PropUtils 的單元測試類別
 */
class PropUtilsTest {

    private static final String TEST_PROP_FILE = "test.properties";
    private File testFile;

    @TempDir
    Path tempDir;

    @BeforeEach
    void setUp() throws IOException {
        // 建立測試用的 properties 檔案
        testFile = tempDir.resolve(TEST_PROP_FILE).toFile();
        FileUtils.stringArrayListToFile(java.util.Arrays.asList(
                "test.key1=value1",
                "test.key2=value2",
                "#This is a comment",
                "test.key3=value3"), testFile.getAbsolutePath());
    }

    @AfterEach
    void tearDown() {
        // 清理測試檔案
        FileUtils.deleteFile(testFile.getAbsolutePath());
    }

    @Test
    void testIsPropertiesFile() {
        // 測試有效的 properties 檔案
        assertTrue(PropUtils.isPropertiesFile(testFile.getAbsolutePath()));

        // 測試不存在的檔案
        assertFalse(PropUtils.isPropertiesFile("nonexistent.properties"));

        // 測試無效的 properties 檔案
        try {
            File invalidFile = tempDir.resolve("invalid.properties").toFile();
            FileUtils.stringArrayListToFile(java.util.Arrays.asList(
                    "invalid content",
                    "not a properties format"), invalidFile.getAbsolutePath());
            assertFalse(PropUtils.isPropertiesFile(invalidFile.getAbsolutePath()));
        } catch (IOException e) {
            fail("Should not throw exception: " + e.getMessage());
        }
    }

    @Test
    void testUpdateProperty() throws Exception {
        // 測試更新現有屬性
        PropUtils.updateProperty(testFile.getAbsolutePath(), "test.key1", "newValue1");
        assertEquals("newValue1", PropUtils.getProps(testFile.getAbsolutePath(), "test.key1"));
        // 測試新增屬性
        PropUtils.updateProperty(testFile.getAbsolutePath(), "test.key4", "value4");
        assertEquals("value4", PropUtils.getProps(testFile.getAbsolutePath(), "test.key4"));
        // 測試更新空值
        PropUtils.updateProperty(testFile.getAbsolutePath(), "test.key5", "");
        assertNull(PropUtils.getProps(testFile.getAbsolutePath(), "test.key5"));
    }

    @Test
    void testLoadProps() throws IOException {
        // 測試載入所有屬性
        Map<String, String> props = PropUtils.loadProps(testFile);

        assertNotNull(props);
        assertEquals(3, props.size());
        assertEquals("value1", props.get("test.key1"));
        assertEquals("value2", props.get("test.key2"));
        assertEquals("value3", props.get("test.key3"));

        // 測試載入不存在的檔案
        File nonExistentFile = new File("nonexistent.properties");
        Map<String, String> emptyProps = PropUtils.loadProps(nonExistentFile);
        assertTrue(emptyProps.isEmpty());
    }

    @Test
    void testGetProps() {
        // 測試取得存在的屬性值
        assertEquals("value1", PropUtils.getProps(testFile.getAbsolutePath(), "test.key1"));
        assertEquals("value2", PropUtils.getProps(testFile.getAbsolutePath(), "test.key2"));

        // 測試取得不存在的屬性值
        assertNull(PropUtils.getProps(testFile.getAbsolutePath(), "nonexistent.key"));

        // 測試取得不存在檔案中的屬性值
        assertNull(PropUtils.getProps("nonexistent.properties", "test.key1"));
    }

    @Test
    void testLoadPropsByString() throws IOException {
        // 測試使用檔案路徑字串載入屬性
        Map<String, String> props = PropUtils.loadProps(testFile.getAbsolutePath());

        assertNotNull(props);
        assertEquals(3, props.size());
        assertEquals("value1", props.get("test.key1"));

        // 測試載入不存在的檔案
        Map<String, String> emptyProps = PropUtils.loadProps("nonexistent.properties");
        assertTrue(emptyProps.isEmpty());
    }
}

圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言