ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Tomcat在IDEA控制台中文乱码:用代码强制UTF-8输出绕开配置

Tomcat在IDEA控制台中文乱码:用代码强制UTF-8输出绕开配置 要说最近做Java开发的同学最常被恶心到的一件事我猜“Tomcat在IDEA控制台输出中文乱码”一定排得上号。你已经按教程装好了TomcatIDEA的Run Configuration也指向了本地目录Servlet一启动System.out.println(你好)出来的却是一排“浣犲ソ”或者“Һ”。然后你开始满网搜方案改IDEA编码、改Tomcat的logging.properties、往catalina.bat塞-Dfile.encodingUTF-8重启三遍有时好了有时换个项目又乱了。这篇东西就是来解决这个问题的。我给的方案说实话有点“野”——不跟你死磕那些五花八门的配置文件直接在你自己的应用出口把编码钉死。它适合所有被这个乱码折磨过的人不管你是刚接触IDEA的新手还是维护老项目的熟手照着做都能收工。下面先把为什么乱码这件事讲透再给你一套真正能“绕开配置”的终极解法。1. 改了配置还是乱码先搞懂System.out和Tomcat日志是两条路1.1 从一次常见的“改配置失败”现场说起多数人遇到乱码后的第一反应是去改Tomcat的conf/logging.properties。因为网上搜“Tomcat中文乱码”十条有九条会让你把下面这行改掉java.util.logging.ConsoleHandler.encoding UTF-8改完重启你发现Tomcat自己打印的启动日志确实正常了比如那句“Server startup in 120 ms”已经不带乱码。但只要你自己的代码里写了System.out.println(中文)控制台依旧乱得亲妈都不认识。这是很多人在这一步浪费时间的原因他们以为Tomcat控制台里看到的所有文字都归logging.properties管实际上完全不是。Tomcat自身的日志走的是java.util.loggingJULI体系而你代码里的System.out打印内容走的是标准输出流。两套东西两个通道一条配置只能管住其中一条。1.2 为什么logging.properties管不住System.outTomcat在启动时会把System.out和System.err“挂”到JULI的两个特殊Handler上叫SystemOutHandler和SystemErrHandler目的是让应用里用System.out.println打出来的内容也能被记录到catalina.out文件里。但请注意它只是“旁路记录”你的内容本身仍然会写到进程的标准输出管道里。IDEA控制台显示你的输出读取的是这条标准输出管道。而logging.properties里的ConsoleHandler.encoding修改的是JULI内部日志处理器打印到控制台时的编码跟你的System.out字节流压根不在一条路上。所以你会发现网上常见的“改Tomcat日志编码”方案只能解决Tomcat自己的日志乱码解决不了你代码里的输出乱码。1.3 catalina.bat和IDEA之间的“配置失联”还有人会把希望寄托在改catalina.bat或者setenv.bat里加JAVA_OPTS-Dfile.encodingUTF-8。这个方法在纯命令行启动Tomcat时有一定概率能生效但在IDEA里经常失灵。原因很简单IDEA启动Tomcat时它自己是负责拼JVM参数的一方。你在IDEA的Run Configuration里填的VM options会被IDE传进去但catalina.bat里的变量能不能被读到取决于IDEA是以哪种方式启动的。在IDEA内嵌模式下它是直接调用Tomcat类加载器的根本不会去解析catalina.bat你在里面加一万行参数也是白搭。更别提不同版本身IDEA行为还不完全一样这就让“改catalina.bat”成了一场赌博。2. 乱码链路拆解一个中文单词从代码到控制台要过三个关口2.1 关口一JDK把字符串变成字节你代码里的“你好”在内存中是一个Unicode字符串JDK在把它写到标准输出流时必须按某种字符集编码成字节。这个编码在JDK 18之前默认取决于启动JVM时的file.encodingWindows中文版系统通常就是GBK。JDK 18开始默认改成了UTF-8但很多项目的Tomcat版本、IDEA版本、JDK版本是混搭的默认值并不统一。这就埋下了第一个变量你的“你好”到底是按UTF-8编码写出去的还是按GBK编码写出去的如果没人显式指定结果就是“看运气”。2.2 关口二IDEA控制台把字节读回字符字节流到达IDEA控制台之后控制台还得按某种编码把它们解码成字符显示。IDEA也不是一个纯终端它是个Swing组件它对子进程输出流的读取编码有自己的逻辑。老版本IDEA在Windows上经常按系统区域编码GBK读取新版本IDEA默认按UTF-8读取。如果发送端按UTF-8写字节、接收端按GBK解码你看到的就是“浣犲ソ”这种经典乱码。反过来发送端GBK、接收端UTF-8你看到的就是“Һ”这种带替换符的乱码。2.3 关口三字体渲染这一关经常被人忽略。如果前面的编码都对了字节也解成正确的Unicode字符了但控制台用的字体里没有这个字形显示出来还是“口”或者“”。IDEA控制台默认的等宽字体一般是包含中文的所以这一关在IDEA里踩到的人不多。但如果你把输出重定向到Windows自带的老式cmd窗口或者用某些精简字体就很有可能出现“每个中文字都变成了方框”的情况。如果你遇到的是“????”而不是“浣犲ソ”说明问题多半出在字体或终端这边而不在编码链路。2.4 三行代码快速定位乱码属于哪一段不用急着猜直接在代码里放三行探针System.out.println(探针中文Abc123); System.out.println(Charset.defaultCharset()); System.out.println(System.getProperty(file.encoding));在IDEA控制台观察输出形态基本可以判断是哪个关口出问题控制台显示说明“探针中文Abc123”完全正常整个链路没问题中文变成“浣犲ソ”这类汉字乱码UTF-8字节被GBK解码发送UTF-8、接收GBK中文变成“Һ”这类问号加替换符GBK字节被UTF-8解码发送GBK、接收UTF-8ASCII正常中文全是“”大概率是字体缺字形跟编码无关ASCII正常中文变成“䏿–‡”这类字母特殊符号UTF-8字节被ISO-8859-1/Latin1逐字节解码看到哪一类就知道该修哪一侧了。绝大多数IDEATomcat乱码属于第一类或第二类解决办法就是我下面要说的把发送端统一到UTF-8并让接收端也稳定按UTF-8读。3. 终极方案在代码出口把System.out钉死成UTF-83.1 思路让发送端不再跟JDK默认编码“猜谜”网上大部分方案都在试图“修配置”而配置的分散性决定了它的脆弱。这个项目改了IDEA编码下个项目又要改Tomcat的logging.properties同事那里能用的方案换台机器又不灵了。我的做法是直接在代码里重新包装System.out强制让它以UTF-8编码输出。这样做有什么好处它不改file.encoding不影响项目里其它依赖默认编码的API行为只影响标准输出流的字节编码。换句话说发送端被你亲手焊死了不会再跟着系统环境来回变。这也解释了为什么我说“绕开配置”——它绕开了Tomcat的conf目录、绕开了IDEA安装目录、绕开了catalina.bat只在你自己的应用边界内做手脚。3.2 通用工具类ForceUtf8Console下面这个类就是整个方案的核心。它做两件事重新打开进程原始的标准输出和标准错误句柄然后包成UTF-8编码的PrintStream最后通过System.setOut和System.setErr替换掉原来的流。import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintStream; import java.io.UnsupportedEncodingException; public final class ForceUtf8Console { private ForceUtf8Console() { } public static void apply() { try { PrintStream utf8Out new PrintStream( new FileOutputStream(FileDescriptor.out), true, UTF-8 ); PrintStream utf8Err new PrintStream( new FileOutputStream(FileDescriptor.err), true, UTF-8 ); System.setOut(utf8Out); System.setErr(utf8Err); } catch (UnsupportedEncodingException e) { // UTF-8 是所有JDK实现必须支持的字符集正常不会到这里 throw new IllegalStateException(UTF-8 is not supported, e); } } }几个关键点说明一下。FileDescriptor.out拿到的是JVM进程最原始的标准输出句柄而不是已经被别人替换掉的System.out。这样即使项目里其它代码在之前偷偷改过System.setOut也不影响你重新拿到真正的输出管道。PrintStream构造器里第二个参数true表示autoFlush即每次调用println都自动flush缓冲区。如果漏了这个参数输出内容可能滞留在缓冲区里控制台刷新不及时看起来像“丢了日志”。代码用UTF-8字符串而不是StandardCharsets.UTF_8是为了兼容JDK 8。StandardCharsets.UTF_8在JDK 7就有了但PrintStream支持Charset参数的重载是JDK 10才加的老项目如果还在JDK 8上跑用字符串形式更稳。3.3 在普通Servlet项目、Spring Boot项目里怎么调用调用位置很有讲究。最理想的是在应用最早期执行越早越好。如果你的项目有main方法直接在main方法第一行调用public static void main(String[] args) { ForceUtf8Console.apply(); // 后续启动逻辑 }如果是传统Servlet项目打成war丢Tomcat里的那种可以写一个ServletContextListenerimport javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; WebListener public class ConsoleEncodingListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { ForceUtf8Console.apply(); } }如果项目用web.xml而不是注解就在web.xml里注册listener listener-classcom.example.ConsoleEncodingListener/listener-class /listener放在Listener里调用有一个额外好处它不依赖你记得在每个入口调一遍。项目里不管从哪个地方触发启动监听器都会执行。SparkFly还是有Framework的只要是嵌入了Servlet容器的项目这个思路都适用contextInitialized是最通用的钩子。3.4 配套的VM options只加一行不碰Tomcat配置代码出口钉死UTF-8之后还剩一个环节需要配合IDEA控制台这边最好也用UTF-8来读。在新版IDEA里控制台读取子进程输出默认用的就是UTF-8所以很多人走到这一步就已经完全正常了。如果探针显示还有问题在Run Configuration的VM options里加一行-Dfile.encodingUTF-8这一行同时把JDK的file.encoding钉成UTF-8。JDK 17以上的版本还可以加得更精准-Dstdout.encodingUTF-8 -Dstderr.encodingUTF-8 -Dfile.encodingUTF-8为什么我建议你加VM options而不是去改IDEA的全局配置因为VM options是跟当前Run Configuration绑定的它只影响当前这个Tomcat进程不影响IDEA自身也不影响其它项目的文件编码。就算项目换机器、换IDEA版本只要这个运行配置还在参数就会带过去。到这里你已经不需要动Tomcat的conf/logging.properties也不需要动catalina.bat更不需要去IDEA安装目录里改什么idea.properties。一个代码类加一个VM参数问题结束。4. 配套收尾日志框架、IDE模板和Linux部署的顺带处理4.1 logback/log4j2的输出编码要一起对齐ForceUtf8Console只处理System.out和System.err这两个标准流。如果你的项目已经接了logback或者log4j2控制台日志走的是ConsoleAppender那就要单独检查日志框架那边有没有自己的编码设置。以logback为例logback.xml里可以显式指定控制台输出编码appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder charsetUTF-8/charset pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender注意charsetUTF-8/charset这一行。logback默认的ConsoleAppender在不同平台上会使用不同的默认编码Windows上很可能按GBK输出导致你明明解决了System.out老项目的logger.info还是乱码。log4j2同理在PatternLayout里也有字符集设置。建议所有日志输出的字符集和标准流统一成UTF-8一次对齐后面不用反复折腾。4.2 新建项目的“一次配置永久生效”做法很多人都有这种感受这个项目的乱码是治好了但下个月新建一个项目同样的坑又来了。我的做法是两件事。第一ForceUtf8Console这个工具类直接放在自己常用的基础工程或者内部公共库里新项目拉过来就能用。第二在IDEA里把Tomcat Server运行配置的模板改好。具体操作是打开“Edit Configurations”找到左侧的“Tomcat Server”下的模板不同版本IDEA叫法略有差异但都有Template/Default的概念在VM options里填入上面那一串参数。这样以后新建的Tomcat运行配置会自动带上这个参数不用每次手敲。只要这两件事做了你的编译环境和运行输出的编码问题基本一次性解决后面新建一百个项目都不会再踩。4.3 部署到Linux后乱码反而是好事还有一种情况值得说。有人在Windows本地改完这套方案代码推上Linux服务器后日志反而变正常了。这不是巧合而是因为Linux上的file.encoding绝大多数是UTF-8你的输出本来就是UTF-8两边自然吻合。反过来如果你在Windows上没做任何处理靠系统默认的GBK输出代码部署到Linux之后catalina.out里可能照常显示正常中文——因为GBK字节在UTF-8环境下被解释成乱码的概率很高但也可能因为某些特殊字节序列“碰巧”能读通。这种“碰巧”是最坑的因为它在本地正常、线上正常一到别人机器上突然乱。所以从一开始就把输出统一成UTF-8是最省心的长期策略。再提醒一点排查线上乱码时先确认是不是日志文件本身编码的问题。比如用tail或less查看Linux上的catalina.out时中文乱码但用sz下载到Windows上反而正常说明文件是UTF-8只是查看工具的默认编码不对。这种情况跟JDK配置没关系。4.4 别让重定向的调用顺序搞出新的坑ForceUtf8Console.apply()是直接替换System.out的所以有一个隐患如果项目里还有其它代码也调用了System.setOut那调用顺序就决定了最后谁生效。举个真实例子。有次我在一个老项目里加了这个工具类放在一个自定义的Filter里结果启动后日志倒是正常了但System.out颜色和格式乱了。排查半天发现项目里的某个监控组件在Filter之后又调了一次System.setOut把流换成了它自己的包装流顺带把这个组件内部的旧编码又带回来了。解决办法很简单把ForceUtf8Console.apply()的调用尽量前置比如放在main方法第一行或者ServletContextListener里并且尽量排在web.xml的listener最前面。一个进程里最后执行的那次setOut才真正决定输出编码所以你要保证自己是最后一个设置者。5. 从乱码形态反推故障环节一张排查表够用十年5.1 乱码特征、原因、对策对照表这几年的排查经验我把常见乱码归纳成这张表。遇到问题直接对号入座比自己瞎试快得多。观察到的现象根因方向对策“浣犲ソ”“鏄庢棩”这类汉字乱码发送UTF-8、接收GBK发送端钉死UTF-8接收端控制台改UTF-8“Һ”这类问号和替换符发送GBK、接收UTF-8发送端改UTF-8或VM options加-Dfile.encodingUTF-8ASCII正常、中文全是“”字体缺字形或终端不识别换控制台字体不要用系统精简字体中文变成“䏿–‡”这类拉丁字符UTF-8字节被单字节编码解码把接收端显示编码改成UTF-8日志文件用记事本打开乱码终端正常文件本身是UTF-8记事本按GBK打开换查看工具或确认查看工具编码记住一个核心发送编码和接收解码必须一致天然对齐的默认值是不存在的。你要么两手都显式指定成UTF-8要么就接受它在某些机器上正常、某些机器上抽风的现实。5.2 常见但容易误判的三种“假乱码”第一种System.out和logger输出编码不一致。你只解决了其中一个另一个还在乱于是以为自己方案有误。不要怀疑用探针分别打两条内容看哪条正常哪条乱谁乱改谁。第二种Windows命令行和IDEA控制台同时开结果一个正常一个乱。这不奇怪IDEA控制台用的是Swing容器的解码逻辑cmd窗口用的是代码页两者的接收编码可以完全不同。以IDEA控制台为准别被cmd的表现带偏。第三种项目里用了new String(bytes, GBK)再打印。这种情况下无论你怎么重定向标准流打印出来前字符串内容已经错了乱码是“数据损坏”而不是“编码错位”。这类代码属于历史遗留问题除了改成StandardCharsets.UTF_8之外没有别的快速办法。5.3 最后一点经验如果你问我为什么不用那些现成的“IDEA编码方案”我的回答是它们不是完全没用而是适用范围太窄。IDEA版本一升级、Tomcat版本一变化、机器从Windows换到Mac配置就得重新演一遍。我现在的习惯是只要项目里出现一次控制台中文乱码就直接上ForceUtf8Console。它不是最“优雅”的方案也不是“零改动”的方案但它把发送端牢牢控制在自己的代码里让整个问题的排查半径缩小到“接收端是否也是UTF-8”这一件事上。最后分享一个小技巧真的想省事可以在ForceUtf8Console的类注释里写一行“不要移除控制台中文输出依赖此类的UTF-8重定向”省得后来维护项目的人看着这个类莫名其妙以为是什么废弃代码把它删掉然后乱码问题又复活一遍。
返回列表