1. 核心概念

  • 字符集 (Character Set): 定义了字符与数字(码点)之间的映射。例如,ASCII、ISO-8859-1、UTF-8、UTF-16 都是字符集。UTF-8 是目前最广泛使用的 Unicode 编码方式,能表示几乎所有的语言字符。
  • 编码 (Encoding): 特指将字符集中的字符(码点)转换为字节序列(二进制数据)的规则。例如,UTF-8GBKISO-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 编码

  • 模板文件编码:
    1. 确保所有 .ftl 模板文件以 UTF-8 编码保存
    2. 使用文本编辑器(如 VS Code, IntelliJ IDEA, Notepad++)打开模板文件。
    3. 在编辑器的“文件”或“编码”菜单中,选择“另存为”,并明确选择编码为 UTF-8 (通常有 "UTF-8" 或 "UTF-8 without BOM" 选项,推荐后者)。
    4. 保存文件。
  • Java 源代码编码:
    1. 确保你的 Java 源代码文件(.java)也以 UTF-8 编码保存,特别是当代码中包含中文字符串字面量时。
    2. 在 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
  • 构建工具配置 (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'
      

步骤 2: 配置 FreeMarker Configuration (核心)

这是最关键的一步,需要在应用启动时(如 Spring Boot 的 @Bean 方法、Servlet 的 init() 方法或 main() 方法中)完成。

  1. 创建 Configuration 实例:

    import freemarker.template.Configuration;
    import freemarker.template.TemplateExceptionHandler;
    import freemarker.template.Version;
    
    // 创建 Configuration 实例,指定 FreeMarker 版本(推荐使用最新稳定版)
    Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); // 使用实际版本号
    
  2. 设置模板加载器 (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"); // 设置模板加载器读取文件时使用的编码
      
  3. 设置默认模板编码 (关键):

    • 告诉 FreeMarker,当它从加载器读取模板文件时,应该假设这些文件是用什么编码存储的
    • 因为我们在步骤 1 中已确保所有模板都是 UTF-8,所以这里必须设置为 UTF-8
    cfg.setDefaultEncoding("UTF-8"); // 这是处理模板文件编码的关键设置
    
    • 重要: setDefaultEncoding 设置的是模板文件的编码,不是输出编码。
  4. 设置输出编码 (关键):

    • 告诉 FreeMarker,当它生成最终的文本输出(写入 WriterOutputStream)时,应该使用什么编码。
    • 这个编码需要与你的 Web 服务器、Servlet 容器以及最终客户端(浏览器)的期望一致。
    • 对于 Web 应用,通常设置为 UTF-8
    cfg.setOutputEncoding("UTF-8"); // 设置生成的输出文本的编码
    
  5. 设置其他重要配置 (推荐):

    // 设置模板缓存策略(生产环境推荐使用,默认通常是 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);
    
  6. (可选) 为特定模板指定编码:

    • 如果某个模板文件使用了非默认编码(不推荐,应统一为 UTF-8),可以在加载时指定:
      Template template = cfg.getTemplate("template.ftl", "GBK"); // 显式指定此模板用 GBK 编码读取
      

步骤 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 字符。
  • 将数据放入数据模型:

    • 只要数据在 Java String 对象中是正确的(内部是 UTF-16),FreeMarker 就能正确处理。FreeMarker 会自动将 String 写入 WriterWriter 的编码由 ConfigurationoutputEncoding 决定。
    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();
    

步骤 4: Web 应用集成 (Servlet/JSP 替代)

  1. 设置 Servlet 响应编码:
    • 在写入响应体之前,设置响应的 Content-Type 头,明确指定 charset=UTF-8
    response.setContentType("text/html; charset=UTF-8");
    // 或者
    response.setCharacterEncoding("UTF-8");
    response.setContentType("text/html");
    // 注意:setCharacterEncoding 必须在 getWriter() 或 getOutputStream() 之前调用
    
  2. 确保输出流使用正确编码:
    • 如上所述,使用 OutputStreamWriter(response.getOutputStream(), "UTF-8")PrintWriter (其编码由 response.setCharacterEncoding 决定)。
  3. 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>
    

3. 常见错误

  1. 中文乱码 (方块、问号、奇怪符号):
    • 原因: 最常见。通常是模板文件编码、Configuration.setDefaultEncoding()Configuration.setOutputEncoding()response.setContentType() 或数据源解码中某一个或多个环节没有统一为 UTF-8。
    • 排查: 逐一检查上述所有环节的编码设置。
  2. 模板加载失败 (MalformedInputException, UnmappableCharacterException):
    • 原因: Configuration.setDefaultEncoding() 设置的编码与模板文件实际编码不符。例如,模板是 GBK 保存,但 setDefaultEncoding("UTF-8") 会导致读取时解析错误。
    • 解决: 统一模板编码为 UTF-8 并正确设置 setDefaultEncoding("UTF-8")
  3. 输出包含 HTML 实体 (如 &lt;, &amp;):
    • 原因: FreeMarker 默认对 ${...} 中的变量进行 HTML 转义以防止 XSS。如果变量本身包含 HTML 标签,且你希望原样输出,需要禁用转义。
    • 解决: 使用 ?no_esc 内建函数:${unsafeHtml?no_esc}注意安全风险! 或者使用 <#noescape> 指令块。
  4. ?html 内建函数导致双重转义:
    • 原因: 变量已经被转义过一次(如从数据库读取的转义字符串),再用 ?html 转义。
    • 解决: 确保数据模型中的字符串是原始的,让 FreeMarker 在输出时统一处理转义。避免在数据源中预先转义。
  5. cfg.setEncoding(...) 不存在:
    • 原因: 旧版 FreeMarker API。现在使用 setDefaultEncoding()setOutputEncoding()
    • 解决: 使用正确的方法名。

4. 注意事项

  1. 统一为 UTF-8: 这是避免编码问题的黄金法则。强制要求所有文件(.ftl, .java, .properties, .html, .css, .js)、数据库、网络传输都使用 UTF-8。
  2. setDefaultEncoding vs setOutputEncoding: 务必区分清楚。setDefaultEncoding 是读模板文件的编码,setOutputEncoding 是写最终结果的编码。两者通常都设为 UTF-8
  3. response.setCharacterEncoding()getWriter(): 在 Servlet 中,setCharacterEncoding() 必须在调用 getWriter()getOutputStream() 之前调用,否则可能无效。
  4. BOM (Byte Order Mark): UTF-8 文件的 BOM (EF BB BF) 有时会导致问题。建议保存为 "UTF-8 without BOM"。
  5. 日志编码: 确保应用日志框架(如 Logback, Log4j2)的输出编码也是 UTF-8,以便正确记录包含非 ASCII 字符的日志。
  6. 第三方库: 注意集成的其他库(如 JSON 库 Jackson/Gson)的编码处理,确保它们也使用 UTF-8。
  7. 开发与生产环境一致性: 确保开发、测试、生产环境的文件编码、JVM 参数、数据库配置等保持一致。

5. 使用技巧

  1. 使用 #include#import: 合理拆分模板,复用公共部分(如头部、尾部、导航栏)。被包含/导入的模板也需是 UTF-8。
  2. ?html, ?js_string, ?json_string 内建函数: 根据输出上下文(HTML Body, JavaScript String, JSON)进行正确的转义。
  3. <#escape> 指令: 为代码块设置默认的转义规则,减少重复的 ?html
  4. <#noescape> 指令: 在已转义的上下文中,临时关闭转义(谨慎使用)。
  5. 自定义 TemplateLoader: 如果需要从特殊位置(如数据库、网络)加载模板,可以实现 TemplateLoader 接口,并在 getBytes()getReader() 方法中正确处理编码。
  6. 调试: 利用 TemplateExceptionHandler.HTML_DEBUG_HANDLER 在页面上显示详细的错误信息(仅限开发环境)。

6. 最佳实践与性能优化

  1. 最佳实践:

    • 强制 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。对用户输入进行验证和清理。
  2. 性能优化:

    • 启用并合理配置缓存: 这是最大的性能提升点。确保 TemplateCache 正常工作。
    • 避免频繁创建 Template 实例: 使用 cfg.getTemplate(templateName) 获取 Template 实例,它会被缓存。避免每次都 new Template(...)
    • 优化数据模型: 只传递模板实际需要的数据,避免传递庞大的对象图。
    • 使用 ?if_exists 或默认值: 避免在模板中因访问 null 属性而抛出异常。
      ${user.profile?.bio!""} <!-- 如果 bio 不存在或为 null,使用空字符串 -->
      
    • 减少复杂计算: 将复杂的计算或数据处理放在 Java 代码中完成,将结果放入数据模型,而不是在模板中进行。
    • 监控: 在生产环境中监控模板处理时间和频率,识别性能瓶颈。

遵循以上指南,你将能够有效地管理和避免 FreeMarker 中的编码问题,并构建出高效、可靠且国际化的应用。