
本文系统梳理 freemarker 模板引擎在 spring boot 环境下因编码不一致导致的乱码、模板加载失败、空值报错等典型问题,覆盖文件编码、jvm 启动参数、spring 配置、http 协议层及终端输出等 5 大关键环节,并提供可直接落地的配置代码与验证方法。
本文系统梳理 freemarker 模板引擎在 spring boot 环境下因编码不一致导致的乱码、模板加载失败、空值报错等典型问题,覆盖文件编码、jvm 启动参数、spring 配置、http 协议层及终端输出等 5 大关键环节,并提供可直接落地的配置代码与验证方法。
FreeMarker 作为轻量级、高性能的 Java 模板引擎,广泛应用于邮件生成、静态页渲染和 SEO 场景。但其默认编码机制(ISO-8859-1)与现代项目普遍采用的 UTF-8 存在天然冲突,稍有疏忽即引发「中文变 ?」、「模板读取为乱码」、「控制台日志显示 j」、「InvalidReferenceException 空值异常」等看似随机实则高度规律的问题。根本原因并非 FreeMarker 本身缺陷,而是编码在「文件存储 → JVM 加载 → 模板解析 → HTTP 响应 → 终端/浏览器渲染」全链路中任一环节断裂所致。
✅ 一、精准定位:为什么显式设 UTF-8 反而更乱?
如问题所述:模板文件是 UTF-8 编码,cfg.setDefaultEncoding("UTF-8") 后却输出 j —— 这并非 FreeMarker 解析错误,而是控制台(Terminal/PowerShell/IDE Console)未正确声明 UTF-8 输出编码。JVM 虽以 UTF-8 读取了模板字节,但 System.out.println() 将字符串写入终端时,若终端自身编码非 UTF-8(如 Windows 默认 CP1252 或 GBK),就会发生二次解码失败。
✅ 验证与修复(三步到位):
-
强制 JVM 使用 UTF-8(构建 & 运行时均生效):
# 启动命令中显式指定 java -Dfile.encoding=UTF-8 -jar your-app.jar
? 推荐全局配置:在
gradle.properties中添加org.gradle.jvmargs=-Dfile.encoding=UTF-8,或在 IDE 的 Run Configuration → VM Options 中统一设置。 -
同步终端编码(以 PowerShell 为例):
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
在执行
java -Dfile.encoding=UTF-8 ...前运行此命令,确保终端输入/输出通道均为 UTF-8。 -
验证 JVM 默认编码是否生效:
System.out.println("Default charset: " + java.nio.charset.Charset.defaultCharset()); // ✅ 正确输出:Default charset: UTF-8
✅ 二、FreeMarker 模板加载阶段的编码一致性
FreeMarker 的 Configuration 对象需明确声明三类编码,缺一不可:
| 属性 | 作用 | 推荐值 | 注意事项 |
|---|---|---|---|
setDefaultEncoding("UTF-8") |
指定模板文件读取时的字符集 | "UTF-8" |
必须与 .ftl 文件实际保存编码严格一致 |
setOutputEncoding("UTF-8") |
指定模板渲染后输出内容的编码 | "UTF-8" |
影响 Writer 写入结果(如 StringWriter) |
setTemplateUpdateDelay(0) |
开发期禁用模板缓存 | 0L |
避免修改模板后仍加载旧版本 |
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDirectoryForTemplateLoading(
new ClassPathResource("templates").getFile()
);
cfg.setDefaultEncoding("UTF-8"); // ← 关键!必须匹配文件编码
cfg.setOutputEncoding("UTF-8"); // ← 关键!匹配响应头 charset
cfg.setTemplateUpdateDelay(0L); // ← 开发必备
cfg.setNumberFormat("#"); // 避免数字格式化乱码
cfg.setDateFormat("yyyy-MM-dd");
cfg.setTimeFormat("HH:mm:ss");
⚠️ 重要提醒:IntelliJ 中 .ftl 文件右下角显示的编码(如 UTF-8)仅表示编辑器当前解读方式,务必通过「File → File Encoding → Convert」确认并保存为真实 UTF-8(无 BOM)。使用记事本另存为时,务必选择「UTF-8」而非「UTF-8-BOM」。
✅ 三、Spring Boot 全链路 UTF-8 强制保障(application.yml)
仅配置 FreeMarker 不够!HTTP 请求/响应、数据库连接、日志输出等环节必须协同:
# 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 # 响应头 Content-Type 显式声明
charset: UTF-8 # 模板文件编码(Spring Boot 2.3+ 已默认)
cache: false # 开发期关闭缓存
settings:
template_update_delay: 0
default_encoding: UTF-8 # FreeMarker 引擎级编码
classic_compatible: true # 容忍空值(避免 InvalidReferenceException)
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
logging:
pattern:
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
charset:
console: UTF-8 # Logback 控制台日志编码(Spring Boot 2.4+ 支持)
✅ 四、空值安全与异常处理(告别 InvalidReferenceException)
FreeMarker 默认对 null 或缺失变量严格报错。两种专业级应对方案:
方案1:模板内防御性写法(推荐)
${user?.name!""}
Hello ${user.name}Guest#if>
@hots<div>Hot list not available</div>#if>
方案2:启用兼容模式 + 全局异常处理器
spring:
freemarker:
settings:
classic_compatible: true # 自动将 null 转为空字符串,不抛异常
或自定义异常处理器(生产环境推荐):
@Component
public class MyTemplateExceptionHandler implements TemplateExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(MyTemplateExceptionHandler.class);
@Override
public void handleTemplateException(TemplateException te, Environment env, Writer out)
throws TemplateException {
log.warn("FreeMarker template error at {}", te.getFTLInstructionStack(), te);
try {
out.write("<!-- Template render error: " + te.getMessage() + " -->");
} catch (IOException e) {
throw new TemplateException("Failed to write error placeholder", env, e);
}
}
}
✅ 五、终极验证清单(部署前必查)
| 环节 | 检查项 | 验证方式 |
|---|---|---|
| 文件层 |
template.ftl 是否为 UTF-8(无 BOM)? |
file -i template.ftl(Linux/macOS)或用 VS Code 查看右下角编码 |
| JVM 层 |
Charset.defaultCharset() 是否为 UTF-8? |
启动时打印日志验证 |
| FreeMarker 层 |
cfg.getDefaultEncoding() 返回 UTF-8? |
调试断点或日志输出 |
| HTTP 层 | 响应头 Content-Type 是否含 charset=UTF-8? |
浏览器 DevTools → Network → Response Headers |
| 终端层 |
System.out 输出中文是否正常? |
运行 System.out.println("你好") 直接验证 |
? 核心结论:FreeMarker 乱码不是“配没配 UTF-8”,而是“所有环节是否用同一把 UTF-8 钥匙开同一把锁”。从
.ftl文件保存、IDE 编码设置、JVM 启动参数、Spring 配置、HTTP 头到终端环境,五者必须严格对齐。任何一处使用GBK/ISO-8859-1/windows-1250,都会导致全链路解码失败——这正是jůůů变j的本质原因。
遵循本文方案,你将彻底告别 FreeMarker 的编码噩梦,让捷克语 koloběžka、中文 验证码、日文 検証コード 在任意环节稳定、准确、优雅地呈现。











