FreeMarker 深度解析:Java 模板引擎的最佳实践

一、FreeMarker 简介

FreeMarker 是一款强大的基于 Java 的模板引擎,用于生成各种文本输出(HTML、XML、JSON、配置文件、源代码等)。它遵循 MVC 模式,将页面表现层与业务逻辑完全分离,是 Java Web 开发中不可或缺的工具。

1.1 核心特性

特性 说明
模板与数据分离 模板文件独立于业务逻辑,便于维护和复用
强大的表达式语言 支持复杂的数据操作和逻辑处理
类型安全 在模板中可以进行类型检查
宏与函数 支持自定义宏和函数,提高代码复用
国际化支持 内置国际化和本地化功能
扩展性强 支持自定义指令和标签

1.2 工作原理

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   业务逻辑层      │────►│    FreeMarker    │────►│    输出结果      │
│   (Java Objects) │     │   Template Engine│     │ (HTML/XML/JSON) │
└──────────────────┘     └──────────────────┘     └──────────────────┘
                              │
                              ▼
                       ┌──────────────────┐
                       │   模板文件       │
                       │  (.ftl 文件)     │
                       └──────────────────┘

二、基本语法与用法

2.1 变量输出

<!-- 简单变量输出 -->
Hello, ${name}!

<!-- 带有默认值的变量 -->
Hello, ${name!"Guest"}!

<!-- 对象属性访问 -->
User: ${user.name}, Age: ${user.age}

<!-- 数组/列表访问 -->
First item: ${items[0]}

<!-- 集合大小 -->
Total items: ${items?size}

2.2 条件判断

<!-- if 语句 -->
<#if user.age >= 18>
    <p>成年用户</p>
<#else>
    <p>未成年用户</p>
</#if>

<!-- 多条件判断 -->
<#if score >= 90>
    <p>优秀</p>
<#elseif score >= 80>
    <p>良好</p>
<#elseif score >= 60>
    <p>及格</p>
<#else>
    <p>不及格</p>
</#if>

<!-- 空值判断 -->
<#if user??>
    <p>用户存在: ${user.name}</p>
</#if>

<!-- 非空判断 -->
<#if user.name?has_content>
    <p>用户名: ${user.name}</p>
</#if>

2.3 循环遍历

<!-- 遍历列表 -->
<ul>
<#list users as user>
    <li>${user_index + 1}. ${user.name} - ${user.email}</li>
</#list>
</ul>

<!-- 遍历 Map -->
<#list map?keys as key>
    <p>${key}: ${map[key]}</p>
</#list>

<!-- 遍历数字范围 -->
<#list 1..10 as i>
    <p>Item ${i}</p>
</#list>

<!-- 空集合处理 -->
<#list users as user>
    <p>${user.name}</p>
<#else>
    <p>暂无用户</p>
</#list>

2.4 宏定义与调用

<!-- 定义宏 -->
<#macro greeting name>
    <div class="greeting">
        Hello, ${name}!
    </div>
</#macro>

<!-- 调用宏 -->
<@greeting name="World"/>

<!-- 带参数默认值的宏 -->
<#macro welcome name="Guest">
    <p>Welcome, ${name}!</p>
</#macro>

<!-- 调用带默认参数的宏 -->
<@welcome/>
<@welcome name="John"/>

<!-- 嵌套内容的宏 -->
<#macro box title>
    <div class="box">
        <h3>${title}</h3>
        <div class="content">
            <#nested>
        </div>
    </div>
</#macro>

<!-- 使用嵌套内容 -->
<@box title="My Box">
    <p>This is the content inside the box.</p>
</@box>

2.5 内置函数

<!-- 字符串函数 -->
${"Hello World"?upper_case}      <!-- HELLO WORLD -->
${"Hello World"?lower_case}      <!-- hello world -->
${"Hello"?trim}                   <!-- Hello (去除首尾空格) -->
${"Hello World"?length}           <!-- 11 -->
${"Hello"?index_of("ll")}         <!-- 2 -->

<!-- 集合函数 -->
${users?size}                     <!-- 集合大小 -->
${users?sort_by("age")}           <!-- 按年龄排序 -->
${users?reverse}                  <!-- 反转集合 -->
${users?first}                    <!-- 第一个元素 -->
${users?last}                     <!-- 最后一个元素 -->

<!-- 数字函数 -->
${3.14?round}                     <!-- 3 -->
${3.14?floor}                     <!-- 3 -->
${3.14?ceiling}                   <!-- 4 -->
${10?string(",###")}              <!-- 10 -->

<!-- 日期函数 -->
${now?date}                       <!-- 2024-01-15 -->
${now?time}                       <!-- 14:30:00 -->
${now?datetime}                   <!-- 2024-01-15 14:30:00 -->
${now?string("yyyy-MM-dd")}       <!-- 自定义格式 -->

2.6 自定义函数

<!-- 定义自定义函数 -->
<#function calculateDiscount price discount>
    <#return price * (1 - discount / 100)>
</#function>

<!-- 使用自定义函数 -->
Original: $${price}
Discount: ${discount}%
Final: $${calculateDiscount(price, discount)}

