排查了大半天,终于把 SpringBoot 读取 properties 中文乱码这个老坑给填平了。这个问题看起来不大,但踩过的人都知道,它会让你本地跑得好好的功能,打包放到服务器上就变成一串问号;也会让你明明在 IDE 里看到的是中文,程序一启动却输出成“锟斤拷”“浣犲ソ”这种看一眼血压就上来的乱码。
我见过不少同事第一反应就是去改数据库连接串、加字符集过滤器,折腾一圈发现根本不关那事。今天这篇,我就按自己从踩坑到根治的过程,把 SpringBoot + properties 中文乱码的来龙去脉、四套解决方案、以及一套标准的排查思路一次讲透。无论你是刚开始用 Spring Boot 的新人,还是被乱码问题反复折腾过的老手,都能找到对应的解法。
1. 乱码为什么总是跟 properties 过不去
1.1 一次让我加班到深夜的乱码事故
先说我自己遇到的一个真实案例。当时负责一个短信服务,短信模板放在自定义的sms.properties文件里,通过@PropertySource加载。奇怪的地方在于:本地 IDEA 里跑得好好的,模板中文显示完全正常,部署到测试服务器后,所有中文全部变成???。
排查一圈后才发现,同事在 Windows 上用旧版编辑器改过一次这个文件,保存成了 GBK 编码,而我本地的是 UTF-8。同一个文件名,两种编码,两台机器,读出来的结果完全不同。
当时以为是个别文件的偶发问题,后来才意识到,这就是 properties 文件在 Java 世界里的“历史遗留规范”和现代开发环境之间长期错位造成的。不把底层机制搞清楚,类似的坑就会换个马甲反复出现。
1.2 源头:同一个 properties,两种默认编码规则
Java 的Properties类诞生得很早,早期规范明确规定:Properties.load(InputStream)读取时固定按 ISO-8859-1 解析。ISO-8859-1 是单字节字符集,只覆盖西文字符,所以 properties 文件里直接写中文,按规范走就是会乱。这也是为什么 JDK 自带一个native2ascii工具,专门把 properties 里的非 ASCII 字符转成\uXXXX转义序列。
但在 Spring Boot 项目里,事情没那么简单,因为有两条完全不同的读取入口:
第一条是application.properties。Spring Boot 2.x 以后,它由PropertiesPropertySourceLoader负责加载,这个类内部用UnicodeReader并按 UTF-8 解码,还会自动处理 UTF-8 BOM。所以主配置文件里的中文,在 Spring Boot 2.x 下基本不会因为“读取器不支持中文”而乱。真正导致它乱码的原因,通常是文件本身根本不是 UTF-8 编码——比如在 Windows 下被另存为 ANSI/GBK,或者 IDE 的编码设置不对。
第二条是自定义 properties 文件,比如用@PropertySource("classpath:xxx.properties")加载的配置。如果不显式指定encoding属性,Spring 底层走的是经典Properties.load(InputStream)路径,也就是按 ISO-8859-1 解码。哪怕你的文件是标准的 UTF-8 编码,中文照样会被读成乱码。
这就是大多数人困惑的地方:同一个 properties 文件,放进application.properties没事,放进自定义配置文件再通过@PropertySource加载就乱。不是文件坏了,而是两条加载路径的默认编码规则根本不一样。
2. 方案一:统一文件编码,让中文在源头活下来
2.1 第一步:把 IDE 编码设置全部校准
绝大多数乱码的第一道闸门,就是开发工具的编码设置。我用 IDEA 举例,如果你在用 Eclipse 或 VS Code,思路也一样。
打开 IDEA 的Settings -> Editor -> File Encodings,确保以下几项全部是 UTF-8:
Global Encoding设为 UTF-8Project Encoding设为 UTF-8Default encoding for properties files设为 UTF-8,并且勾选Transparent native-to-ascii conversion
这个Transparent native-to-ascii conversion是目前 IDEA 里处理 properties 中文最实用的功能。勾选之后,你在编辑区看到的仍然是正常的中文,但文件保存到磁盘时,IDEA 会自动把非 ASCII 字符转成\uXXXX转义序列。也就是说,磁盘上的文件其实是纯 ASCII 内容,任何编码的读取器都不会把它读乱。
还有一个小细节,新手特别容易踩:当你在 IDEA 右下角看到文件编码标识并点击切换时,会弹出Convert和Reload两个选项。Convert是“把文件的磁盘字节重新按目标编码保存”,这是我们要的;Reload只是“换一种编码重新解析显示”,并不会改变文件本身的字节。如果你已经写满了中文,选择 Reload 通常会显示成乱码,这时候再切回原来的编码就好,别急着重新输入。
2.2 第二步:Maven 构建期编码兜底
文件编码统一了,本地 IDEA 里没问题了,不代表构建打包后还没问题。Maven 在编译和资源处理阶段,如果 JVM 默认的file.encoding不是 UTF-8,资源文件一旦经过过滤、复制,就可能被重新编码,中文就坏了。
所以 Maven 项目的pom.xml里,下面这几行我建议从一开始就加上。别嫌麻烦,这是一个几乎零成本的保险:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties>如果项目里对资源文件做了过滤处理,还需要给maven-resources-plugin显式指定编码:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin>project.build.sourceEncoding这个属性同时会被maven-compiler-plugin引用,所以只要这一个属性配上,编译阶段的源码编码和资源处理基本就稳了。这一步尤其对那种多人协作、跨平台开发的项目重要,因为你没法保证每个同事的 IDE 设置都一样。
2.3 第三步:native2ascii 转义,不信任任何环境
如果说前两步是“尽量让环境统一”,那么把 properties 里的中文转成\uXXXX转义,就是“不信任任何环境”的终极手段。JDK 自带的native2ascii工具就是干这个的。
处理一个 UTF-8 编码的文件,命令行这样写:
native2ascii -encoding UTF-8 sms.properties sms-escaped.properties转出来的文件里,中文全部变成类似\u4f60\u597d这样的 ASCII 字符。这样的文件不管放到 Windows、Linux 还是 Docker 容器里,不管 Java 的默认编码是什么,读出来都不会乱。
当然,纯手工维护\uXXXX文件,可读性确实很差。但在老项目里,尤其是那些还在用 JDK 8、团队协作工具链比较混乱的场景,这就是最省心的方案。实际操作上,我建议配合 IDEA 的Transparent native-to-ascii conversion使用:界面上正常写中文,落盘自动转义,两全其美。
转义后的文件可以用file -bi验证,输出应该是us-ascii,这就说明文件已经是全 ASCII 内容,乱码的风险被压缩到零。
3. 方案二:@PropertySource 指定 encoding,指哪打哪
3.1 最直接的修复:加一个 encoding 参数
如果你不想改文件编码,也不打算做转义,那@PropertySource自带的encoding属性是最快的修复手段。Spring Framework 从 4.3 开始支持这个属性,Spring Boot 2.x 和 3.x 都直接用得稳稳的。
写法非常简单:
@Configuration @PropertySource(value = "classpath:sms.properties", encoding = "UTF-8") public class SmsConfig { @Value("${sms.template}") private String template; }加了这个参数之后,Spring 底层会用InputStreamReader按 UTF-8 读取文件,而不是走 ISO-8859-1 的默认路径,中文自然就能正确解析。
但这里有个隐藏前提,我必须强调:encoding = "UTF-8"只对真正的 UTF-8 文件有效。如果你的文件本身就是 GBK/ANSI 编码,这个设置反而会引入新的乱码。要么先把文件转成 UTF-8,要么把encoding改成"GBK"。后者我不太建议长期使用,毕竟 GBK 是特定语言环境的编码,跨平台部署时迟早还会冒出问题。
3.2 哪些场景有效,哪些场景无效
用@PropertySource加 encoding 之前,先确认你到底在跟哪种配置入口打交道。我做了一张简单的对照:
| 配置场景 | encoding 参数是否有效 | 正确做法 |
|---|---|---|
自定义 properties,通过@PropertySource加载 | 有效 | 加encoding = "UTF-8" |
application.properties或带 profile 的application-*.properties | 无效 | 统一文件编码为 UTF-8 |
application.yml/application.yaml | 无效 | 使用 Spring Boot 标准配置加载 |
@ConfigurationProperties绑定配置 | 间接有效 | 取决于属性源解码是否正常 |
很多人会犯一个错误:在@PropertySource里写classpath:application.properties,然后再加 encoding 想修复主配置文件乱码。这种做法意义不大,因为application.properties早在 Environment 后处理阶段就被 Spring Boot 按 UTF-8 加载完了,@PropertySource再去加载一份同名文件,只是给 Environment 额外塞了一个重复的 PropertySource,并不会改变主配置文件的解码结果。修主配置文件的乱码,要从文件编码本身下手。
3.3 多文件、外部文件与动态加载场景
实际项目中,@PropertySource经常要一次加载多个文件,或者加载容器挂载的外部配置文件。这些都是可以并列处理的:
@Configuration @PropertySource( value = { "classpath:sms.properties", "file:${config.dir}/common.properties" }, encoding = "UTF-8", ignoreResourceNotFound = true ) public class AppConfig { }encoding和ignoreResourceNotFound可以同时使用。file:前缀用于加载运行环境里的外部文件,这在 Docker 部署时很常见——把配置挂载进容器,然后启动时通过环境变量指定路径。外部文件同样要满足“文件本身是 UTF-8”这个前提,否则 encoding 参数救不了你。
我建议把外部配置文件纳入部署检查清单,每次发布前确认一次文件编码,别等启动后日志里冒出一堆乱码才开始排查。
4. 方案三:趁早换 YAML,省心一劳永逸
4.1 为什么 YAML 对中文天然友好
YAML 之所以成为 Spring Boot 官方主推的配置格式,除了结构清晰之外,还有一个容易被忽略的优点:它在编码处理上几乎不会出岔子。Spring Boot 加载application.yml时,走的是 SnakeYAML 的解析链路,默认按 UTF-8 解码。中文写进去,读出来就是中文,不用转义,不用指定 encoding。
如果你在一个新项目里,或者手头项目的配置还不算多,我的建议很直接:能用 YAML 就尽量用 YAML。同样是配置中文内容,properties 需要操心文件编码、读取编码、转义规则,而 YAML 只需要保证文件本身是 UTF-8。
举个直观的例子。把老的 properties 配置:
app.name=体验中心 app.desc=这是中文描述转成 YAML:
app: name: 体验中心 desc: 这是中文描述层级关系一眼就明白,@ConfigurationProperties绑定也更顺手。新项目直接在application.yml里写中文,几乎遇不到乱码问题。
4.2 Spring Boot 2.4+ 配置加载机制的新变化
Spring Boot 2.4 把配置加载机制重构过一次,引入了ConfigData和spring.config.import,同时对 properties 文件增加了多文档支持,用#---分隔不同 profile 的片段。这次重构之后,application.properties的编码处理依然沿用了 UTF-8 读取策略,所以对乱码本身没有引入新的坑,但有几个变化值得注意:
第一,spring.config.import加载的外部配置文件,编码同样按 UTF-8 处理,这点和@PropertySource不是一套逻辑。如果你被@PropertySource的习惯带偏,可能会误判外部文件的解码方式。
第二,properties 多文档支持让配置文件内部的 profile 隔离变得更方便,但#---分隔符前后不能有空格,这是个容易顺手踩的坑。
第三,从 2.4 开始,原来有些通过spring.config.location之类方式处理的场景,行为有变化,老项目升级时配置加载顺序会出现差异。如果升级后配置莫名其妙读不到或优先级不对,重点往这个方向排查。
4.3 遗留 properties 迁移到 YAML 的实操建议
老项目里如果有大量 properties 文件,全部迁移确实有工作量,但可以把风险分摊开。我的操作顺序是这样的:
- 把
application.properties的内容复制成application.yml,按层级结构重写,同时保留原文件作为回滚方案。 - 全局搜索
@PropertySource引用,把加载的 properties 文件逐步替换成 YAML。注意,@PropertySource默认加载不了 YAML,需要自定义PropertySourceFactory,配合 Spring Boot 的YamlPropertySourceLoader实现。 - 全局搜索
@Value和@ConfigurationProperties绑定的 key,确认转换后的层级结构和原 key 能对上。 - 启动项目,逐个模块核对配置项是否读到预期值。
迁移过程中有一个细节容易漏:YAML 对纯数字字符串的类型推断比较激进,比如手机号13800138000如果没加引号,可能被解析成数字1380013800。带前导零的字符串还会丢零。如果配置项里这类内容,迁移时给它们加上引号,避免类型转换带来的新问题。
5. 方案四:代码兜底,清空最后一片乱码
5.1 手动读取外部配置文件,自己控制编码
有些场景下,配置文件不在 classpath 里,也不是 Spring Boot 标准加载路径能覆盖的,比如容器挂载的临时文件、第三方系统生成的配置、或者格式比较特殊的.conf文件。这时候与其纠结 Spring 的默认行为,不如直接手动读取,把编码控制权握在自己手里。
经典写法:
InputStream in = new FileInputStream(configFile); BufferedReader reader = new BufferedReader( new InputStreamReader(in, StandardCharsets.UTF_8) ); Properties props = new Properties(); props.load(reader);核心就是InputStreamReader的字符集参数。Java 的Properties.load(Reader)会完全遵循你传入 Reader 的编码,不会再强行按 ISO-8859-1 解析。这一招在复杂部署环境下特别好用,不管配置从哪来,只要文件字节本身是 UTF-8,代码就能保证读出来是中文。
5.2 用 PropertySourceFactory 统一编码策略
如果你不想在每个@PropertySource上都写encoding = "UTF-8",还有一个更优雅的方式:自定义PropertySourceFactory。Spring 的@PropertySource支持factory属性,允许你完全接管属性源的加载逻辑。
下面这个工厂类,相当于把所有 properties 文件的默认编码强制设为 UTF-8:
import org.springframework.core.io.support.DefaultPropertySourceFactory; import org.springframework.core.io.support.EncodedResource; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertiesPropertySource; import org.springframework.core.io.support.PropertiesLoaderUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Properties; public class Utf8PropertySourceFactory extends DefaultPropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException { Properties props = PropertiesLoaderUtils.loadProperties( new EncodedResource(resource.getResource(), StandardCharsets.UTF_8)); String sourceName = (name != null) ? name : (resource.getResource().getFilename() != null ? resource.getResource().getFilename() : "utf8-properties"); return new PropertiesPropertySource(sourceName, props); } }使用方式:
@Configuration @PropertySource( value = "classpath:sms.properties", factory = Utf8PropertySourceFactory.class ) public class SmsConfig { }用工厂方案之后,encoding属性就可以不写了,避免两套配置混在一起反而造成混乱。工厂里也可以加日志、解密、从远端拉取配置等逻辑,灵活性比单纯指定 encoding 高得多。这个方案特别适合那种配置比较多、团队多模块并行开发的场景,一次封装,全项目复用。
5.3 顺带排查其他容易跟乱码混淆的入口
排查 properties 乱码时,很容易把周边模块的乱码问题也引到自己身上。有几个入口经常被误判,我顺手列一下:
logback-spring.xml或log4j2.xml中的中文日志格式、自定义输出字段,乱码通常是 XML 文件本身的编码问题,和 properties 无关。banner.txt里的中文字符,如果启动时控制台显示乱码,大概率是终端字符集问题,不是配置加载问题。- 数据库连接串里的中文参数,比如某些字符集配置写成了中文,往往需要在 URL 里做 URL 编码,而不是靠配置文件编码解决。
这些容易混淆的入口,排查思路上要分开:配置文件乱码,重点看“文件编码”和“读取编码”;输出乱码,重点看“终端编码”和“日志框架编码”。
6. 实战排查:一套标准流程定位乱码根因
6.1 从乱码形态反推原因
乱码不是随机产生的,不同形态对应不同的错位方式。看一眼乱码长什么样,基本能猜出问题方向。
| 乱码形态 | 典型原因 | 修复方向 |
|---|---|---|
???或大量问号 | UTF-8 或 GBK 字节被 ISO-8859-1 解码 | 指定读取编码为 UTF-8 |
锟斤拷 | UTF-8 字节被 GBK 错误解码后再重新编码 | 统一文件与读取编码 |
浣犲ソ这类汉字乱码 | UTF-8 字节被 GBK 解码 | 按 UTF-8 读取或转文件编码 |
中文变成\uXXXX字面量 | 文件已转义但被二次转义或显示层未转换 | 检查读取后的解码与展示链路 |
看到锟斤拷和浣犲ソ这类典型乱码,不要慌,直接往“UTF-8 和本机默认编码不一致”的方向查,八九不离十。
6.2 三个命令锁定文件真实编码
排查的第一步,永远是确认磁盘上的文件实际是什么编码。不要依赖编辑器右下角显示的编码,那个只是“当前解释方式”。用命令看最靠谱。
file -bi application.properties输出类似text/plain; charset=utf-8或text/plain; charset=iso-8859-1。如果显示us-ascii,说明文件已经是转义后的纯 ASCII 内容,乱码问题大概率在读取链路。
如果想看具体字节,用xxd看文件头和中文字符区域:
xxd application.properties | head -20UTF-8 编码的常见中文字符是三个字节一组,比如你的 UTF-8 字节是E4 BD A0;GBK 编码则是两个字节一组。如果文件头部能看到EF BB BF,说明文件还带了 UTF-8 BOM。BOM 在 Spring Boot 的UnicodeReader下能被自动剥离,但如果你用@PropertySource走老路径,BOM 有可能被当成不可见字符混进键名,导致取不到配置。
6.3 多环境部署中的编码陷阱
本地正常、服务器乱码,这是被问得最多的一种情况。根因往往出在“开发环境编码”和“运行环境编码”不一致上。
Windows 上,旧版文本编辑器经常把文件默认存成 ANSI(也就是 GBK),而 Linux 服务器的 JVM 按 UTF-8 读取,这就产生错位。另一种常见场景是 Docker 部署:宿主机上手动改过配置文件,经过编辑器的编码转换后,文件变成 GBK,但容器内 Spring Boot 依然按 UTF-8 读,结果就是中文全部变成乱码。
针对环境迁移,我的建议是:在 CI 的打包流程里加一步文件编码检查,或者在发布脚本中强制转换。对于已经乱码的文件,Linux 上用iconv转换也很直接:
iconv -f GBK -t UTF-8 old.properties > new.properties转完再用file -bi验证。要记住一个原则:文件落地到运行环境之前,统一成 UTF-8 无 BOM,并且保持全程不经过任何可能改编码的编辑器。
6.4 避坑速查清单
最后整理一份速查清单,覆盖我这些年遇到的高频场景,可以直接当排查手册用:
| 场景 | 现象 | 解决方案 |
|---|---|---|
application.properties中文乱码 | 启动日志或实际取值全是问号 | 将文件统一保存为 UTF-8 无 BOM,检查 IDE 与 Maven 编码 |
自定义 properties 经@PropertySource加载乱码 | @Value取到的中文是乱码 | 加encoding = "UTF-8",或改用自定义PropertySourceFactory |
| YAML 配置乱码 | 少见,但@PropertySource加载 yml 时可能遇到 | 用 Spring Boot 标准配置加载,别用@PropertySource直接读 yml |
| 本地正常,Linux 上乱码 | 环境编码不一致 | file -bi检查文件编码,统一为 UTF-8,必要时iconv转换 |
| 打包进 jar 后乱码 | IDE 正常,java -jar后乱 | 检查 Mavenproject.build.sourceEncoding和 resources 插件编码 |
| Docker/ConfigMap 挂载后乱码 | 容器内读到的配置中文乱码 | 确认挂载文件编码,ConfigMap 本身要求 UTF-8,宿主机改文件后需重新验证 |
| 日志输出乱码但配置显示正常 | 控制台或文件里日志中文乱码 | 排查终端字符集和日志框架输出编码,与 properties 读取无关 |
我一个很深的体会是:乱码问题不可怕,可怕的是不知道自己的配置到底经过了几条读取路径、被谁用什么编码解过一遍。把“文件实际编码”和“读取器期望编码”两个变量对照起来,问题基本上半小时内就能定位。把 IDE、Maven、容器三处编码统一成 UTF-8,再给自定义配置挂上强制 UTF-8 的@PropertySource,这套组合拳打下来,SpringBoot 读取 properties 的中文乱码基本跟你无缘。