目 录CONTENT

文章目录

FreeMarker 完全入门指南:从零到精通模板引擎的艺术 🚀

Administrator
2025-10-03 / 0 评论 / 0 点赞 / 11 阅读 / 0 字 / 正在检测是否收录...

📖 引言

如果你是一名Java开发者,一定遇到过这样的场景:需要生成HTML页面、发送格式化的邮件、创建代码模板、或者批量生成静态网站。这时候,你可能会想:"要是有个工具能让我把'数据'和'展示'分离就好了!"


🎯 第一部分:环境准备

1.1 Maven依赖配置

首先,在你的pom.xml中添加FreeMarker依赖:

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

💡 小提示:建议使用2.3.32或更新版本,这是目前最稳定的版本!

1.2 快速体验

让我们写一个超级简单的Hello World:

import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.*;
import java.util.*;

public class FreeMarkerDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建配置对象
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setDirectoryForTemplateLoading(new File("templates"));
        cfg.setDefaultEncoding("UTF-8");
        
        // 2. 加载模板
        Template template = cfg.getTemplate("hello.ftl");
        
        // 3. 准备数据
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("name", "FreeMarker新手");
        
        // 4. 输出结果
        Writer out = new OutputStreamWriter(System.out);
        template.process(dataModel, out);
        out.flush();
    }
}

创建templates/hello.ftl文件:

<!DOCTYPE html>
<html>
<head>
    <title>欢迎页面</title>
</head>
<body>
    <h1>你好,${name}!欢迎来到FreeMarker的世界!🎉</h1>
</body>
</html>

运行程序,你就会看到生成的HTML!是不是很酷?😎


🎨 第二部分:FreeMarker基础语法详解

2.1 插值(Interpolation)- ${...}

插值是FreeMarker最基础的功能,用于在模板中输出变量的值。

基本用法

<h1>欢迎 ${username}!</h1>
<p>你的邮箱是:${email}</p>
<p>今天是:${today}</p>

对应的Java代码:

Map<String, Object> data = new HashMap<>();
data.put("username", "张三");
data.put("email", "zhangsan@example.com");
data.put("today", "2024-10-03");

输出结果

<h1>欢迎 张三!</h1>
<p>你的邮箱是:zhangsan@example.com</p>
<p>今天是:2024-10-03</p>

🚨 重要注意事项:${} vs #{}

  • ${expression} - 用于文本插值(最常用)

  • #{expression} - 用于数值插值(主要用于数字格式化)

示例

<p>价格:${price}</p>           <!-- 输出:100 -->
<p>格式化价格:#{price; m2M2}</p>  <!-- 输出:100.00 -->

2.2 遍历List集合 - <#list>指令

遍历是模板引擎的核心功能之一!💡

基础遍历

Java代码

List<String> fruits = Arrays.asList("苹果🍎", "香蕉🍌", "橙子🍊", "西瓜🍉");
dataModel.put("fruits", fruits);

FTL模板

<h2>水果清单</h2>
<ul>
<#list fruits as fruit>
    <li>${fruit}</li>
</#list>
</ul>

输出

<h2>水果清单</h2>
<ul>
    <li>苹果🍎</li>
    <li>香蕉🍌</li>
    <li>橙子🍊</li>
    <li>西瓜🍉</li>
</ul>

遍历对象列表(实战重点!)

这是最常用的场景!比如遍历用户列表、商品列表等。

Java Bean

public class User {
    private String name;
    private Integer age;
    private String email;
    
    // 构造方法、getter、setter省略
}

Java代码

List<User> users = new ArrayList<>();
users.add(new User("张三", 25, "zhangsan@example.com"));
users.add(new User("李四", 30, "lisi@example.com"));
users.add(new User("王五", 28, "wangwu@example.com"));
dataModel.put("users", users);

FTL模板

<h2>用户列表</h2>
<table border="1">
    <thead>
        <tr>
            <th>序号</th>
            <th>姓名</th>
            <th>年龄</th>
            <th>邮箱</th>
        </tr>
    </thead>
    <tbody>
    <#list users as user>
        <tr>
            <td>${user_index + 1}</td>  <!-- user_index 从0开始 -->
            <td>${user.name}</td>
            <td>${user.age}</td>
            <td>${user.email}</td>
        </tr>
    </#list>
    </tbody>
</table>

🎯 遍历中的特殊变量

<#list>指令中,FreeMarker提供了很多内置变量:

<#list items as item>
    ${item_index}     <!-- 当前索引(从0开始) -->
    ${item_has_next}  <!-- 是否有下一个元素(true/false) -->
    ${item_is_first}  <!-- 是否是第一个元素 -->
    ${item_is_last}   <!-- 是否是最后一个元素 -->
</#list>

实战应用 - 添加奇偶行样式

<#list users as user>
    <tr class="<#if user_index % 2 == 0>even<#else>odd</#if>">
        <td>${user.name}</td>
    </tr>
</#list>

空列表处理

<#list products as product>
    <p>${product.name} - ¥${product.price}</p>
<#else>
    <p>暂无商品数据!😔</p>
</#list>

products为空或不存在时,会显示"暂无商品数据"。


2.3 获取Map中的数据

Map是Java中最常用的数据结构,FreeMarker提供了多种访问方式!

方式一:点号访问(推荐)

Map<String, Object> config = new HashMap<>();
config.put("siteName", "我的博客");
config.put("siteUrl", "https://myblog.com");
config.put("author", "小明");
dataModel.put("config", config);
<h1>${config.siteName}</h1>
<p>网址:${config.siteUrl}</p>
<p>作者:${config.author}</p>

方式二:方括号访问

当key包含特殊字符或动态key时使用:

${config["site-name"]}        <!-- key包含连字符 -->
${config["site.url"]}         <!-- key包含点号 -->
${config[dynamicKey]}         <!-- 动态key -->

遍历Map

Map<String, Integer> scores = new HashMap<>();
scores.put("语文", 95);
scores.put("数学", 88);
scores.put("英语", 92);
dataModel.put("scores", scores);
<h3>成绩单</h3>
<ul>
<#list scores?keys as subject>
    <li>${subject}:${scores[subject]}分</li>
</#list>
</ul>

嵌套Map访问

Map<String, Object> user = new HashMap<>();
Map<String, String> address = new HashMap<>();
address.put("province", "广东省");
address.put("city", "深圳市");
user.put("name", "张三");
user.put("address", address);
dataModel.put("user", user);
<p>姓名:${user.name}</p>
<p>地址:${user.address.province} ${user.address.city}</p>

2.4 if指令 - 条件判断

基础if语句

<#if age >= 18>
    <p>你已经成年了!🎉</p>
</#if>

if-else

<#if score >= 60>
    <p style="color: green;">恭喜你,考试通过!✅</p>
<#else>
    <p style="color: red;">很遗憾,需要补考!😢</p>
</#if>

if-elseif-else(多条件判断)

<#if score >= 90>
    <span class="grade-a">优秀 🌟</span>
<#elseif score >= 80>
    <span class="grade-b">良好 👍</span>
<#elseif score >= 60>
    <span class="grade-c">及格 ✅</span>
<#else>
    <span class="grade-d">不及格 ❌</span>
</#if>

复杂条件组合

<#if (age >= 18) && (hasLicense == true)>
    <p>你可以开车!🚗</p>
</#if>

<#if (role == "admin") || (role == "moderator")>
    <button>删除</button>
</#if>

2.5 运算符大全 📊

比较运算符

FreeMarker提供了两套比较运算符:

符号形式

字母形式

含义

示例

==

=

等于

<#if x == 5>

!=

-

不等于

<#if x != 5>

<

lt

小于

<#if x < 5><#if x lt 5>

>

gt

大于

<#if x > 5><#if x gt 5>

<=

lte

小于等于

<#if x <= 5><#if x lte 5>

>=

gte

大于等于

<#if x >= 5><#if x gte 5>

🚨 特别重要:<> 的转义问题!

这是FreeMarker新手最容易踩的坑!❗❗❗

问题原因:在HTML/XML环境中,<> 是特殊字符,可能被误认为是标签。

解决方案有三种

方案1:使用字母形式(强烈推荐!)👍

<#if age gt 18>        <!-- 推荐 -->
    <p>成年人</p>
</#if>

<#if price lt 100>     <!-- 推荐 -->
    <p>便宜</p>
</#if>

方案2:使用括号包裹

<#if (age > 18)>       <!-- 可行 -->
    <p>成年人</p>
</#if>

方案3:使用转义字符

<#if age &gt; 18>      <!-- 不推荐,影响可读性 -->
    <p>成年人</p>
</#if>

算术运算符