三、Java 集成实战

3.1 基本配置

Maven 依赖

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>

Spring Boot 集成

@Configuration
public class FreeMarkerConfig {
    
    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates/");
        Properties props = new Properties();
        props.setProperty("default_encoding", "UTF-8");
        props.setProperty("number_format", "#.##");
        configurer.setFreemarkerSettings(props);
        return configurer;
    }
}

3.2 基本使用示例

import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

public class FreeMarkerExample {
    
    public static void main(String[] args) throws Exception {
        // 创建配置对象
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setClassForTemplateLoading(FreeMarkerExample.class, "/templates");
        cfg.setDefaultEncoding("UTF-8");
        
        // 创建数据模型
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("title", "FreeMarker Example");
        dataModel.put("message", "Hello from FreeMarker!");
        
        // 获取模板
        Template template = cfg.getTemplate("hello.ftl");
        
        // 处理模板并输出
        StringWriter writer = new StringWriter();
        template.process(dataModel, writer);
        
        // 输出结果
        System.out.println(writer.toString());
    }
}

3.3 Spring MVC 集成

Controller 示例

@Controller
public class UserController {
    
    @GetMapping("/users")
    public String listUsers(Model model) {
        List<User> users = userService.findAll();
        model.addAttribute("users", users);
        model.addAttribute("pageTitle", "用户列表");
        return "users/list"; // 对应 templates/users/list.ftl
    }
}

模板文件 users/list.ftl

<!DOCTYPE html>
<html>
<head>
    <title>${pageTitle}</title>
</head>
<body>
    <h1>${pageTitle}</h1>
    
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Email</th>
            </tr>
        </thead>
        <tbody>
            <#list users as user>
                <tr>
                    <td>${user.id}</td>
                    <td>${user.name}</td>
                    <td>${user.email}</td>
                </tr>
            <#else>
                <tr>
                    <td colspan="3">暂无用户</td>
                </tr>
            </#list>
        </tbody>
    </table>
</body>
</html>

3.4 生成代码文件

public class CodeGenerator {
    
    public void generateEntity(String entityName, List<String> fields) throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setClassForTemplateLoading(CodeGenerator.class, "/templates");
        
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("entityName", entityName);
        dataModel.put("fields", fields);
        dataModel.put("packageName", "com.example.entity");
        
        Template template = cfg.getTemplate("entity.ftl");
        
        try (FileWriter writer = new FileWriter("src/main/java/com/example/entity/" + entityName + ".java")) {
            template.process(dataModel, writer);
        }
    }
}

代码模板 entity.ftl

package ${packageName};

public class ${entityName} {
<#list fields as field>
    private String ${field};
    
    public String get${field?cap_first}() {
        return ${field};
    }
    
    public void set${field?cap_first}(String ${field}) {
        this.${field} = ${field};
    }
</#list>
}

四、高级特性

4.1 自定义指令

public class CustomDirective implements TemplateDirectiveModel {
    
    @Override
    public void execute(Environment env, Map params, TemplateModel[] loopVars, 
                        TemplateDirectiveBody body) throws TemplateException, IOException {
        
        // 获取参数
        String title = getParam(params, "title");
        
        // 输出开始标签
        env.getOut().write("<div class=\"custom-box\">");
        env.getOut().write("<h2>" + title + "</h2>");
        
        // 处理嵌套内容
        if (body != null) {
            body.render(env.getOut());
        }
        
        // 输出结束标签
        env.getOut().write("</div>");
    }
    
    private String getParam(Map params, String name) throws TemplateModelException {
        TemplateModel model = (TemplateModel) params.get(name);
        if (model instanceof SimpleScalar) {
            return ((SimpleScalar) model).getAsString();
        }
        return null;
    }
}

注册自定义指令

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setSharedVariable("customBox", new CustomDirective());

使用自定义指令

<@customBox title="My Custom Box">
    <p>This is the content inside the custom box.</p>
</@customBox>

4.2 宏库与导入

<!-- common.ftl - 宏库文件 -->
<#macro button text color="primary">
    <button class="btn btn-${color}">${text}</button>
</#macro>

<#macro alert type="info" message>
    <div class="alert alert-${type}">${message}</div>
</#macro>
<!-- 在其他模板中导入宏库 -->
<#import "common.ftl" as common>

<@common.button text="Click Me"/>
<@common.button text="Submit" color="success"/>
<@common.alert type="warning" message="Warning!"/>

4.3 国际化支持

资源文件 messages_en.properties

greeting=Hello
welcome=Welcome to our application

资源文件 messages_zh.properties

greeting=你好
welcome=欢迎使用我们的应用

模板中使用国际化

<#assign locale = "zh_CN">
<#assign bundle = ResourceBundle.getBundle("messages", locale)>

<p>${bundle.greeting}, ${user.name}!</p>
<p>${bundle.welcome}</p>

4.4 模板继承

<!-- base.ftl - 基础模板 -->
<!DOCTYPE html>
<html>
<head>
    <title><#if title??>${title}</#if></title>
    <link rel="stylesheet" href="style.css">
    <#block name="head">
        <!-- 子模板可以覆盖这里 -->
    </#block>
