1. 概述

Java的Properties是存储应用配置的键值对集合。通常我们将这些配置保存在属性文件中,以保持代码结构清晰。Java为Properties提供了原生支持,方便我们在代码中修改属性文件和属性值。

Java的Properties类提供了存储修改后属性的方法,但核心方法会覆盖整个属性文件。因此我们需要自定义方案,在修改属性时避免影响文件中的其他配置。

本文将展示如何在不丢失现有数据的前提下修改属性文件,分别使用原生Java API和Apache Commons库实现,最后介绍如何处理XML格式的属性文件。

2. 项目结构和工具类

先定义属性文件修改器的接口:

public interface PropertyMutator {
    String getProperty(String key) throws IOException, ConfigurationException;

    void addOrUpdateProperty(String key, String value) throws IOException, ConfigurationException;
}

PropertyMutator接口定义了两个核心方法:获取属性值和新增/更新属性。每个实现类都需要提供这些功能。

再创建一个工具类用于加载属性文件:

public class PropertyLoader {
    public Properties fromFile(String filename) throws IOException {
        String appPropertiesFileName = getFilePathFromResources(filename);
        FileInputStream in = new FileInputStream(appPropertiesFileName);
        Properties properties = new Properties();

        properties.load(in);
        in.close();

        return properties;
    }

    public String getFilePathFromResources(String filename) {
        URL resourceUrl = getClass().getClassLoader()
          .getResource(filename);
        Objects.requireNonNull(resourceUrl, "Property file with name [" + filename + "] was not found.");

        return resourceUrl.getFile();
    }
}

PropertyLoader类提供了从项目资源目录加载属性文件的方法。*getFilePathFromResources()*用于获取资源文件的绝对路径:

资源目录结构

测试用的app.properties文件内容如下:

version=1.0
name=TestApp
date=2016-11-12

后续测试将验证:修改操作不会影响其他现有属性(如name和date)。

3. 使用Java文件流修改属性文件

从另一个角度看,修改属性文件本质就是文件操作。Java的java.io包提供了FileInputStreamFileOutputStream类,我们可以利用它们实现属性修改。

实现PropertyMutator接口的addOrUpdateProperty方法:

public class FileStreamsPropertyMutator implements PropertyMutator {
    private final String propertyFileName;
    private final PropertyLoader propertyLoader;

    public FileStreamsPropertyMutator(String propertyFileName, PropertyLoader propertyLoader) {
        this.propertyFileName = propertyFileName;
        this.propertyLoader = propertyLoader;
    }

    @Override
    public void addOrUpdateProperty(String key, String value) throws IOException {
        Properties properties = propertyLoader.fromFile(propertyFileName);
        properties.setProperty(key, value);

        FileOutputStream out = new FileOutputStream(propertyLoader.getFilePathFromResources(propertyFileName));
        properties.store(out, null);
        out.close();
    }

    // ... 实现接口所需的其他方法
}

在*addOrUpdateProperty()*中:

  1. 加载现有属性
  2. 设置新属性值
  3. 使用FileOutputStream保存修改

通过JUnit测试新增属性是否影响其他属性:

@Test
void givenFileStreams_whenAddNonExistingProperty_thenNewPropertyWithoutAffectingOtherProperties()
  throws IOException {
    assertEquals("TestApp", propertyMutator.getProperty("name"));
    assertNull(propertyMutator.getProperty("new.property"));

    propertyMutator.addOrUpdateProperty("new.property", "new-value");

    assertEquals("new-value", propertyMutator.getProperty("new.property"));
    assertEquals("TestApp", propertyMutator.getProperty("name"));
}

测试逻辑:

  • ✅ 验证"name"存在,"new.property"不存在
  • ✅ 添加新属性
  • ✅ 确认新属性已添加且原有属性未受影响

⚠️ *getProperty()*方法未缓存属性,每次调用都会重新加载文件:

@Override
public String getProperty(String key) throws IOException {
    Properties properties = propertyLoader.fromFile(propertyFileName);

    return properties.getProperty(key);
}

同一方法也可用于更新现有属性

@Test
void givenFileStreams_whenUpdateExistingProperty_thenUpdatedPropertyWithoutAffectingOtherProperties()
  throws IOException {
    assertEquals("1.0", propertyMutator.getProperty("version"));
    assertEquals("TestApp", propertyMutator.getProperty("name"));

    propertyMutator.addOrUpdateProperty("version", "2.0");

    assertEquals("2.0", propertyMutator.getProperty("version"));
    assertEquals("TestApp", propertyMutator.getProperty("name"));
}

测试要点:

  • ✅ 验证两个属性的初始值
  • ✅ 更新version属性
  • ✅ 确认version已更新且name属性未受影响

4. 使用Apache Commons修改属性文件

Apache Commons库提供了更优雅的解决方案,特别是FileBasedConfigurationBuilder类。

先添加Maven依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-configuration2</artifactId>
    <version>2.11.0</version>
</dependency>
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.10.1</version>
</dependency>

实现修改器:

public class ApacheCommonsPropertyMutator implements PropertyMutator {
    private final String propertyFileName;

