
本文系统梳理 freemarker 模板引擎在 spring boot 项目中因编码不一致导致的乱码、模板加载失败、空值报错等典型问题,覆盖文件编码、jvm 层、web 容器、http 协议、模板配置及控制台输出六大环节,并提供可落地的 utf-8 全链路统一配置方案。
本文系统梳理 freemarker 模板引擎在 spring boot 项目中因编码不一致导致的乱码、模板加载失败、空值报错等典型问题,覆盖文件编码、jvm 层、web 容器、http 协议、模板配置及控制台输出六大环节,并提供可落地的 utf-8 全链路统一配置方案。
FreeMarker 作为经典 Java 模板引擎,其乱码问题长期困扰开发者——表面看是“中文显示为 ? 或 ”,实则往往是多层编码未对齐引发的连锁反应。尤其当开发者明确设置 cfg.setDefaultEncoding("UTF-8") 后反而出现 j 这类退化现象(如 Czech 字符 jůůů 显示异常),恰恰说明问题不在模板本身,而在编码链的某个环节发生了隐式转码或终端解码失配。
? 根本原因:编码链断裂的六个关键节点
| 环节 | 默认行为 | 风险点 | 验证方式 |
|---|---|---|---|
| 1. 模板文件物理编码 | IDE 保存时指定(IntelliJ 默认 UTF-8) | 文件实际以 GBK/Windows-1250 保存,但声明为 UTF-8 | 用 VS Code 或 file -i template.ftl 查看真实编码 |
2. JVM file.encoding |
依赖系统 locale(如 en_US → ISO-8859-1;zh_CN → GBK) |
影响 ClassPathResource.getFile() 读取字节流的解码 |
System.out.println(Charset.defaultCharset()); |
3. FreeMarker Configuration |
Configuration.VERSION_2_3_32 默认 ISO-8859-1
|
setDefaultEncoding("UTF-8") 仅影响模板解析,不改变文件读取逻辑 |
调试 cfg.getTemplate(...) 前后 template.getSourceString() 是否含乱码 |
| 4. Tomcat / Web 容器 |
URIEncoding 默认 ISO-8859-1(GET 参数)、Connector 无默认字符集 |
URL 路径、查询参数被错误解码 | 访问 /test?name=张三,Controller 打印 request.getParameter("name")
|
| 5. HTTP 响应头 |
Content-Type: text/html(无 charset) |
浏览器按历史/启发式规则猜测编码,易失败 | Chrome DevTools → Network → Response Headers → content-type
|
| 6. 终端/IDE 控制台 | Windows CMD 默认 GBK,PowerShell 默认 UTF-16
|
System.out.println() 输出乱码,误导判断为业务逻辑问题 |
在 PowerShell 执行 $OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
|
⚠️ 关键洞察:你观察到“不设
defaultEncoding反而能正确显示jůůů”,正是因为 JVM 当前file.encoding=windows-1250,恰好与模板文件真实编码一致——此时 FreeMarker 用ISO-8859-1解析字节流虽不严谨,但windows-1250和ISO-8859-1在 ASCII 区兼容,且部分扩展字符碰巧映射成功,属于偶然正确,不可依赖。
✅ 终极解决方案:六步强制 UTF-8 全链路对齐
步骤 1:统一源码与模板文件编码(IDE 层)
- IntelliJ:
File → Settings → Editor → File Encodings- Global Encoding:
UTF-8 - Project Encoding:
UTF-8 - Default encoding for properties files:
UTF-8
- Global Encoding:
-
验证:右下角状态栏确认
UTF-8,另存为时勾选 “Write BOM”(非必需,但可避免某些工具误判)。
步骤 2:强制 JVM 使用 UTF-8(启动层)
在 gradle.properties 或运行脚本中添加:
# gradle.properties org.gradle.jvmargs=-Dfile.encoding=UTF-8
或启动 JAR 时显式指定:
java -Dfile.encoding=UTF-8 -jar your-app.jar
✅ 黄金法则:
-Dfile.encoding=UTF-8是解决日志、资源读取、反射类名等所有 JVM 字符串解码问题的基石,优先级高于任何应用层配置。
步骤 3:FreeMarker 配置精准对齐(代码层)
@Configuration
public class FreemarkerConfig {
@Bean
public Configuration freemarkerConfiguration() throws IOException {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
// ✅ 关键:使用 ClassPathTemplateLoader(自动处理 classpath 编码)
ClassPathTemplateLoader templateLoader = new ClassPathTemplateLoader();
templateLoader.setBasePath("templates");
templateLoader.setDefaultEncoding("UTF-8"); // ← 指定模板文件读取编码
cfg.setTemplateLoader(templateLoader);
// ✅ 设置模板解析与输出编码(渲染阶段)
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputEncoding("UTF-8");
cfg.setLocale(Locale.getDefault()); // 如需本地化,建议显式设为 Locale.CHINA
// ✅ 开启 classic_compatible 避免空值报错(开发期友好)
cfg.setClassicCompatible(true);
return cfg;
}
}
? 注意:
setDirectoryForTemplateLoading(new ClassPathResource(...).getFile())是危险操作——它绕过 ClassLoader 的编码感知,直接用File读取,将完全依赖 JVMfile.encoding。务必改用ClassPathTemplateLoader。
步骤 4:Spring Boot 全局编码加固(框架层)
application.yml 中配置:
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true # ? 强制所有请求/响应使用 UTF-8,覆盖客户端声明
spring:
freemarker:
template-loader-path: classpath:/templates/
suffix: .ftl
content-type: text/html;charset=UTF-8 # 响应头显式声明
charset: UTF-8
cache: false # 开发期关闭缓存,确保修改实时生效
步骤 5:Tomcat 容器编码(部署层)
若使用内嵌 Tomcat,通过 WebServerFactoryCustomizer 配置:
@Bean
public WebServerFactoryCustomizer<tomcatservletwebserverfactory> containerCustomizer() {
return factory -> factory.addAdditionalTomcatConnectors(
createStandardConnector()
);
}
private Connector createStandardConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setPort(8080);
connector.setURIEncoding("UTF-8"); // ← 关键:解码 URL 路径和参数
return connector;
}</tomcatservletwebserverfactory>
步骤 6:终端/IDE 控制台修复(调试层)
-
IntelliJ:
Help → Edit Custom VM Options→ 添加-Dfile.encoding=UTF-8 -
PowerShell(临时):
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
-
Linux/macOS Terminal:
export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8
? 验证清单(逐项检查)
- 模板文件
template.ftl用file -i template.ftl确认输出charset=utf-8 - 启动日志中
Default charset: utf-8 -
curl -I http://localhost:8080/test返回头含Content-Type: text/html;charset=UTF-8 - 浏览器访问页面,DevTools → Elements → 查看
<meta charset="UTF-8"> - Controller 接收含中文的 POST 请求,
@RequestBody正确解析 -
System.out.println("jůůů")在控制台正常显示
⚠️ 补充注意事项
-
避免
classic_compatible=true用于生产环境:它会静默忽略空值(如hots未传时仍渲染),掩盖数据缺失问题。推荐改为:<!-- fallback --> #if>
-
数据库连接字符串必须显式声明编码(如 MySQL):
jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
-
静态资源(CSS/JS)也需 UTF-8:在
application.yml中配置spring.web.resources.charset=UTF-8(Spring Boot 2.6+)。
FreeMarker 乱码不是模板引擎的缺陷,而是分布式系统中“编码契约”未被严格履行的必然结果。坚持 “一处声明,处处生效;一环校验,全链贯通” 的原则,即可彻底告别 ??、`和j`,让多语言支持成为项目基建的可靠底座。