</head>
<body>
    <header>
        <h1>My App</h1>
    </header>
    
    <div class="content">
        <#block name="content">
            <!-- 子模板内容 -->
        </#block>
    </div>
    
    <footer>
        <p>© 2024 My App</p>
    </footer>
</body>
</html>
<!-- 子模板继承基础模板 -->
<#include "base.ftl">

<#assign title = "Home Page">

<#block name="head">
    <style>
        .special { color: red; }
    </style>
</#block>

<#block name="content">
    <h2>Welcome to the Home Page</h2>
    <p class="special">This is the home content.</p>
</#block>

五、性能优化与最佳实践

5.1 性能优化技巧

1. 模板缓存

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setCacheStorage(new MruCacheStorage(20, 250)); // 设置缓存大小

2. 避免重复计算

<!-- 不推荐 -->
<#list items as item>
    ${getItemDetails(item)?html}
</#list>

<!-- 推荐:使用 assign 缓存结果 -->
<#assign itemDetails = getItemDetails()>
<#list items as item>
    ${itemDetails[item.id]?html}
</#list>

3. 使用 ?no_esc 避免不必要的转义

<!-- 已知内容安全时使用 -->
${trustedContent?no_esc}

5.2 安全最佳实践

1. 输出转义

<!-- HTML转义 -->
${userInput?html}

<!-- XML转义 -->
${userInput?xml}

<!-- JSON转义 -->
${userInput?json_string}

2. 禁用危险指令

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setWhitespaceStripping(true);
cfg.setDisableBuiltin("eval"); // 禁用 eval

3. 验证用户输入

<#if userInput?matches("^[a-zA-Z0-9]+$")>
    <p>Valid input: ${userInput}</p>
<#else>
    <p>Invalid input</p>
</#if>

5.3 代码组织最佳实践

1. 模板目录结构

templates/
├── layout/          # 布局模板
│   ├── base.ftl
│   └── sidebar.ftl
├── common/          # 公共宏库
│   ├── forms.ftl
│   └── widgets.ftl
├── users/           # 用户模块
│   ├── list.ftl
│   └── detail.ftl
└── products/        # 产品模块
    ├── list.ftl
    └── detail.ftl

2. 使用宏库减少重复代码

<!-- common/forms.ftl -->
<#macro input name label value="" type="text">
    <div class="form-group">
        <label for="${name}">${label}</label>
        <input type="${type}" id="${name}" name="${name}" value="${value?html}"/>
    </div>
</#macro>

3. 注释模板代码

<#-- 
    用户列表页面模板
    参数:
    - users: 用户列表
    - pageTitle: 页面标题
-->

六、常见问题与解决方案

6.1 中文乱码问题

问题:模板输出的中文显示为乱码

解决方案

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputEncoding("UTF-8");

6.2 模板找不到

问题TemplateNotFoundException

解决方案

// 确保模板路径正确
cfg.setClassForTemplateLoading(MyClass.class, "/templates");

// 或者使用文件系统路径
cfg.setDirectoryForTemplateLoading(new File("/path/to/templates"));

6.3 空值异常

问题:变量为 null 时抛出异常

解决方案

<!-- 使用默认值 -->
${user.name!"Unknown"}

<!-- 使用 if 判断 -->
<#if user.name??>
    ${user.name}
</#if>

6.4 性能问题

问题:模板渲染速度慢

解决方案

  1. 启用模板缓存
  2. 减少嵌套循环
  3. 避免在模板中进行复杂计算
  4. 使用 ?cache 缓存昂贵的表达式

七、FreeMarker 与其他模板引擎对比

特性 FreeMarker Thymeleaf Velocity Mustache
语言类型 专用模板语言 HTML-first 专用模板语言 极简标记
学习曲线 中等 极低
功能丰富度
Spring集成 良好 优秀 一般 一般
国际化 内置 内置 内置 需扩展
性能 优秀 良好 良好 优秀
社区活跃度 稳定 活跃 较低 活跃

选择建议

选择 FreeMarker 当:

  • 需要强大的模板功能和灵活的表达式语言
  • 正在构建复杂的动态内容系统
  • 需要代码生成或文档生成功能
  • 已有 FreeMarker 项目需要维护

选择 Thymeleaf 当:

  • 构建现代 HTML5 应用
  • 追求自然模板(模板本身是有效的 HTML)
  • 需要更好的 Spring Boot 集成

选择 Mustache 当:

  • 需要跨平台模板(JavaScript、Java、Python等)
  • 追求极简语法
  • 构建轻量级应用

八、总结

FreeMarker 作为一款成熟的 Java 模板引擎,以其强大的功能、灵活的语法和良好的性能,在 Java Web 开发中占据重要地位。它特别适合以下场景:

  1. Web 页面生成:与 Spring MVC 集成,生成动态 HTML
  2. 代码生成:自动生成 Java 实体类、配置文件等
  3. 文档生成:生成报告、PDF 内容、邮件模板等
  4. 配置文件生成:根据模板生成各种格式的配置文件

掌握 FreeMarker 的核心语法和最佳实践,能够显著提高开发效率,使代码更加清晰和可维护。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容