做后端的朋友十有八九都见过这个场景:项目跑得好好的,突然有批数据在日志或者前端页面上变成了一堆问号,或者在 IDEA 里看起来一切正常,一 mvn package 部署到服务器,配置文件里的中文全部变成了 ???。更离谱的是,有的乱码只在 Linux 上出现,Windows 本地怎么测都复现不了。这些问题十有八九都指向同一个根因:properties 文件的编码处理。
Spring Boot 项目里读取 properties 配置文件出现中文乱码,是一件卡过无数人的小问题。说它小,是因为解决起来通常就是一两行配置的事;说它卡人,是因为它横跨 IDE 设置、文件编码、Java 编译、Spring 加载机制好几个层面,任何一个环节出问题都会导致同样的症状。这篇文章我会从 properties 的编码机制讲起,把我在实际项目中踩过的坑、用过的解法和它们各自的适用场景全部梳理一遍,看完你基本就能做到“见码识因、对症下药”。
这篇内容适合谁看?刚接触 Spring Boot 的新手、正被乱码折腾得焦头烂额的维护者、以及想把配置文件编码这件事彻底理清的开发者,都能在里面找到对应自己场景的答案。
1. 乱码到底是怎么产生的:从 properties 的编码机制说起
要解决乱码,先得搞清楚 properties 文件为什么这么容易出问题。很多人以为乱码是“文件坏了”或者“编码格式不对”,这个说法太笼统,真正的原因藏在 properties 文件格式和 Java 类库的设计里。
1.1 properties 文件天生只认 ISO-8859-1
Java 的 java.util.Properties 类从 JDK 1.0 就有了,它的设计目标非常老派:面向 ISO-8859-1 编码设计。什么是 ISO-8859-1?你可以把它理解成拉丁字母体系的单字节编码,每个字符固定占 1 个字节,能表示 256 种字符,里面根本没有中文字符的位置。
Properties.load(InputStream) 方法在加载文件时,会按照 ISO-8859-1 去解码每一个字节。如果你把一个 UTF-8 编码、包含中文的 properties 文件丢给它,就会发生这样的事:UTF-8 里一个汉字占 3 个字节,比如“配置”两个字对应的是 6 个字节,ISO-8859-1 会把每 1 个字节都当作一个独立的“字符”读出来。这还不是最糟糕的,如果某些字节序列在 ISO-8859-1 里是控制字符,读出来的内容甚至会破坏文件结构。
所以最根本的问题是:Properties 这个类在底层设计上就不支持直接写中文。Oracle 官方文档里也明确写了,properties 文件里的非 Latin-1 字符必须用 \uXXXX 这种 Unicode 转义序列来保存。
注意:这里说的“设计上不支持”,指的是 properties 这种文件格式本身,不是说 Java 程序就不能处理中文。你完全可以在 properties 里写转义后的中文,也可以在读取时指定正确的编码,这些解法后面都会讲。
1.2 Spring Boot 的加载链路:一条路上有多个关卡
Spring Boot 加载 application.properties 走的是 ConfigDataEnvironmentPostProcessor 这条链路,最终会用到 Spring 的 PropertiesPropertySourceLoader。顺着这个链路,你会遇到这几个可能出问题的环节:
- 第一关:文件本身的保存编码。文件是 UTF-8 还是 GBK 保存的,决定了字节序列长什么样。
- 第二关:构建工具的处理。Maven 在编译和资源拷贝阶段,可能会因为
project.build.sourceEncoding没设置,使用平台默认编码去读写文件,这会改变文件内容。 - 第三关:Spring 读取时默认使用的编码。Spring Boot 2.4+ 在读取 properties 文件时,默认是按
ISO-8859-1来解码的,除非你显式地通过spring.config.properties.encoding告诉它用别的编码。 - 第四关:读取到的字符串在你的代码里如何被输出。日志、前端页面、数据库存储,每一处都有自己的字符编码设定,任何一个环节不一致,即使配置文件读对了,最终显示出来依然可能是乱码。
把这四关列出来,你就明白为什么同样一个乱码问题,网上的解决方案有七八种,因为大家乱在那个环节各不相同。
1.3 以“为什么”为中心的判断思路
我在排查乱码问题时,习惯先问自己三个问题:
- 乱码出现在哪里?是 IDEA 里打开文件就看到乱码,还是程序运行后输出才乱码?前者是 IDE 读取文件的问题,后者是程序读取文件的问题,这是两条完全不同的排查路线。
- 本地正常、服务器上乱码?大概率是构建阶段或运行环境编码不同导致的,重点查 Maven 配置和服务器默认编码。
- 日志里是
???还是锟斤拷还是一堆\uXXXX转义?每种形态背后都有特定的成因,后面我会专门用一节来讲。
理清楚这几点,再去看解决方案,你就能判断自己该用哪一种,而不是把网上所有方法都试一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一层解法:让文件本身以正确的编码保存
最简单、也最容易被忽视的一层,是确保 properties 文件本身保存的字节是正确的。很多人改了代码、加了配置,最后发现无效,原因就是文件保存时已经被 IDE 用错误编码写入了磁盘。
2.1 IDEA 里把项目统一设置为 UTF-8
如果你用的是 IntelliJ IDEA,请按以下步骤检查:
- 打开
File -> Settings -> Editor -> File Encodings。 - 把
Global Encoding、Project Encoding、Properties Files三个地方全部设置为UTF-8。 - 关键一步:勾选
Transparent native-to-ascii conversion选项。
前面两个设置不难理解,重点说一下第三项。IDEA 默认开启 Transparent native-to-ascii conversion 时,你在编辑器中看到的 properties 文件内容是中文,但磁盘上保存的实际上是 \uXXXX 转义序列。这样设计是为了兼容 Java 的老式 properties 规范,是官方推荐的做法。
这个选项勾选与否会影响最后的解决方式:勾选后,文件在磁盘上以 native 字符保存,源代码里可以直接看到中文,也方便 Git 走 diff。但这意味着文件字节是 UTF-8,在 Spring Boot 2.4+ 默认按 ISO-8859-1 解码时就会出问题。不勾选时,IDEA 会把中文自动转成 \uXXXX 存入文件,这样反而是最“安全”的——不管哪个环境读取,转义后的 ASCII 内容永远不会有编码问题。
如果你发现项目里某个 properties 文件已经以错误编码保存了,在 IDEA 右下角的文件编码切换里改成 UTF-8,会弹出对话框询问 Convert 还是 Reload,这时候要选 Convert,才能真正把文件重新以 UTF-8 写入磁盘。选 Reload 只是改变编辑器显示方式,文件内容不会变。
实操心得:我一般建议团队新建项目时,就在
.gitattributes里固定*.properties text eol=lf,并在统一规范里要求所有配置文件都保存为 UTF-8。这样能避免很多“我本地没问题、提交后被同事一编辑就乱码”的诡异问题。
2.2 用 .mvn/jvm.config 或 Maven 配置固定编码
IDEA 里看着正常不代表构建出来就正常。Maven 在复制 resources 目录时,如果遇到文本文件,会按照 project.build.sourceEncoding 来决定读写编码。如果你没在 pom.xml 里声明这个属性,Maven 会使用操作系统默认编码,Windows 中文系统大概率是 GBK,Linux 服务器大概率是 UTF-8,于是就会出现“本地 OK、上服务器就乱码”的情况。
强烈建议在任何 Maven 项目的 pom.xml 里加上:
xml复制<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties>
这段配置的作用是告诉 Maven:编译源码和拷贝资源时,统一用 UTF-8 读写文件。特别是如果你开启了 IDEA 的 Transparent native-to-ascii conversion,文件里是 \uXXXX 转义,构建时如果被 Maven 用 GBK 转了一遍,转义符可能被二次编码,读出来就是另一种莫名其妙的乱码。
2.3 Gradle 项目的对应配置
Gradle 项目也要做类似的事,在 build.gradle 里设置:
groovy复制tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
如果使用了 processResources 任务,还可以显式指定:
groovy复制processResources {
filteringCharset = 'UTF-8'
}
配置的核心思路和 Maven 完全一样:把构建工具的默认字符集固定下来,不让它依赖操作系统环境。
3. 第二层解法:在 Spring Boot 里显式控制 properties 解码
文件层面搞定了,剩下的问题就是 Spring Boot 读文件时用什么编码。这一层取决于你的 Spring Boot 版本和具体使用方式。
3.1 Spring Boot 2.4+ 的 spring.config.properties.encoding
Spring Boot 从 2.4 开始重构了配置加载机制,引入了 ConfigData 的概念。在这个版本之后,你可以在 application.properties 里加上一行:
properties复制spring.config.properties.encoding=UTF-8
这个配置项的作用是告诉 Spring Boot:读取 properties 配置文件时,使用 UTF-8 而不是默认的 ISO-8859-1 来解码。
听起来很简单,对吧?但这里有个很坑的点:这行配置本身必须能被 Spring Boot 正确读取。如果你的 application.properties 文件里这行配置是写在一堆乱码之间的,其实没关系,因为这行本身是纯 ASCII,不会乱码。但如果你的文件编码是 GBK,而 Spring Boot 又默认按 ISO-8859-1 读,那这行配置读取时 GBK 的中文会被误解,但 spring.config.properties.encoding=UTF-8 这行纯 ASCII 配置不受影响,所以依然能生效。
放到实际项目里,这行配置的局限性也很明显:
- 它只对
application.properties这类通过 Spring Boot 原生机制加载的 properties 文件有效。 - 对
@PropertySource注解手动加载的 properties 文件无效。 - 对
spring-boot-starter以外你自己封装的 Properties 工具类读取无效。
所以这个配置适合解决“Spring Boot 主配置文件里的中文乱码”这个具体场景,但不是万能药。
3.2 用 @PropertySource 读取自定义 properties 文件时的乱码处理
项目里经常有这种写法:
java复制@Configuration
@PropertySource(value = "classpath:my-config.properties")
public class MyConfig {
@Value("${my.config.name}")
private String name;
}
这种写法走的是 @PropertySource 的加载机制,不使用 spring.config.properties.encoding 配置,所以上面那个方案在这里不生效。解决办法是自己实现一个 PropertySourceFactory,用定制编码的 Properties 去加载文件。
java复制public class Utf8PropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
Properties props = new Properties();
// 关键步骤:把输入流的编码指定为 UTF-8
try (InputStreamReader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
props.load(reader);
return new PropertiesPropertySource(resource.getResource().getFilename(), props);
}
}
}
然后在 @PropertySource 里指定这个 Factory:
java复制@Configuration
@PropertySource(value = "classpath:my-config.properties", factory = Utf8PropertySourceFactory.class)
public class MyConfig {
}
这段代码的原理很简单:Properties.load(Reader) 会按你传入的 Reader 的编码去解码,我们传入了 UTF-8 的 Reader,中文自然就能被正确读取。
我自己的项目里一直保留着这个工具类,因为 @PropertySource 加载自定义配置文件的情况太多了,很多第三方库的配置经常需要额外文件来补充,有备无患。
注意:如果你的 properties 文件里存的是
\uXXXX转义序列,那么没关系,Properties.load(Reader)会先解码转义再返回真实字符。但如果你同时开启了 IDEA 的Transparent native-to-ascii conversion,文件里可能同时存在中文和转义序列,这时候一定要确认处理方式,不要出现二次转义。
4. 第三层解法:把编码问题绕开——优先用 YAML 或自定义读取
有时候与其和 properties 的死板编码机制死磕,不如换个文件格式,从根本上绕开这个坑。这一节我讲两个“绕开”的方案,适合不同场景。
4.1 改用 YAML:Spring Boot 原生支持的更优解
YAML 从诞生起就支持 UTF-8,没有 ISO-8859-1 这种历史包袱。Spring Boot 对 application.yml 的加载默认使用 UTF-8 解码,所以你只需要保证文件保存为 UTF-8,YAML 里的中文几乎不会出问题。
把 application.properties 改成 application.yml 的迁移成本很低。大部分配置键值对只是改了写法:
yaml复制server:
port: 8080
custom:
name: 我的服务
desc: 这是一段中文描述
对应原来的:
properties复制server.port=8080
custom.name=\u6211\u7684\u670D\u52A1
custom.desc=\u8FD9\u662F\u4E00\u6BB5\u4E2D\u6587\u63CF\u8FF0
看出来了没?properties 里如果没有开 IDEA 的 Transparent 选项,中文会被转成 \uXXXX,阅读和维护都很不友好。YAML 里直接写中文,清晰直观。Spring Boot 对配置文件的查找顺序里,application.yml 的优先级是高于 application.properties 的(4.0 及以后版本有调整,但 YAML 一直是第一优先),所以在大多数项目里你可以直接删掉 properties 切换成 YAML。
有人说 YAML 缩进容易写错,容易踩格式坑。这确实是它的缺点,但对比配置乱码带来的调试成本,我宁可多检查两遍缩进。而且现代 IDE 对 YAML 的支持已经很完善,格式错误一般会即时标红。
如果你处于项目初期或者配置文件中中文特别多,我的建议是直接选 YAML 作为主配置格式,别再纠结要不要用 properties。
4.2 用 ResourceBundle 实现自定义 properties 读取
还有一种常见的场景:你写了一个工具类,要读取一个 properties 文件里的业务配置,比如敏感词列表、错误码映射表,这时候不走 Spring 的 @Value 注入,而是手动加载文件。这种情况下,直接用 Properties.load(InputStream) 十有八九会踩中 ISO-8859-1 的坑。
正确做法是用我上面说的 InputStreamReader 包装输入流,指定 UTF-8:
java复制public class PropertiesUtils {
public static Properties loadUtf8(String classpathFile) {
Properties props = new Properties();
try (InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream(classpathFile);
InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
props.load(reader);
} catch (IOException e) {
throw new RuntimeException("加载配置文件失败: " + classpathFile, e);
}
return props;
}
}
这个方法有几个细节值得注意:
getResourceAsStream返回的流不能是 null,否则InputStreamReader会报空指针。建议在这里加一个判空。Properties.load(Reader)和Properties.load(InputStream)是两种完全不同的加载逻辑。前者按你给定的 Reader 编码解码,后者强制按 ISO-8859-1。一定要用前者。StandardCharsets.UTF_8是 Java 7+ 的标准写法,不要再用"UTF-8"字符串,避免拼错。
5. 补充实战:消息资源文件(i18n)的中文乱码处理
前面讲的都是配置文件,现在聊聊另一种更容易让开发者懵圈的场景:messages.properties 国际化的中文乱码。这个场景和配置文件乱码的症状很像,但处理方式完全不同,因为消息文件不是通过 Spring 的 Environment 机制加载的,而是通过 ResourceBundleMessageSource。
5.1 ResourceBundleMessageSource 的编码设置
如果你在 messages.properties 里写了中文提示语,页面上显示的是 ??? 或者 \uXXXX 原样输出,大概率是 ResourceBundleMessageSource 没有设置 defaultEncoding。
默认情况下,ResourceBundleMessageSource 使用 MessageSource 的默认编码,在大多数环境里就是 ISO-8859-1。这就意味着 messages.properties 里的中文如果不转义,就没法被正确解码。
解决方式是在配置类里显式指定:
java复制@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
// 关键:指定 UTF-8 编码,覆盖默认的 ISO-8859-1
messageSource.setDefaultEncoding("UTF-8");
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
}
这里有个细节:setDefaultEncoding("UTF-8") 设置成功后,messages_zh_CN.properties 里的中文就能被正确读取了。如果设置了 fallbackToSystemLocale(false),资源文件的合并逻辑会更可控,避免因为系统 locale 不同而加载到不期望的资源文件。
5.2 常见的 i18n 中文乱码症状对比
我整理了一个对照表,方便你快速定位自己遇到的是哪种情况:
| 症状 | 可能原因 | 推荐解法 |
|---|---|---|
页面上显示 ??? |
ResourceBundleMessageSource 默认编码导致 | 设置 setDefaultEncoding("UTF-8") |
页面上显示 \uXXXX 原始转义 |
properties 文件里已经存了转义,但被二次转义 | 检查 IDE 的 Transparent native-to-ascii 设置 |
| 本地正常,服务器上的提示变乱码 | 构建过程或运行时 locale 不一致 | 检查 Maven/Gradle 编码配置、设置 fallbackToSystemLocale(false) |
| 某些消息正常,某些乱码 | 部分文件编码不统一 | 统一保存为 UTF-8,并在 IDE 里打开文件确认右下角编码 |
这个表里的前三种我都实际遇到过,印象最深的是某次同事只在本地改了一个 messages_zh_CN.properties,IDEA 默认把他那段中文写成了 GBK,他提交后我这边一拉代码,文件内容是乱的。当时排查了半天,最后发现是 IDEA 单文件编码设置被改过。所以后来我特别强调:统一编码这种基础规范,一定要写在项目 README 里,最好在 CI 构建里加一步编码检查。
6. 终极排查流程:从“乱码形态”倒推根因
遇到乱码问题,最高效的方式不是把网上的方案挨个试,而是根据具体形态快速定位。这里我总结一个排查流程,帮你省掉反复尝试的折腾。
6.1 看乱码的“长相”,判断大致原因
乱码的形态其实透露了非常多的信息:
锟斤拷型乱码:字符被 GBK/GB2312 误解码后产生的“占位符”效果,大量出现在 UTF-8 文件被当作 GBK 读取时。看到这种乱码,重点检查 IDE 文件编码和构建工具编码。????型乱码:字符在某个环境里无法被映射到目标字符集,比如中文被写到只支持 Latin-1 的数据库列,或者 console 输出流编码不支持中文。看到这种乱码,重点检查输出的那一道链路。\u5B89\u88C5型乱码:properties 文件里的\uXXXX被当作普通文本展示,没有经过Properties解析。看到这种,说明文件被以纯文本方式读取,比如你用FileReader去读 properties,而不是用Properties类。- 一堆奇奇怪怪的符号,比如
鸿:这是典型的 UTF-8 字节被当成了 Latin-1 显示的结果,说明文件保存是 UTF-8,但读取的时候按 Latin-1 来。Spring Boot 2.4+ 不设置编码时最常见。
6.2 完整排查路线图
按这个顺序排查,基本能覆盖 90% 的场景:
第一步:用 IDEA 打开出问题的文件,看右下角显示的编码。如果显示的不是 UTF-8,先转换到 UTF-8。
第二步:在 IDEA 的 File Encodings 里确认三项设置都是 UTF-8,确认 Transparent native-to-ascii conversion 的状态和你的项目约定一致。
第三步:检查 pom.xml 或 build.gradle 里的编码配置,确认构建工具不会二次篡改文件内容。
第四步:如果你用的是 Spring Boot 2.4+,在主配置文件里加上 spring.config.properties.encoding=UTF-8。如果是自定义加载文件,改用自定义 PropertySourceFactory 或手动指定 Reader 编码。
第五步:如果以上都检查完还没解决,写一个最简单的测试代码直接读取文件并输出字节码,把问题从 Spring Boot 层隔离掉:
java复制public class EncodingCheck {
public static void main(String[] args) throws IOException {
Path path = Paths.get("src/main/resources/application.properties");
byte[] bytes = Files.readAllBytes(path);
System.out.println("文件字节数: " + bytes.length);
// 把原始字节按 UTF-8 解码
System.out.println("UTF-8 解码: " + new String(bytes, StandardCharsets.UTF_8));
// 再按默认编码解码,对比差异
System.out.println("默认编码解码: " + new String(bytes, Charset.defaultCharset()));
}
}
这个测试类的输出能直接告诉你:文件里的字节到底是 UTF-8 还是别的编码。知道字节的真实编码,你就能确定问题出在加载还是输出环节。
6.3 一个我踩过的真实坑:只改代码没改文件
几年前接手一个老项目,配置文件里的中文在页面上全是问号。我按照正常流程,在 pom 里加了 UTF-8 相关的配置,又设置了 ResourceBundleMessageSource,结果还是不行。后来发现那个 properties 文件是 GBK 保存的,IDEA 里显示正常是因为 IDE 自动识别了 GBK,但 Spring Boot 按 UTF-8 读就是乱码。
最终处理方式是:在 IDEA 里用 GBK 打开文件,全选复制,然后把文件编码切换成 UTF-8,粘贴内容覆盖,确认保存。这个操作把文件字节从 GBK 重新编码成了 UTF-8,问题才彻底解决。
这也是我为什么反复强调“文件本身保存编码”是第一层原因。很多时候配置都对,只是文件字节本身就错了。
7. 实操总结:我的默认方案和避坑清单
讲到这里,核心内容已经全部覆盖了。按照我自己的习惯,在项目里遇到 properties 中文乱码,会按照下面的金字塔方案去处理。
第一选择:能用 YAML 就尽量用 YAML,从源头绕开 properties 的编码问题。新项目我基本都是 application.yml,没有乱码烦恼。
第二选择:必须用 properties 时,把文件保存为 UTF-8,并在 Spring Boot 2.4+ 里设置 spring.config.properties.encoding=UTF-8。同时确认 @PropertySource 的自定义文件用了自己的 Utf8PropertySourceFactory。
第三选择:i18n 消息文件,在 ResourceBundleMessageSource 里设置 setDefaultEncoding("UTF-8")。
这三条路走下来,配置层面的乱码基本能解决。最后再附一份我自己的避坑清单,都是实际踩出来的教训:
- 在项目的
.editorconfig里显式声明charset = utf-8,别只依靠口头约定。 - 修改 IDEA 编码设置后,需要重新构建项目(Build -> Rebuild Project),有些旧编译产物会缓存乱掉的 class。
- properties 文件如果在 Linux 下创建,一定要确认没有 BOM 头。带 BOM 的 UTF-8 文件在部分环境下解析时会把 BOM 当字符读进去,导致第一个 key 前面多一个不可见字符。我遇到过 Spring Boot 启动时报
Cannot determine embedded database driver class for database type NONE,排查一圈才发现是application.properties第一个配置项被 BOM 污染了。 - 控制台输出乱码和文件读取乱码是两回事。如果你文件读对了但控制台还是乱码,检查 IDE 的
Console编码,Windows 下把-Dfile.encoding=UTF-8加到运行参数里。 - 排查问题的时候,别急着改代码。先用最笨的方法——一个
main方法直接读文件打印字节,确认文件本身编码正确,再往上层查。这个习惯能帮你少走很多弯路。 - 团队协作项目里,如果你发现某个配置文件之前一直是
\uXXXX转义,突然某次提交变成了明文中文字符,大概率是有同事改动 IDEA 设置导致文件重写了。这种情况要及时统一,避免不同文件混用两种风格。
说实话,properties 中文乱码这个问题不会因为 Spring Boot 版本升级而消失,因为根子出在 Java 老牌类库的历史设计上。但只要掌握了“文件编码、加载编码、输出编码”这三条线,遇到任何乱码场景都能快速定位,不需要死记八种解决方案。希望这篇文章能帮你省下几个小时排查的时间。