${x + y}      <!-- 加法 -->
${x - y}      <!-- 减法 -->
${x * y}      <!-- 乘法 -->
${x / y}      <!-- 除法 -->
${x % y}      <!-- 取模 -->

实战示例 - 计算折扣价

<#assign originalPrice = 100>
<#assign discount = 0.8>
<p>原价:¥${originalPrice}</p>
<p>折扣价:¥${originalPrice * discount}</p>

逻辑运算符

${condition1 && condition2}   <!-- 与 -->
${condition1 || condition2}   <!-- 或 -->
${!condition}                 <!-- 非 -->

实战示例 - 会员权限判断

<#if (user.vip == true) && (user.level gte 3)>
    <button>专属功能</button>
</#if>

2.6 空值处理 - 避免空指针异常!

空值处理是模板开发中极其重要的一环!💡

问题场景

dataModel.put("username", null);  // username为null
// 或者根本没有put "email" 这个key

如果直接使用:

<p>${username}</p>   <!-- ❌ 会报错!-->
<p>${email}</p>      <!-- ❌ 会报错!-->

会抛出异常:Expression username is undefined

解决方案1:默认值操作符 !

${username!"游客"}              <!-- 如果为null或不存在,显示"游客" -->
${email!"未设置邮箱"}            <!-- 如果为null或不存在,显示"未设置邮箱" -->
${age!0}                       <!-- 数字类型的默认值 -->

实战示例

<h1>欢迎,${user.nickname!"匿名用户"}!</h1>
<p>积分:${user.points!0} 分</p>
<p>会员等级:${user.vipLevel!"普通会员"}</p>

解决方案2:存在性判断 ??

?? 运算符返回布尔值,判断变量是否存在且不为null。

<#if username??>
    <p>用户名:${username}</p>
<#else>
    <p>用户名未设置</p>
</#if>

组合使用

<#if user?? && user.email??>
    <a href="mailto:${user.email}">发送邮件</a>
</#if>

解决方案3:安全导航 ! 在链式访问中

${user.profile.avatar!"default.jpg"}   <!-- 如果user或profile为null,使用默认值 -->

🎯 最佳实践

<!-- ✅ 推荐写法 -->
<#if items?? && (items?size > 0)>
    <#list items as item>
        <p>${item.name!"未命名"}</p>
    </#list>
<#else>
    <p>暂无数据</p>
</#if>

2.7 内建函数(Built-ins)- 强大的工具箱 🧰

内建函数使用?调用,格式为:${variable?function}

字符串处理函数

<!-- 1. 转大写/小写 -->
${"hello"?upper_case}          <!-- 输出:HELLO -->
${"WORLD"?lower_case}          <!-- 输出:world -->
${"freemarker"?cap_first}      <!-- 输出:Freemarker(首字母大写)-->
${"hello world"?capitalize}    <!-- 输出:Hello World(每个单词首字母大写)-->

<!-- 2. 字符串长度 -->
${"FreeMarker"?length}         <!-- 输出:10 -->

<!-- 3. 字符串裁剪 -->
${"这是一段很长的文字"?substring(0, 5)}  <!-- 输出:这是一段很 -->

<!-- 4. 字符串替换 -->
${"hello world"?replace("world", "FreeMarker")}  <!-- 输出:hello FreeMarker -->

<!-- 5. 字符串分割 -->
<#assign str = "apple,banana,orange">
<#list str?split(",") as fruit>
    <li>${fruit}</li>
</#list>

<!-- 6. 去除空白 -->
${"  hello  "?trim}            <!-- 输出:hello -->

<!-- 7. 判断是否包含 -->
<#if "hello world"?contains("world")>
    <p>包含world!</p>
</#if>

<!-- 8. 判断开头/结尾 -->
<#if filename?ends_with(".jpg")>
    <p>这是一张图片</p>
</#if>

<#if url?starts_with("https")>
    <p>安全连接</p>
</#if>

数字格式化函数

<#assign price = 1234.5678>

<!-- 保留小数位数 -->
${price?string("0.00")}           <!-- 输出:1234.57 -->
${price?string("#.##")}           <!-- 输出:1234.57 -->

<!-- 千分位分隔符 -->
${price?string(",##0.00")}        <!-- 输出:1,234.57 -->

<!-- 百分比 -->
${0.156?string.percent}           <!-- 输出:15.6% -->

<!-- 货币格式 -->
${price?string.currency}          <!-- 输出:$1,234.57(根据locale) -->

