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包提供了FileInputStream和FileOutputStream类,我们可以利用它们实现属性修改。
实现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()*中:
- 加载现有属性
- 设置新属性值
- 使用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));
}
}
关键步骤:
- 通过*getAppPropertiesConfigBuilder()*获取配置构建器
- 使用*Configuration.setProperty()*添加/更新属性
- 调用*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);
}
添加属性的步骤:
- 读取现有文件行
- 追加新属性行
- 写回文件
测试新增属性:
@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;
}
关键步骤:
- 读取所有文件行
- 遍历查找目标属性行
- 替换找到的行
- 写回文件并返回更新位置
测试属性更新:
@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);
}
}
操作步骤:
- 加载XML属性
- 设置新属性值
- 使用*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中修改属性文件的多种方案:
- 文件流方案:使用FileInputStream/OutputStream,简单直接
- Apache Commons方案:通过FileBasedConfigurationBuilder,代码更优雅
- Files API方案:现代API,但需处理行级操作
- XML属性文件:使用Properties的XML专用方法
所有方案都确保了在修改单个属性时不会影响文件中的其他配置。完整代码示例可在GitHub获取。