日志乱码一旦出现,排查思路不应该是逐个猜测编码参数,而是先把日志从输出到查看的完整链路拆开。Java 应用在内存中使用 Unicode 字符,写入文件或控制台时需要将字符按某种字符集编码成字节,随后查看工具再用某种字符集解码回字符。只要写入和读取使用的字符集不一致,中文、特殊符号就会显示为乱码。

一、先定位乱码发生在哪个编码环节
日志链路可以分成三段:应用内存字符串、日志框架输出字节、终端或日志平台读取字节。第一段中,Java 字符串本身没有乱码的概念,它是内存中的 char 序列;第二段如果使用平台默认编码输出,就很容易被环境带偏。比如 Windows 上 JDK 17 及以前,file.encoding 通常继承系统 ANSI 代码页,中文环境常为 GBK;Linux 容器如果 locale 配置不完整,可能退回到 POSIX 或 ANSI_X3.4-1968,连中文都无法正确表示。
第三段更隐蔽。即使文件已经以 UTF-8 写入,Windows 命令行窗口如果用 GBK 代码页 chcp 936 查看,也会把 UTF-8 字节误解成 GBK,出现类似 鍙d汉 的乱码。反过来,文件是 GBK,终端是 UTF-8,也会出现乱码。所以解决乱码不是改一个参数,而是把三个环节的字符集全部对齐到 UTF-8。
# Linux 或 macOS 查看 JVM 默认编码 java -XshowSettings:properties -version 2>&1 | grep encoding # Windows PowerShell 查看 JVM 默认编码 java -XshowSettings:properties -version 2>&1 | Select-String encoding
二、统一编码:从 JVM 到日志框架的显式配置
应用侧不要依赖操作系统默认编码。最直接的方式是在启动脚本中显式设置 JVM 参数。-Dfile.encoding=UTF-8 会影响 Java 读写文件、字符串与字节互转时的默认字符集,而 -Dstdout.encoding=UTF-8 和 -Dstderr.encoding=UTF-8 则控制标准输出、标准错误流使用 UTF-8,这两项对容器环境尤其重要,因为容器日志通常直接采集 stdout。
# 设置 JAVA_TOOL_OPTIONS,Java 进程会自动继承 export JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8" # 或直接启动 java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -jar app.jar
仅设置 JVM 参数还不够,日志框架可能有自己的编码配置。以 Logback 为例,RollingFileAppender 中的 encoder 需要显式添加 <charset> 配置,否则也可能继承 JVM 默认编码。虽然 JVM 已经统一成 UTF-8,但多写一句 charset 配置可以避免未来部署环境变化后再次踩坑。
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
如果项目使用 Log4j2,同样需要在 Console 和 RollingFile 的 <PatternLayout> 中加上 charset="UTF-8"。很多团队只给文件 appender 配置编码,却忽略了控制台 appender,导致部署到容器后本地日志文件正常、kubectl logs 仍然乱码。
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c{1} - %m%n" charset="UTF-8"/>
</Console>
<RollingFile name="File" fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd}.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %c{1} - %m%n" charset="UTF-8"/>
<Policies>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
查看侧也要对齐。Windows 终端可以执行 chcp 65001 切换到 UTF-8 代码页,Linux 则通过 locale 命令确认 LANG 是否为 zh_CN.UTF-8 或 C.UTF-8。如果是容器环境,建议在镜像内部直接设置语言环境,而不是依赖宿主机。
三、结构化日志:从源头降低终端解码依赖
传统文本日志即使编码全部正确,在纯文本终端中仍然可能因为字体、换行、堆栈缩进等问题变得不易阅读。结构化日志每行一个 JSON 对象,字段清晰,日志平台可以按字段解析、索引和检索。更重要的是,结构化输出通常使用 UTF-8 写入,中文不再依赖终端字体和代码页的表现,只要采集端和展示端统一使用 UTF-8,中文就能稳定显示。
在 Logback 中实现结构化日志,常用 logstash-logback-encoder。引入依赖后,只需把 appender 的 encoder 替换为 LogstashEncoder,它会把日志事件、MDC 字段、异常堆栈统一序列化为 JSON。
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>8.0</version>
</dependency>
配置文件中的 appender 可以这样写。这里仍然建议显式添加 <charset>UTF-8</charset>,避免在极端环境中被默认编码覆盖。
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app-json.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app-json.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<charset>UTF-8</charset>
<customFields>{"application":"order-service"}</customFields>
</encoder>
</appender>
如果暂时不能引入第三方依赖,也可以通过 MDC 组织业务字段,再用固定格式输出。注意这种方式只适合临时过渡,真正进入日志平台时,JSON 结构化依然是更稳定的方案。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void createOrder(String orderId) {
MDC.put("orderId", orderId);
MDC.put("module", "order");
log.info("订单创建成功");
MDC.clear();
}
}
四、验证落盘字节与排查清单
编码配置完成后,最好用工具确认文件里的字节真的是 UTF-8,而不是只靠肉眼判断。Linux 或 macOS 下可以先用 file -i 查看文件类型和 charset,再用 xxd 查看中文字节的十六进制值。例如“你好”对应的 UTF-8 字节是 E4 BD A0 E5 A5 BD,如果看到的完全不同,说明仍有环节编码不一致。
# 查看文件编码声明 file -i logs/app.log # 查看前 256 字节,确认中文是否为 UTF-8 序列 head -c 256 logs/app.log | xxd
Windows 环境可以使用 PowerShell 的 Format-Hex 查看文件字节。注意示例中的路径使用反斜杠,这是 Windows 的路径分隔符,执行时请按实际项目路径调整。
# Windows PowerShell 查看文件字节 Format-Hex -Path logs\app.log -Count 256
还有一个很容易被忽略的环节是日志采集器。Filebeat、Promtail 这类工具在读取文件时也会使用编码设置,如果采集端没有显式声明 UTF-8,可能会按系统默认编码读取,导致日志进入平台后仍然乱码。容器场景则要保证 stdout 输出编码正确,建议在镜像内同时设置语言环境变量和 JVM 参数。
FROM eclipse-temurin:17-jre ENV LANG=C.UTF-8 ENV JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8" COPY app.jar /app/app.jar WORKDIR /app ENTRYPOINT ["java","-jar","app.jar"]
最后可以按下面清单逐项检查。只要链条上任意一个节点没有统一到 UTF-8,乱码都可能再次出现。
- 确认 JVM 默认编码:
file.encoding是否为 UTF-8。 - 启动脚本是否显式设置
-Dfile.encoding=UTF-8。 - 日志框架的 Console 和 File appender 是否都配置了 UTF-8。
- 查看日志的工具或终端是否使用 UTF-8,而不是系统默认代码页。
- 容器内是否设置
LANG=C.UTF-8或LC_ALL=C.UTF-8。 - 如果日志要被采集,Filebeat、Promtail 等采集端是否声明 UTF-8。
日志乱码表面上是显示问题,深层次是编码链路没有形成约定。把 JVM、日志框架、终端和采集端全部显式统一到 UTF-8,再配合结构化日志降低纯文本依赖,后续就很少再被中文乱码打扰。