<!-- 四舍五入 -->
${price?round}                    <!-- 输出:1235 -->
${price?floor}                    <!-- 向下取整:1234 -->
${price?ceiling}                  <!-- 向上取整:1235 -->

日期格式化函数

<#assign now = .now>  <!-- 获取当前时间 -->

${now?string("yyyy-MM-dd")}              <!-- 2024-10-03 -->
${now?string("yyyy-MM-dd HH:mm:ss")}     <!-- 2024-10-03 14:30:00 -->
${now?string("yyyy年MM月dd日")}          <!-- 2024年10月03日 -->

<!-- 单独获取年月日 -->
${now?string.year}      <!-- 2024 -->
${now?string.month}     <!-- 10 -->
${now?string.day}       <!-- 3 -->

集合处理函数

<#assign nums = [1, 2, 3, 4, 5]>

<!-- 集合大小 -->
${nums?size}                         <!-- 输出:5 -->

<!-- 第一个/最后一个元素 -->
${nums?first}                        <!-- 输出:1 -->
${nums?last}                         <!-- 输出:5 -->

<!-- 序列反转 -->
<#list nums?reverse as n>
    ${n}  <!-- 输出:5 4 3 2 1 -->
</#list>

<!-- 序列排序 -->
<#assign fruits = ["orange", "apple", "banana"]>
<#list fruits?sort as fruit>
    ${fruit}  <!-- apple, banana, orange -->
</#list>

<!-- 集合连接成字符串 -->
${fruits?join(", ")}                 <!-- 输出:orange, apple, banana -->

<!-- 序列切片 -->
<#list nums?chunk(2) as chunk>
    ${chunk?join("-")}  <!-- 1-2, 3-4, 5 -->
</#list>

HTML/XML转义函数

<#assign userInput = "<script>alert('XSS')</script>">

<!-- HTML转义(防止XSS攻击!)-->
${userInput?html}                    
<!-- 输出:&lt;script&gt;alert('XSS')&lt;/script&gt; -->

<!-- URL编码 -->
<#assign query = "搜索关键词">
<a href="/search?q=${query?url}">搜索</a>
<!-- 输出:/search?q=%E6%90%9C%E7%B4%A2%E5%85%B3%E9%94%AE%E8%AF%8D -->

<!-- JavaScript转义 -->
<script>
    var message = "${message?js_string}";
</script>

JSON格式化

<#assign user = {"name": "张三", "age": 25}>
<script>
    var userData = ${user?json};  
    // 输出:{"name":"张三","age":25}
</script>

🛠️ 第三部分:实战应用场景

3.1 Web开发场景 - 动态页面生成

场景1:博客文章列表页

Java Controller

@Controller
public class BlogController {
    
    @Autowired
    private Configuration freemarkerConfig;
    
    @GetMapping("/blog/list")
    public void getBlogList(HttpServletResponse response) throws Exception {
        // 1. 查询文章列表
        List<Article> articles = articleService.getRecentArticles();
        
        // 2. 准备数据模型
        Map<String, Object> model = new HashMap<>();
        model.put("articles", articles);
        model.put("siteName", "技术博客");
        model.put("currentYear", LocalDate.now().getYear());
        
        // 3. 加载模板并渲染
        Template template = freemarkerConfig.getTemplate("blog-list.ftl");
        response.setContentType("text/html;charset=UTF-8");
        template.process(model, response.getWriter());
    }
}

