無論是數據庫連接參數、第三方服務地址、功能開關還是業務參數,將其外部化到 .properties 文件是 Java 應用的標準做法。本文從基本讀取開始,逐步深入到生產環境的類型安全訪問、多環境隔離、熱加載、加密解密等進階模式。
Properties props = new Properties();
props.load(new FileInputStream("src/jdbc.properties")); // 或 getResourceAsStream
String driver = props.getProperty("jdbc.driver");
String url = props.getProperty("jdbc.url");
關鍵點:
FileInputStream 相對當前工作目錄;ClassLoader.getResourceAsStream() 相對 classpath 根目錄load() 拋出 IOException,必須捕獲或拋出
# config.properties (UTF-8 編碼保存)
app.name=我的應用
props.load(new FileInputStream("config.properties"));
System.out.println(props.getProperty("app.name")); // 輸出:æç度ç¨
原因:Properties.load(InputStream) 規範強制使用 ISO-8859-1 解碼。
// 使用 Reader 顯式指定編碼
props.load(new InputStreamReader(new FileInputStream("config.properties"), StandardCharsets.UTF_8));
// 或 Files.newBufferedReader (Java 11+)
props.load(Files.newBufferedReader(Paths.get("config.properties"), StandardCharsets.UTF_8));
// 寫入時:native2ascii 轉義或 IDE 自動轉義
// app.name=\u6211\u7684\u5E94\u7528
// 讀取時:自動還原為 Unicode 字符
props.load(new FileInputStream("config.properties")); // 正常讀取
建議:現代項目統一使用 UTF-8 + Reader 讀取,避免轉義帶來的可讀性損失和維護困難。
public class TypedProperties {
private final Properties props;
public TypedProperties(Properties props) { this.props = props; }
public String getString(String key) { return props.getProperty(key); }
public String getString(String key, String def) { return props.getProperty(key, def); }
public int getInt(String key, int def) {
String v = props.getProperty(key);
return v != null ? Integer.parseInt(v) : def;
}
public long getLong(String key, long def) { /* 類似 */ }
public boolean getBoolean(String key, boolean def) { /* 類似 */ }
public Duration getDuration(String key, Duration def) { /* 解析 ISO-8601 或自定義格式 */ }
public List getList(String key, String delimiter, List def) { /* 分割字符串 */ }
// 泛型獲取(配合轉換器)
public T get(String key, Converter converter, T def) {
String v = props.getProperty(key);
return v != null ? converter.convert(v) : def;
}
}
// 使用
TypedProperties tp = new TypedProperties(props);
int poolSize = tp.getInt("db.pool.size", 10);
Duration timeout = tp.getDuration("http.timeout", Duration.ofSeconds(30));
List servers = tp.getList("redis.nodes", ",", Collections.emptyList());
src/main/resources/
├── application.properties # 共享配置
├── application-dev.properties # 開發環境
├── application-test.properties # 測試環境
└── application-prod.properties # 生產環境
public Properties loadForProfile(String profile) {
Properties props = new Properties();
// 1. 基礎配置
load(props, "application.properties");
// 2. 環境特定配置(覆蓋基礎配置)
load(props, "application-" + profile + ".properties");
// 3. 本地覆蓋(不提交版本控制,用於開發機個性化)
load(props, "application-local.properties");
return props;
}
private void load(Properties props, String resource) {
try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) {
if (in != null) props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
} catch (IOException e) {
log.warn("配置加載失敗: {}", resource, e);
}
}
啟動參數指定環境:-Dspring.profiles.active=prod 或 -Dapp.profile=prod
public class ReloadableConfig {
private final Path configPath;
private final WatchService watcher;
private volatile TypedProperties current;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
public ReloadableConfig(String configFile) throws IOException {
this.configPath = Paths.get(configFile).toAbsolutePath();
this.watcher = FileSystems.getDefault().newWatchService();
configPath.getParent().register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
reload();
scheduler.scheduleAtFixedRate(this::pollChanges, 1, 1, TimeUnit.SECONDS);
}
private void pollChanges() {
try {
WatchKey key;
while ((key = watcher.poll()) != null) {
for (WatchEvent> event : key.pollEvents()) {
if (event.context().equals(configPath.getFileName())) {
reload();
break;
}
}
key.reset();
}
} catch (Exception e) {
log.error("配置熱加載異常", e);
}
}
private void reload() {
try (Reader reader = Files.newBufferedReader(configPath, StandardCharsets.UTF_8)) {
Properties props = new Properties();
props.load(reader);
this.current = new TypedProperties(props);
log.info("配置已重新加載: {}", configPath);
} catch (IOException e) {
log.error("配置重新加載失敗", e);
}
}
public TypedProperties get() { return current; }
public void close() { watcher.close(); scheduler.shutdown(); }
}
適用場景:功能開關、業務閾值、限流參數等需頻繁調整無需重啟的配置。
// 命令行加密
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \
input="mysql_password" password="master_key" algorithm=PBEWithHMACSHA512AndAES_256
// 輸出:ENC(G6NvX8...)
# application-prod.properties
db.password=ENC(G6NvX8...)
// pom.xml
com.github.ulisesbocchio
jasypt-spring-boot-starter
3.0.5
// application.yml
jasypt:
encryptor:
password: ${JASYPT_MASTER_PASSWORD} # 從環境變量讀取主密鑰
algorithm: PBEWithHMACSHA512AndAES_256
// 代碼中直接獲取解密後的值
@Value("${db.password}")
String dbPassword; // 自動解密
| 維度 | 建議 | 工具/技術 |
|---|---|---|
| 編碼 | UTF-8 + Reader 讀取 | StandardCharsets.UTF_8, Files.newBufferedReader |
| 類型安全 | 封裝 TypedProperties | 自定義轉換器、Duration/List 解析 |
| 多環境 | profile 分層覆蓋 + local 覆蓋 | application-{profile}.properties |
| 熱加載 | WatchService + 定時輪詢 | java.nio.file.WatchService |
| 敏感信息 | 密文存儲 + 運行時解密 | Jasypt, HashiCorp Vault, AWS Secrets Manager |
| 版本管理 | 配置文件納入 Git,密文/密鑰不入庫 | .gitignore, .gitattributes |
| 驗證 | 啟動時校驗必填項、格式、範圍 | Bean Validation, 自定義校驗器 |
配置管理看似簡單,實則關係應用的可部署性、安全性、可運維性。從 Properties.load() 開始,構建屬於團隊的配置規範與工具鏈,讓配置成為加速交付的資產,而非故障的溫床。