    public ApacheCommonsPropertyMutator(String propertyFileName) {
        this.propertyFileName = propertyFileName;
    }

    @Override
    public void addOrUpdateProperty(String key, String value) throws ConfigurationException {
        FileBasedConfigurationBuilder<FileBasedConfiguration> builder = getAppPropertiesConfigBuilder();
        Configuration configuration = builder.getConfiguration();

        configuration.setProperty(key, value);
        builder.save();
    }

    // ... 实现接口所需的其他方法

    private FileBasedConfigurationBuilder<FileBasedConfiguration> getAppPropertiesConfigBuilder() {
        Parameters params = new Parameters();

        return new FileBasedConfigurationBuilder<FileBasedConfiguration>(PropertiesConfiguration.class).configure(params.properties()
          .setFileName(propertyFileName));
    }
}

关键步骤:

  1. 通过*getAppPropertiesConfigBuilder()*获取配置构建器
  2. 使用*Configuration.setProperty()*添加/更新属性
  3. 调用*builder.save()*保存修改

测试新增属性:

@Test
void givenApacheCommons_whenAddNonExistingProperty_thenNewPropertyWithoutAffectingOtherProperties()
  throws ConfigurationException {
    assertNull(propertyMutator.getProperty("new.property"));
    assertEquals("TestApp", propertyMutator.getProperty("name"));

    propertyMutator.addOrUpdateProperty("new.property", "new-value");

    assertEquals("new-value", propertyMutator.getProperty("new.property"));
    assertEquals("TestApp", propertyMutator.getProperty("name"));
}

验证逻辑:

  • ✅ 确认新属性不存在
  • ✅ 添加新属性
  • ✅ 验证新属性存在且原有属性未受影响

更新现有属性同样简单

@Test
void givenApacheCommons_whenUpdateExistingProperty_thenUpdatedPropertyWithoutAffectingOtherProperties()
  throws ConfigurationException {
    assertEquals("1.0", propertyMutator.getProperty("version"));
    assertEquals("TestApp", propertyMutator.getProperty("name"));

    propertyMutator.addOrUpdateProperty("version", "2.0");

    assertEquals("2.0", propertyMutator.getProperty("version"));
    assertEquals("TestApp", propertyMutator.getProperty("name"));
}

测试要点:

  • ✅ 验证初始属性值
  • ✅ 更新version属性
  • ✅ 确认更新生效且其他属性未受影响

5. 使用Java Files API修改属性文件

Java的Files API提供了更现代的文件操作方式,但有个坑:它直接处理文件行而非键值对。因此FileAPIPropertyMutator不实现PropertyMutator接口。

5.1. 添加属性

添加属性相对简单,只需在文件末尾追加新行:

public class FileAPIPropertyMutator {
    private final String propertyFileName;
    private final PropertyLoader propertyLoader;

    public FileAPIPropertyMutator(String propertyFileName, PropertyLoader propertyLoader) {
        this.propertyFileName = propertyFileName;
        this.propertyLoader = propertyLoader;
    }

    // ... 实现方法

    private List<String> getFileLines() throws IOException {
        File propertiesFile = new File(propertyLoader.getFilePathFromResources(propertyFileName));
        return getFileLines(propertiesFile);
    }

    private List<String> getFileLines(File propertiesFile) throws IOException {
        return new ArrayList<>(Files.readAllLines(propertiesFile.toPath(), StandardCharsets.UTF_8));
    }
}

*getFileLines()*方法读取文件所有行:

public void addPropertyKeyWithValue(String keyAndValue) throws IOException {
    File propertiesFile = new File(propertyLoader.getFilePathFromResources(propertyFileName));
    List<String> fileContent = getFileLines(propertiesFile);

    fileContent.add(keyAndValue);
    Files.write(propertiesFile.toPath(), fileContent, StandardCharsets.UTF_8);
}

添加属性的步骤:

  1. 读取现有文件行
  2. 追加新属性行
  3. 写回文件

测试新增属性:

@Test
void givenFilesAPI_whenAddNonExistingProperty_thenNewPropertyWithoutAffectingOtherProperties()
  throws IOException {
    assertEquals("name=TestApp", propertyMutator.getPropertyKeyWithValue(1));

    propertyMutator.addPropertyKeyWithValue("new.property=new-value");

    assertEquals("new.property=new-value", propertyMutator.getLastPropertyKeyWithValue());
    assertEquals("name=TestApp", propertyMutator.getPropertyKeyWithValue(1));
}

验证要点:

  • ✅ 确认现有属性存在
  • ✅ 添加新属性
  • ✅ 验证新属性添加成功且原有属性未受影响

5.2. 更新现有属性值

更新属性需要处理行定位和替换:

public int updateProperty(String oldKeyValuePair, String newKeyValuePair) throws IOException {
    File propertiesFile = new File(propertyLoader.getFilePathFromResources(propertyFileName));
    List<String> fileContent = getFileLines(propertiesFile);
    int updatedIndex = -1;

    for (int i = 0; i < fileContent.size(); i++) {
        if (fileContent.get(i)
          .replaceAll("\\s+", "")
          .equals(oldKeyValuePair)) {
            fileContent.set(i, newKeyValuePair);
            updatedIndex = i;
            break;
        }
    }
    Files.write(propertiesFile.toPath(), fileContent, StandardCharsets.UTF_8);

    return updatedIndex;
}

关键步骤:

  1. 读取所有文件行
  2. 遍历查找目标属性行
  3. 替换找到的行
  4. 写回文件并返回更新位置

测试属性更新:

@Test
void givenFilesAPI_whenUpdateExistingProperty_thenUpdatedPropertyWithoutAffectingOtherProperties()
  throws IOException {
    assertEquals("name=TestApp", propertyMutator.getPropertyKeyWithValue(1));

    int updatedPropertyIndex = propertyMutator.updateProperty("version=1.0", "version=2.0");

    assertEquals("version=2.0", propertyMutator.getPropertyKeyWithValue(updatedPropertyIndex));
    assertEquals("name=TestApp", propertyMutator.getPropertyKeyWithValue(1));
}

验证逻辑:

  • ✅ 确认现有属性存在
  • ✅ 更新version属性
  • ✅ 验证更新生效且其他属性未受影响

6. 修改XML属性文件

*处理XML属性文件时,我们使用Properties类的loadFromXML()storeToXML()*方法**:

public class XMLFilePropertyMutator implements PropertyMutator {
    private final String propertyFileName;

    public XMLFilePropertyMutator(String propertyFileName) {
        this.propertyFileName = propertyFileName;
    }

    // ... 实现方法

    private Properties loadProperties() throws IOException {
        return loadProperties(getXMLAppPropertiesWithFileStreamFilePath());
    }

    private Properties loadProperties(String filepath) throws IOException {
        Properties props = new Properties();
        try (InputStream is = Files.newInputStream(Paths.get(filepath))) {
            props.loadFromXML(is);
        }

        return props;
    }

    String getXMLAppPropertiesWithFileStreamFilePath() {
        URL resourceUrl = getClass().getClassLoader()
          .getResource(propertyFileName);
        Objects.requireNonNull(resourceUrl, "Property file with name [" + propertyFileName + "] was not found.");

        return resourceUrl.getFile();
    }
}

测试用的icons.xml文件:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>xml example</comment>
    <entry key="fileIcon">icon1.jpg</entry>
    <entry key="imageIcon">icon2.jpg</entry>
    <entry key="videoIcon">icon3.jpg</entry>
</properties>

添加/更新属性的方法:

@Override
public void addOrUpdateProperty(String key, String value) throws IOException {
    String filePath = getXMLAppPropertiesWithFileStreamFilePath();
    Properties properties = loadProperties(filePath);

    try (OutputStream os = Files.newOutputStream(Paths.get(filePath))) {
        properties.setProperty(key, value);
        properties.storeToXML(os, null);
    }
}

操作步骤:

  1. 加载XML属性
  2. 设置新属性值
  3. 使用*storeToXML()*保存修改

测试新增属性:

@Test
void givenXMLPropertyFile_whenAddNonExistingProperty_thenNewPropertyWithoutAffectingOtherProperties()
  throws IOException {
    assertEquals("icon1.jpg", propertyMutator.getProperty("fileIcon"));
    assertNull(propertyMutator.getProperty("new.property"));

    propertyMutator.addOrUpdateProperty("new.property", "new-value");

    assertEquals("new-value", propertyMutator.getProperty("new.property"));
    assertEquals("icon1.jpg", propertyMutator.getProperty("fileIcon"));
}

验证要点:

  • ✅ 确认fileIcon存在,new.property不存在
  • ✅ 添加新属性
  • ✅ 验证新属性存在且原有属性未受影响

更新现有属性同样适用

@Test
void givenXMLPropertyFile_whenUpdateExistingProperty_thenUpdatedPropertyWithoutAffectingOtherProperties()
  throws IOException {
    assertEquals("icon1.jpg", propertyMutator.getProperty("fileIcon"));
    assertEquals("icon2.jpg", propertyMutator.getProperty("imageIcon"));

    propertyMutator.addOrUpdateProperty("fileIcon", "icon5.jpg");

    assertEquals("icon5.jpg", propertyMutator.getProperty("fileIcon"));
    assertEquals("icon2.jpg", propertyMutator.getProperty("imageIcon"));
}

测试逻辑:

  • ✅ 验证初始属性值
  • ✅ 更新fileIcon属性
  • ✅ 确认更新生效且其他属性未受影响

7. 总结

本文介绍了Java中修改属性文件的多种方案:

  1. 文件流方案:使用FileInputStream/OutputStream,简单直接
  2. Apache Commons方案:通过FileBasedConfigurationBuilder,代码更优雅
  3. Files API方案:现代API,但需处理行级操作
  4. XML属性文件:使用Properties的XML专用方法

所有方案都确保了在修改单个属性时不会影响文件中的其他配置。完整代码示例可在GitHub获取。


原始标题:Modify Property Files in Java | Baeldung