FTL模板(blog-list.ftl)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>${siteName} - 文章列表</title>
    <style>
        .article-card {
            border: 1px solid #ddd;
            padding: 20px;
            margin: 10px 0;
            border-radius: 8px;
        }
        .article-meta {
            color: #666;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <header>
        <h1>${siteName}</h1>
        <nav>
            <a href="/">首页</a>
            <a href="/about">关于</a>
        </nav>
    </header>
    
    <main>
        <h2>最新文章</h2>
        
        <#if articles?? && (articles?size gt 0)>
            <#list articles as article>
                <div class="article-card">
                    <h3>
                        <a href="/blog/${article.id}">${article.title}</a>
                    </h3>
                    <div class="article-meta">
                        作者:${article.author!"匿名"} | 
                        发布时间:${article.publishTime?string("yyyy-MM-dd")} |
                        阅读量:${article.views!0}
                    </div>
                    <p>${article.summary?html}</p>
                    
                    <!-- 标签显示 -->
                    <#if article.tags??>
                        <div class="tags">
                            <#list article.tags as tag>
                                <span class="tag">#${tag}</span>
                            </#list>
                        </div>
                    </#if>
                </div>
            </#list>
            
            <!-- 分页 -->
            <div class="pagination">
                <#if currentPage gt 1>
                    <a href="?page=${currentPage - 1}">上一页</a>
                </#if>
                <span>第 ${currentPage} / ${totalPages} 页</span>
                <#if currentPage lt totalPages>
                    <a href="?page=${currentPage + 1}">下一页</a>
                </#if>
            </div>
        <#else>
            <p>暂无文章,敬请期待!📝</p>
        </#if>
    </main>
    
    <footer>
        <p>&copy; ${currentYear} ${siteName}. All rights reserved.</p>
    </footer>
</body>
</html>

3.2 邮件模板场景

场景2:订单确认邮件

Java Service

@Service
public class EmailService {
    
    @Autowired
    private Configuration freemarkerConfig;
    
    @Autowired
    private JavaMailSender mailSender;
    
    public void sendOrderConfirmation(Order order) throws Exception {
        // 1. 准备邮件数据
        Map<String, Object> model = new HashMap<>();
        model.put("order", order);
        model.put("customer", order.getCustomer());
        model.put("items", order.getItems());
        model.put("totalAmount", order.getTotalAmount());
        
        // 2. 渲染邮件模板
        Template template = freemarkerConfig.getTemplate("email/order-confirmation.ftl");
        StringWriter writer = new StringWriter();
        template.process(model, writer);
        String emailContent = writer.toString();
        
        // 3. 发送邮件
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
        helper.setTo(order.getCustomer().getEmail());
        helper.setSubject("订单确认 - 订单号: " + order.getOrderNo());
        helper.setText(emailContent, true);  // true表示HTML格式
        mailSender.send(message);
    }
}

FTL模板(email/order-confirmation.ftl)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }
        .content { padding: 20px; background: #f9f9f9; }
        table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
        .total { font-size: 18px; font-weight: bold; color: #4CAF50; }
        .footer { text-align: center; color: #666; padding: 20px; font-size: 12px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>✅ 订单确认</h1>
        </div>
        
        <div class="content">
            <p>尊敬的 ${customer.name},您好!</p>
            <p>感谢您的购买!您的订

3.3 代码生成场景 - MyBatis代码生成器

场景3:自动生成Entity、Mapper、Service代码

Java代码生成器

public class CodeGenerator {
    
    public void generateCode(TableInfo tableInfo) throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setClassForTemplateLoading(this.getClass(), "/templates/codegen");
        cfg.setDefaultEncoding("UTF-8");
        
        Map<String, Object> model = new HashMap<>();
        model.put("table", tableInfo);
        model.put("package", "com.example.project");
        model.put("author", "CodeGenerator");
        model.put("date", LocalDate.now().toString());
        
        // 生成Entity
        generateFile(cfg, "entity.ftl", model, 
            "entity/" + tableInfo.getClassName() + ".java");
        
        // 生成Mapper
        generateFile(cfg, "mapper.ftl", model, 
            "mapper/" + tableInfo.getClassName() + "Mapper.java");
        
        // 生成Service
        generateFile(cfg, "service.ftl", model, 
            "service/" + tableInfo.getClassName() + "Service.java");
        
        System.out.println("✅ 代码生成完成!");
    }
    
    private void generateFile(Configuration cfg, String templateName, 
                            Map<String, Object> model, String outputPath) 
                            throws Exception {
        Template template = cfg.getTemplate(templateName);
        File outputFile = new File("src/main/java/" + outputPath);
        outputFile.getParentFile().mkdirs();
        
        try (Writer out = new FileWriter(outputFile)) {
            template.process(model, out);
        }
    }
}

Entity模板(entity.ftl)

package ${package}.entity;

import lombok.Data;
import java.io.Serializable;
<#if table.hasDateField>
import java.util.Date;
</#if>
<#if table.hasBigDecimalField>
import java.math.BigDecimal;
</#if>

/**
 * ${table.tableComment!table.tableName}实体类
 *
 * @author ${author}
 * @date ${date}
 */
@Data
public class ${table.className} implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
<#list table.columns as column>
    /**
     * ${column.columnComment!column.columnName}
     */
    <#if column.isPrimaryKey>
    @TableId(type = IdType.AUTO)
    </#if>
    private ${column.javaType} ${column.fieldName};
    
</#list>
}

Mapper模板(mapper.ftl)

package ${package}.mapper;

import ${package}.entity.${table.className};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * ${table.tableComment!table.tableName}Mapper接口
 *
 * @author ${author}
 * @date ${date}
 */
@Mapper
public interface ${table.className}Mapper extends BaseMapper<${table.className}> {

}

3.4 静态网站生成 - 博客静态化

场景4:将动态博客生成为静态HTML

静态页面生成器

@Service
public class StaticPageGenerator {
    
    @Autowired
    private Configuration freemarkerConfig;
    
    @Autowired
    private ArticleService articleService;
    
    /**
     * 生成所有文章的静态HTML
     */
    public void generateAllArticles() throws Exception {
        List<Article> articles = articleService.getAllArticles();
        
        for (Article article : articles) {
            generateArticlePage(article);
        }
        
        // 生成首页
        generateIndexPage(articles);
        
        System.out.println("✅ 生成了 " + articles.size() + " 个静态页面!");
    }
    
    /**
     * 生成单个文章页面
     */
    private void generateArticlePage(Article article) throws Exception {
        Map<String, Object> model = new HashMap<>();
        model.put("article", article);
        model.put("relatedArticles", articleService.getRelatedArticles(article.getId()));
        
        Template template = freemarkerConfig.getTemplate("article-detail.ftl");
        
        // 输出到静态文件目录
        File outputFile = new File("static/blog/" + article.getId() + ".html");
        outputFile.getParentFile().mkdirs();
        
        try (Writer out = new FileWriter(outputFile)) {
            template.process(model, out);
        }
    }
    
    /**
     * 生成首页
     */
    private void generateIndexPage(List<Article> articles) throws Exception {
        Map<String, Object> model = new HashMap<>();
        model.put("articles", articles);
        model.put("siteName", "我的技术博客");
        
        Template template = freemarkerConfig.getTemplate("index.ftl");
        File outputFile = new File("static/index.html");
        
        try (Writer out = new FileWriter(outputFile)) {
            template.process(model, out);
        }
    }
}

文章详情模板(article-detail.ftl)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${article.title} - 技术博客</title>
    <meta name="description" content="${article.summary?html}">
    <meta name="keywords" content="${article.tags?join(",")}">
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <header>
        <nav>
            <a href="/index.html">首页</a>
            <a href="/about.html">关于</a>
        </nav>
    </header>
    
    <article class="post">
        <h1>${article.title}</h1>
        
        <div class="post-meta">
            <span>📅 ${article.publishTime?string("yyyy-MM-dd")}</span>
            <span>👤 ${article.author!"管理员"}</span>
            <span>👁️ ${article.views!0} 次阅读</span>
        </div>
        
        <#if article.coverImage??>
            <img src="${article.coverImage}" alt="${article.title}" class="cover-image">
        </#if>
        
        <div class="post-content">
            ${article.content}
        </div>
        
        <#if article.tags?? && (article.tags?size gt 0)>
            <div class="tags">
                <strong>标签:</strong>
                <#list article.tags as tag>
                    <a href="/tag/${tag?url}.html" class="tag">${tag}</a>
                </#list>
            </div>
        </#if>
    </article>
    
    <#if relatedArticles?? && (relatedArticles?size gt 0)>
        <section class="related-posts">
            <h2>相关文章</h2>
            <ul>
            <#list relatedArticles as related>
                <li>
                    <a href="${related.id}.html">${related.title}</a>
                    <span>${related.publishTime?string("yyyy-MM-dd")}</span>
                </li>
            </#list>
            </ul>
        </section>
    </#if>
    
    <footer>
        <p>&copy; ${.now?string("yyyy")} 技术博客. 由FreeMarker静态生成。</p>
    </footer>
</body>
</html>

🎓 第四部分:高级技巧与最佳实践

4.1 宏(Macro)- 可复用的代码片段

宏类似于函数,可以定义可复用的模板片段:

<#-- 定义一个按钮宏 -->
<#macro button text color="blue" size="normal">
    <button class="btn btn-${color} btn-${size}">
        ${text}
    </button>
</#macro>

<#-- 使用宏 -->
<@button text="提交" color="green" size="large" />
<@button text="取消" color="red" />
<@button text="确定" />  <#-- 使用默认参数 -->

实用宏示例 - 分页组件

<#macro pagination currentPage totalPages baseUrl>
    <div class="pagination">
        <#if (currentPage gt 1)>
            <a href="${baseUrl}?page=1">首页</a>
            <a href="${baseUrl}?page=${currentPage - 1}">上一页</a>
        </#if>
        
        <#list 1..totalPages as page>
            <#if (page == currentPage)>
                <span class="current">${page}</span>
            <#else>
                <a href="${baseUrl}?page=${page}">${page}</a>
            </#if>
        </#list>
        
        <#if (currentPage lt totalPages)>
            <a href="${baseUrl}?page=${currentPage + 1}">下一页</a>
            <a href="${baseUrl}?page=${totalPages}">尾页</a>
        </#if>
    </div>
</#macro>

<#-- 使用分页宏 -->
<@pagination currentPage=3 totalPages=10 baseUrl="/articles" />

4.2 包含(Include)- 模板复用

<#-- 在主模板中包含其他模板 -->
<!DOCTYPE html>
<html>
<head>
    <title>我的网站</title>
</head>
<body>
    <#include "header.ftl">
    
    <main>
        <!-- 主要内容 -->
    </main>
    
    <#include "footer.ftl">
</body>
</html>

4.3 命名空间 - 避免变量冲突

<#import "common/utils.ftl" as utils>

${utils.formatDate(date)}
${utils.formatPrice(price)}

⚠️ 第五部分:常见坑点与注意事项

5.1 必须知道的重要坑点!

❌ 坑点1:比较运算符在HTML中的问题

<!-- ❌ 错误写法 -->
<#if age > 18>  <#-- 可能被解析为标签 -->

<!-- ✅ 正确写法 -->
<#if age gt 18>
<#if (age > 18)>

❌ 坑点2:空值未处理导致异常

<!-- ❌ 危险写法 -->
${user.email}  <#-- 如果email为null会报错 -->

<!-- ✅ 安全写法 -->
${user.email!"未设置"}
<#if user.email??>
    ${user.email}
</#if>

❌ 坑点3:忘记HTML转义导致XSS

<!-- ❌ 危险写法 -->
<p>${userInput}</p>

<!-- ✅ 安全写法 -->
<p>${userInput?html}</p>

❌ 坑点4:日期对象未格式化

<!-- ❌ 错误写法 -->
${createTime}  <#-- 会显示Java对象toString -->

<!-- ✅ 正确写法 -->
${createTime?string("yyyy-MM-dd HH:mm:ss")}

❌ 坑点5:集合遍历时修改原数据

FreeMarker的模板是只读的,不应该在模板中修改数据!


📋 第六部分:完整配置示例

Spring Boot集成配置

@Configuration
public class FreeMarkerConfig {
    
    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates/");
        configurer.setDefaultEncoding("UTF-8");
        
        Properties settings = new Properties();
        settings.setProperty("number_format", "#.##");
        settings.setProperty("date_format", "yyyy-MM-dd");
        settings.setProperty("time_format", "HH:mm:ss");
        settings.setProperty("datetime_format", "yyyy-MM-dd HH:mm:ss");
        settings.setProperty("classic_compatible", "true");  // 空值处理
        settings.setProperty("whitespace_stripping", "true"); // 去除多余空白
        configurer.setFreemarkerSettings(settings);
        
        return configurer;
    }
}

🎯 结束

让我们回顾一下你现在掌握的技能:

基础语法:插值、指令、注释
数据操作:遍历List、访问Map、条件判断
运算符:比较、算术、逻辑运算,特别是<>的坑点
空值处理!默认值、??存在性判断
内建函数:字符串、数字、日期、集合处理
实战场景:Web开发、邮件模板、代码生成、静态网站
高级技巧:宏、包含、命名空间
安全实践:HTML转义、XSS防护

💡 学习建议

  1. 动手实践:理论知识只是开始,多写代码才能真正掌握!

  2. 阅读官方文档:本文涵盖了90%的常用功能,但还有更多高级特性等你探索

  3. 关注安全:始终记得HTML转义,防止XSS攻击

  4. 保持模板简洁:复杂逻辑应该在Java代码中处理,模板只负责展示

记住:好的模板引擎使用,能让你的代码更优雅、维护更简单! 💪

如果在使用过程中遇到问题,不要慌张,查阅文档、调试代码、多实践,你一定能成为FreeMarker高手!


0

评论区