1. 核心概念
- 字符集 (Character Set): 定义了字符与数字(码点)之间的映射。例如,ASCII、ISO-8859-1、UTF-8、UTF-16 都是字符集。UTF-8 是目前最广泛使用的 Unicode 编码方式,能表示几乎所有的语言字符。
- 编码 (Encoding): 特指将字符集中的字符(码点)转换为字节序列(二进制数据)的规则。例如,
UTF-8
、GBK
、ISO-8859-1
都是编码方式。在 FreeMarker 上下文中,“编码”通常指的就是文件的编码格式。 - 模板文件编码 (Template File Encoding): 指
.ftl
模板文件本身在磁盘上存储时使用的字符编码。如果模板包含中文、日文等非 ASCII 字符,必须明确其编码(强烈推荐 UTF-8)。 - 数据模型编码 (Data Model Encoding): 指传递给模板的数据(如 Java 字符串、对象中的字符串属性)在内存中的编码。在 Java 中,
String
对象内部使用 UTF-16,但其来源(如数据库、文件读取、网络请求)可能涉及不同的编码。 - 输出编码 (Output Encoding): 指 FreeMarker 模板引擎处理完模板后,生成的最终文本输出所使用的字符编码。这个编码需要与客户端(如浏览器)期望的编码一致。
- FreeMarker 模板加载器 (Template Loader): 负责从文件系统、类路径、数据库等位置加载模板文件。加载时需要知道模板文件的原始编码。
Configuration
对象: FreeMarker 的核心配置对象,用于设置全局行为,包括默认模板编码和输出编码。
2. 操作步骤 (非常详细)
以下是确保 FreeMarker 编码正确处理的详细操作步骤:
步骤 1: 统一使用 UTF-8 编码
- 模板文件编码:
- 确保所有
.ftl
模板文件以 UTF-8 编码保存。 - 使用文本编辑器(如 VS Code, IntelliJ IDEA, Notepad++)打开模板文件。
- 在编辑器的“文件”或“编码”菜单中,选择“另存为”,并明确选择编码为 UTF-8 (通常有 "UTF-8" 或 "UTF-8 without BOM" 选项,推荐后者)。
- 保存文件。
- 确保所有
- Java 源代码编码:
- 确保你的 Java 源代码文件(
.java
)也以 UTF-8 编码保存,特别是当代码中包含中文字符串字面量时。 - 在 IDE(如 IntelliJ IDEA, Eclipse)中设置项目或模块的默认编码为 UTF-8。
- IntelliJ IDEA:
File
->Settings
->Editor
->File Encodings
->Global Encoding
,Project Encoding
,Default encoding for properties files
都设置为UTF-8
。 - Eclipse:
Window
->Preferences
->General
->Workspace
->Text file encoding
->Other
->UTF-8
。
- IntelliJ IDEA:
- 确保你的 Java 源代码文件(
- 构建工具配置 (Maven/Gradle):
- Maven: 在
pom.xml
中添加或确认:<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties>
- Gradle: 在
build.gradle
中添加:compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8'
- Maven: 在
步骤 2: 配置 FreeMarker Configuration
(核心)
这是最关键的一步,需要在应用启动时(如 Spring Boot 的 @Bean
方法、Servlet 的 init()
方法或 main()
方法中)完成。
创建
Configuration
实例:import freemarker.template.Configuration; import freemarker.template.TemplateExceptionHandler; import freemarker.template.Version; // 创建 Configuration 实例,指定 FreeMarker 版本(推荐使用最新稳定版) Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); // 使用实际版本号
设置模板加载器 (TemplateLoader):
- 从类路径加载 (推荐用于 Web 应用打包):
// 假设模板在 src/main/resources/templates/ 目录下 cfg.setClassForTemplateLoading(YourClass.class, "/templates");
- 从文件系统加载:
// 指定模板文件所在的目录路径 cfg.setDirectoryForTemplateLoading(new File("/path/to/your/templates"));
- 设置模板加载器的默认编码 (可选但推荐):
如果你的模板文件编码不全是 UTF-8,或者想显式指定,可以设置。但强烈建议所有模板都用 UTF-8。
// 如果模板是 UTF-8,这行通常可以省略,因为 UTF-8 是 FreeMarker 的默认值 // cfg.setDefaultEncoding("UTF-8"); // 设置模板加载器读取文件时使用的编码
- 从类路径加载 (推荐用于 Web 应用打包):
设置默认模板编码 (关键):
- 告诉 FreeMarker,当它从加载器读取模板文件时,应该假设这些文件是用什么编码存储的。
- 因为我们在步骤 1 中已确保所有模板都是 UTF-8,所以这里必须设置为
UTF-8
。
cfg.setDefaultEncoding("UTF-8"); // 这是处理模板文件编码的关键设置
- 重要:
setDefaultEncoding
设置的是模板文件的编码,不是输出编码。
设置输出编码 (关键):
- 告诉 FreeMarker,当它生成最终的文本输出(写入
Writer
或OutputStream
)时,应该使用什么编码。 - 这个编码需要与你的 Web 服务器、Servlet 容器以及最终客户端(浏览器)的期望一致。
- 对于 Web 应用,通常设置为
UTF-8
。
cfg.setOutputEncoding("UTF-8"); // 设置生成的输出文本的编码
- 告诉 FreeMarker,当它生成最终的文本输出(写入
设置其他重要配置 (推荐):
// 设置模板缓存策略(生产环境推荐使用,默认通常是 TemplateCache) cfg.setTemplateLookupStrategy(new TemplateLookupStrategy() { /* ... */ }); // 通常用默认即可 // 设置模板异常处理策略 cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); // 开发时推荐,便于调试 // cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER); // 另一种调试方式 // cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER); // 生产环境有时用,但不推荐隐藏错误 // 设置对象包装器 (ObjectWrapper),处理 Java 对象到模板变量的转换 cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build()); // 设置日志 cfg.setLogTemplateExceptions(false); // 生产环境可设为 false,避免敏感信息泄露 cfg.setWrapUncheckedExceptions(true); cfg.setFallbackOnNullLoopVariable(false);
(可选) 为特定模板指定编码:
- 如果某个模板文件使用了非默认编码(不推荐,应统一为 UTF-8),可以在加载时指定:
Template template = cfg.getTemplate("template.ftl", "GBK"); // 显式指定此模板用 GBK 编码读取
- 如果某个模板文件使用了非默认编码(不推荐,应统一为 UTF-8),可以在加载时指定:
步骤 3: 处理数据模型中的字符串
确保数据来源正确解码:
- 数据库: 确保 JDBC 连接 URL 包含正确的字符集参数(对于 MySQL:
?useUnicode=true&characterEncoding=UTF-8
),并且数据库、表、列的字符集也是 UTF-8。 - 文件读取: 使用
InputStreamReader
时,明确指定编码:BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("data.txt"), "UTF-8"));
- 网络请求 (HTTP): 解析请求参数或 Body 时,注意
Content-Type
头中的charset
参数。对于application/x-www-form-urlencoded
,Servlet 容器的request.setCharacterEncoding("UTF-8")
很重要(需在读取参数前调用)。对于 JSON,通常用 UTF-8 解析。 - Java 字符串字面量: 确保
.java
文件是 UTF-8 编码,IDE 正确识别,这样字符串字面量才能正确表示非 ASCII 字符。
- 数据库: 确保 JDBC 连接 URL 包含正确的字符集参数(对于 MySQL:
将数据放入数据模型:
- 只要数据在 Java
String
对象中是正确的(内部是 UTF-16),FreeMarker 就能正确处理。FreeMarker 会自动将String
写入Writer
,Writer
的编码由Configuration
的outputEncoding
决定。
Map<String, Object> dataModel = new HashMap<>(); dataModel.put("userName", "张三"); // 假设 "张三" 在 String 中是正确的 dataModel.put("message", "你好,世界!"); Template template = cfg.getTemplate("welcome.ftl"); // 使用步骤 2 配置好的 cfg Writer out = new OutputStreamWriter(response.getOutputStream(), "UTF-8"); // 确保输出流使用 UTF-8 template.process(dataModel, out); out.flush();
- 只要数据在 Java
步骤 4: Web 应用集成 (Servlet/JSP 替代)
- 设置 Servlet 响应编码:
- 在写入响应体之前,设置响应的
Content-Type
头,明确指定charset=UTF-8
。
response.setContentType("text/html; charset=UTF-8"); // 或者 response.setCharacterEncoding("UTF-8"); response.setContentType("text/html"); // 注意:setCharacterEncoding 必须在 getWriter() 或 getOutputStream() 之前调用
- 在写入响应体之前,设置响应的
- 确保输出流使用正确编码:
- 如上所述,使用
OutputStreamWriter(response.getOutputStream(), "UTF-8")
或PrintWriter
(其编码由response.setCharacterEncoding
决定)。
- 如上所述,使用
- HTML 页面 Meta 标签 (可选但推荐):
- 在生成的 HTML 模板中,添加
<meta charset="UTF-8">
标签,作为浏览器的后备提示。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Welcome</title> </head> <body> <h1>Hello, ${userName}!</h1> </body> </html>
- 在生成的 HTML 模板中,添加
3. 常见错误
- 中文乱码 (方块、问号、奇怪符号):
- 原因: 最常见。通常是模板文件编码、
Configuration.setDefaultEncoding()
、Configuration.setOutputEncoding()
、response.setContentType()
或数据源解码中某一个或多个环节没有统一为 UTF-8。 - 排查: 逐一检查上述所有环节的编码设置。
- 原因: 最常见。通常是模板文件编码、
- 模板加载失败 (MalformedInputException, UnmappableCharacterException):
- 原因:
Configuration.setDefaultEncoding()
设置的编码与模板文件实际编码不符。例如,模板是 GBK 保存,但setDefaultEncoding("UTF-8")
会导致读取时解析错误。 - 解决: 统一模板编码为 UTF-8 并正确设置
setDefaultEncoding("UTF-8")
。
- 原因:
- 输出包含 HTML 实体 (如
<
,&
):- 原因: FreeMarker 默认对
${...}
中的变量进行 HTML 转义以防止 XSS。如果变量本身包含 HTML 标签,且你希望原样输出,需要禁用转义。 - 解决: 使用
?no_esc
内建函数:${unsafeHtml?no_esc}
。注意安全风险! 或者使用<#noescape>
指令块。
- 原因: FreeMarker 默认对
?html
内建函数导致双重转义:- 原因: 变量已经被转义过一次(如从数据库读取的转义字符串),再用
?html
转义。 - 解决: 确保数据模型中的字符串是原始的,让 FreeMarker 在输出时统一处理转义。避免在数据源中预先转义。
- 原因: 变量已经被转义过一次(如从数据库读取的转义字符串),再用
cfg.setEncoding(...)
不存在:- 原因: 旧版 FreeMarker API。现在使用
setDefaultEncoding()
和setOutputEncoding()
。 - 解决: 使用正确的方法名。
- 原因: 旧版 FreeMarker API。现在使用
4. 注意事项
- 统一为 UTF-8: 这是避免编码问题的黄金法则。强制要求所有文件(.ftl, .java, .properties, .html, .css, .js)、数据库、网络传输都使用 UTF-8。
setDefaultEncoding
vssetOutputEncoding
: 务必区分清楚。setDefaultEncoding
是读模板文件的编码,setOutputEncoding
是写最终结果的编码。两者通常都设为UTF-8
。response.setCharacterEncoding()
和getWriter()
: 在 Servlet 中,setCharacterEncoding()
必须在调用getWriter()
或getOutputStream()
之前调用,否则可能无效。- BOM (Byte Order Mark): UTF-8 文件的 BOM (EF BB BF) 有时会导致问题。建议保存为 "UTF-8 without BOM"。
- 日志编码: 确保应用日志框架(如 Logback, Log4j2)的输出编码也是 UTF-8,以便正确记录包含非 ASCII 字符的日志。
- 第三方库: 注意集成的其他库(如 JSON 库 Jackson/Gson)的编码处理,确保它们也使用 UTF-8。
- 开发与生产环境一致性: 确保开发、测试、生产环境的文件编码、JVM 参数、数据库配置等保持一致。
5. 使用技巧
- 使用
#include
和#import
: 合理拆分模板,复用公共部分(如头部、尾部、导航栏)。被包含/导入的模板也需是 UTF-8。 ?html
,?js_string
,?json_string
内建函数: 根据输出上下文(HTML Body, JavaScript String, JSON)进行正确的转义。<#escape>
指令: 为代码块设置默认的转义规则,减少重复的?html
。<#noescape>
指令: 在已转义的上下文中,临时关闭转义(谨慎使用)。- 自定义
TemplateLoader
: 如果需要从特殊位置(如数据库、网络)加载模板,可以实现TemplateLoader
接口,并在getBytes()
或getReader()
方法中正确处理编码。 - 调试: 利用
TemplateExceptionHandler.HTML_DEBUG_HANDLER
在页面上显示详细的错误信息(仅限开发环境)。
6. 最佳实践与性能优化
最佳实践:
- 强制 UTF-8: 项目根目录放置
.editorconfig
文件,统一团队编码:root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.ftl] charset = utf-8
- 单例
Configuration
:Configuration
对象是线程安全的,应在应用生命周期内创建一个实例并重复使用。不要为每次请求创建新实例。 - 模板缓存:
Configuration
默认启用模板缓存。确保setTemplateUpdateDelayMilliseconds(0)
仅在开发环境设置(实时刷新模板),生产环境应设置一个合理的延迟(如60000
毫秒)或-1
(永不检查更新) 以提升性能。// 开发环境 cfg.setTemplateUpdateDelayMilliseconds(0); // 生产环境 cfg.setTemplateUpdateDelayMilliseconds(60000); // 每分钟检查一次更新
- 清晰分离: 保持模板逻辑简单,避免在模板中编写复杂业务逻辑。使用
@* comments *
进行模板内注释。 - 安全: 始终考虑 XSS 风险。除非明确需要(且已验证内容安全),否则不要使用
?no_esc
。对用户输入进行验证和清理。
- 强制 UTF-8: 项目根目录放置
性能优化:
- 启用并合理配置缓存: 这是最大的性能提升点。确保
TemplateCache
正常工作。 - 避免频繁创建
Template
实例: 使用cfg.getTemplate(templateName)
获取Template
实例,它会被缓存。避免每次都new Template(...)
。 - 优化数据模型: 只传递模板实际需要的数据,避免传递庞大的对象图。
- 使用
?if_exists
或默认值: 避免在模板中因访问null
属性而抛出异常。${user.profile?.bio!""} <!-- 如果 bio 不存在或为 null,使用空字符串 -->
- 减少复杂计算: 将复杂的计算或数据处理放在 Java 代码中完成,将结果放入数据模型,而不是在模板中进行。
- 监控: 在生产环境中监控模板处理时间和频率,识别性能瓶颈。
- 启用并合理配置缓存: 这是最大的性能提升点。确保
遵循以上指南,你将能够有效地管理和避免 FreeMarker 中的编码问题,并构建出高效、可靠且国际化的应用。