📖 引言
如果你是一名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提供了两套比较运算符:
🚨 特别重要:<
和 >
的转义问题!
这是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 > 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}
<!-- 输出:<script>alert('XSS')</script> -->
<!-- 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>© ${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>© ${.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防护
💡 学习建议
动手实践:理论知识只是开始,多写代码才能真正掌握!
阅读官方文档:本文涵盖了90%的常用功能,但还有更多高级特性等你探索
关注安全:始终记得HTML转义,防止XSS攻击
保持模板简洁:复杂逻辑应该在Java代码中处理,模板只负责展示
记住:好的模板引擎使用,能让你的代码更优雅、维护更简单! 💪
如果在使用过程中遇到问题,不要慌张,查阅文档、调试代码、多实践,你一定能成为FreeMarker高手!
评论区