核心是将配置加载抽象为可配置、可替换、可测试的接口,通过configloader、configsource与charsetstrategy解耦编码与数据源,用组合封装properties并支持spi动态切换实现。

核心不是改编码,而是把“读配置”这件事封装成一个可配置、可替换、可测试的对象,让乱码问题在设计层面就被隔离。
把配置加载行为抽象成接口
不直接用 Properties.load(),而是定义一个 ConfigLoader 接口:
- 它只暴露 load(String path) 或 load(InputStream is) 方法,返回 Map
或自定义 Config 对象 - 具体实现类(如 Utf8PropertiesLoader、Native2AsciiLoader、AutoDetectingLoader)各自负责自己的解码逻辑,互不影响
- 调用方完全不知道底层是 ISO-8859-1 还是 UTF-8,也不关心 BOM 或转义,只认接口契约
用组合代替硬编码路径
Properties 类本身不是问题,问题在于它被直接 new 出来又直接 load(InputStream)。换成组合方式:
- 写一个 WrapperProperties 类,内部持有一个 Properties 实例 + 一个 Charset 字段
- 构造时传入 StandardCharsets.UTF_8,load 方法只接受 InputStream,内部自动包装成 InputStreamReader
- 这样既复用 Properties 的解析能力(包括 \uXXXX 转义支持),又绕过其 ISO-8859-1 硬编码
- 还可扩展:加日志、加空行过滤、加 key 标准化(toLowerCase、trim)等横切逻辑
让配置源和编码策略解耦
同一个配置文件,可能在开发环境是 UTF-8,在遗留生产环境是 GBK。面向对象的做法是把“从哪来”和“怎么读”拆开:
- 定义 ConfigSource 接口:getResourceAsStream()、getFilePath()、asBytes() 等获取原始字节的方法
- 定义 CharsetStrategy 接口:guessCharset(byte[])、getFixedCharset()、fallbackTo(StandardCharsets.GBK)
- ConfigReader 类接收两者作为依赖,先取字节,再按策略选 Charset,最后交给 Properties.load(Reader)
- 不同环境只需注入不同的 Strategy 实现,代码零修改
用工厂+SPI支持运行时切换
避免在代码里 if-else 判断 JDK 版本或文件后缀。用标准 Java SPI:
- 在 META-INF/services 下声明 com.example.ConfigLoader
- 提供 jdk8-utf8-loader、jdk11-gbk-loader 等多个实现
- 启动时 ServiceLoader.load(ConfigLoader.class) 自动选择匹配项
- 甚至可结合系统属性 -Dconfig.loader=gbk,动态加载对应实现











