一、FreeMarker简介与Spring Boot整合基础
1.1 FreeMarker是什么?
FreeMarker是一款基于Java的模板引擎,主要用于生成HTML Web页面(特别是MVC模式中的视图层),也可以用于生成源代码、配置文件、电子邮件等文本输出。
专业解释:FreeMarker是一个模板引擎,采用"模板+数据模型=输出"的工作方式。它不依赖于Servlet或HTML/Web,可以用于任何文本生成的场景。
通俗理解:想象FreeMarker就像一个"填空大师",你给它一个模板(填空题的题目)和一些数据(填空题的答案),它就能帮你把答案填到正确的位置,生成完整的文档。
1.2 FreeMarker核心优势
特性 | 说明 | 对比其他模板引擎 |
---|---|---|
轻量级 | 不依赖Servlet容器,可以独立使用 | 比Velocity更轻量 |
强大表达式 | 支持复杂表达式和逻辑处理 | 比Thymeleaf表达式更简洁 |
静态文本优势 | 对静态文本处理效率高 | 比JSP处理静态内容更高效 |
模板继承 | 支持宏定义和模板继承 | 功能比JSP的include更强大 |
国际化支持 | 内置国际化支持 | 与Thymeleaf国际化能力相当 |
1.3 Spring Boot整合FreeMarker
1.3.1 添加依赖
在pom.xml
中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
1.3.2 基础配置
Spring Boot会自动配置FreeMarker,但我们可以自定义一些属性。在application.properties
中添加:
# FreeMarker配置
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.cache=false # 开发时关闭缓存,生产环境应开启
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
spring.freemarker.suffix=.ftl
1.3.3 第一个FreeMarker示例
- 创建Controller:
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("name", "Spring Boot");
model.addAttribute("now", new Date());
return "hello"; // 对应src/main/resources/templates/hello.ftl
}
}
- 创建模板文件
src/main/resources/templates/hello.ftl
:
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Hello, ${name}!</h1>
<p>Current time: ${now?datetime}</p>
</body>
</html>
代码解析:
${name}
:显示控制器传递的name变量${now?datetime}
:显示日期时间格式化的now变量return "hello"
:Spring会自动查找hello.ftl文件
二、FreeMarker基础语法详解
2.1 变量与表达式
2.1.1 基本变量输出
<!-- 输出普通变量 -->
<p>用户名: ${username}</p>
<!-- 输出对象属性 -->
<p>用户年龄: ${user.age}</p>
<!-- 输出集合元素 -->
<p>第一个爱好: ${hobbies[0]}</p>
2.1.2 变量处理指令
指令 | 说明 | 示例 |
---|---|---|
?html | HTML转义 | ${content?html} |
?cap_first | 首字母大写 | ${word?cap_first} |
?lower_case | 转为小写 | ${word?lower_case} |
?upper_case | 转为大写 | ${word?upper_case} |
?length | 获取长度 | ${list?length} |
?size | 获取大小 | ${map?size} |
?default | 默认值 | ${name?default(“匿名”)} |
2.1.3 表达式示例
<!-- 算术运算 -->
<p>总价: ${price * quantity}</p>
<!-- 比较运算 -->
<#if age gt 18>
<p>成年人</p>
</#if>
<!-- 逻辑运算 -->
<#if isStudent && (age lt 22)>
<p>大学生</p>
</#if>
<!-- 三目运算 -->
<p>${isMember?string("会员", "非会员")}</p>
2.2 常用指令
2.2.1 if-else指令
<#if temperature < 0>
<p>今天很冷,记得穿羽绒服</p>
<#elseif temperature < 15>
<p>天气凉爽,建议穿外套</p>
<#else>
<p>天气暖和,穿短袖即可</p>
</#if>
2.2.2 list指令
<h3>购物清单</h3>
<ul>
<#list items as item>
<li>${item.name} - ¥${item.price}</li>
<#if item?is_last>
<li>----------</li>
</#if>
</#list>
</ul>
2.2.3 include指令
<!-- 包含头部 -->
<#include "header.ftl">
<!-- 主要内容 -->
<div class="content">
...
</div>
<!-- 包含尾部 -->
<#include "footer.ftl">
2.3 内置函数与日期处理
2.3.1 常用内置函数
<!-- 字符串处理 -->
<p>${"hello"?upper_case}</p> <!-- 输出: HELLO -->
<p>${"hello,world"?split(",")[1]}</p> <!-- 输出: world -->
<!-- 数字处理 -->
<p>${123.456?string["0.##"]}</p> <!-- 输出: 123.46 -->
<p>${1234?string["#,###"]}</p> <!-- 输出: 1,234 -->
<!-- 布尔值处理 -->
<p>${true?string("是", "否")}</p> <!-- 输出: 是 -->
2.3.2 日期时间处理
<!-- 基本日期输出 -->
<p>当前日期: ${now?date}</p>
<p>当前时间: ${now?time}</p>
<p>日期时间: ${now?datetime}</p>
<!-- 日期格式化 -->
<p>自定义格式: ${now?string("yyyy-MM-dd HH:mm:ss")}</p>
<!-- 日期运算 -->
<#assign nextWeek = now + 7.days>
<p>下周今天: ${nextWeek?date}</p>
三、Spring Boot与FreeMarker高级整合
3.1 自动配置原理
Spring Boot对FreeMarker的自动配置主要在FreeMarkerAutoConfiguration
类中完成。关键配置点:
- 模板加载路径:默认
classpath:/templates/
- 文件后缀:默认
.ftl
- 视图解析器:自动配置
FreeMarkerViewResolver
- 配置属性:通过
FreeMarkerProperties
绑定spring.freemarker
前缀的属性
3.2 自定义FreeMarker配置
如果需要更复杂的配置,可以创建FreeMarkerConfigurer
Bean:
@Configuration
public class FreeMarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/");
Properties settings = new Properties();
settings.setProperty("datetime_format", "yyyy-MM-dd HH:mm:ss");
settings.setProperty("number_format", "0.##");
configurer.setFreemarkerSettings(settings);
Map<String, Object> variables = new HashMap<>();
variables.put("appName", "我的应用");
variables.put("version", "1.0.0");
configurer.setFreemarkerVariables(variables);
return configurer;
}
}
3.3 静态资源与模板布局
3.3.1 静态资源处理
在Spring Boot中,静态资源默认放在以下位置:
classpath:/static/
classpath:/public/
classpath:/resources/
在FreeMarker模板中引用静态资源:
<!-- 引用CSS -->
<link href="/css/style.css" rel="stylesheet">
<!-- 引用JS -->
<script src="/js/app.js"></script>
<!-- 引用图片 -->
<img src="/images/logo.png" alt="Logo">
3.3.2 模板布局方案
方案一:使用include指令
<!-- layout.ftl -->
<!DOCTYPE html>
<html>
<head>
<title><#block "title">默认标题</#block></title>
<#include "head.ftl">
</head>
<body>
<#include "header.ftl">
<div class="content">
<#block "content"></#block>
</div>
<#include "footer.ftl">
</body>
</html>
<!-- page.ftl -->
<#include "layout.ftl">
<#block "title">
页面标题
</#block>
<#block "content">
<h1>页面内容</h1>
<p>这里是具体内容...</p>
</#block>
方案二:使用宏(macro)
<!-- macros.ftl -->
<#macro page title="">
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
</head>
<body>
<#nested>
</body>
</html>
</#macro>
<!-- 使用宏 -->
<#import "macros.ftl" as m>
<@m.page title="用户页面">
<h1>用户信息</h1>
<p>用户名: ${user.name}</p>
</@m.page>
3.4 表单处理与数据绑定
3.4.1 表单提交示例
Controller:
@Controller
public class UserController {
@GetMapping("/user/form")
public String showForm(Model model) {
model.addAttribute("user", new User());
return "user-form";
}
@PostMapping("/user/save")
public String saveUser(@ModelAttribute User user) {
// 保存用户逻辑
return "redirect:/user/list";
}
}
模板user-form.ftl
:
<form action="/user/save" method="post">
<input type="hidden" name="id" value="${user.id!}">
<div>
<label>用户名:</label>
<input type="text" name="name" value="${user.name!}">
</div>
<div>
<label>年龄:</label>
<input type="number" name="age" value="${user.age!}">
</div>
<div>
<label>邮箱:</label>
<input type="email" name="email" value="${user.email!}">
</div>
<button type="submit">保存</button>
</form>
3.4.2 表单验证与错误显示
Controller:
@PostMapping("/user/save")
public String saveUser(@Valid @ModelAttribute User user, BindingResult result) {
if (result.hasErrors()) {
return "user-form";
}
// 保存逻辑
return "redirect:/user/list";
}
模板中添加错误显示:
<div>
<label>用户名:</label>
<input type="text" name="name" value="${user.name!}">
<#if springMacroRequestContext.getFieldErrors("name")??>
<div class="error">
${springMacroRequestContext.getFieldErrors("name")[0]}
</div>
</#if>
</div>
四、FreeMarker高级特性
4.1 宏(Macro)详解
宏是FreeMarker中的可重用代码块,类似于函数。
4.1.1 基本宏定义与使用
<!-- 定义宏 -->
<#macro greet name>
<p>Hello, ${name}!</p>
</#macro>
<!-- 使用宏 -->
<@greet name="Alice"/>
<!-- 输出嵌套内容 -->
<#macro bordered>
<div style="border: 1px solid black; padding: 10px;">
<#nested>
</div>
</#macro>
<@bordered>
<p>这段内容会被边框包围</p>
</@bordered>
4.1.2 带参数的宏
<#macro alert type="info" message>
<div class="alert alert-${type}">
${message}
</div>
</#macro>
<!-- 使用 -->
<@alert type="danger" message="操作失败!"/>
<@alert message="普通提示信息"/>
4.2 命名空间与模板导入
为了避免命名冲突,可以使用命名空间管理宏。
<!-- lib/macros.ftl -->
<#macro copyright year>
<p>Copyright © ${year} My Company. All rights reserved.</p>
</#macro>
<!-- 主模板 -->
<#import "/lib/macros.ftl" as my>
<@my.copyright year=2023/>
4.3 自定义指令与函数
4.3.1 自定义指令
创建自定义指令需要实现TemplateDirectiveModel
接口:
@Component
public class UpperDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment env, Map params,
TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
if (body != null) {
StringWriter writer = new StringWriter();
body.render(writer);
String content = writer.toString().toUpperCase();
env.getOut().write(content);
}
}
}
注册指令:
@Configuration
public class FreeMarkerConfig {
@Autowired
private UpperDirective upperDirective;
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
// 其他配置...
Map<String, Object> sharedVariables = new HashMap<>();
sharedVariables.put("upper", upperDirective);
configurer.setFreemarkerVariables(sharedVariables);
return configurer;
}
}
模板中使用:
<@upper>
这段文字会被转为大写
</@upper>
4.3.2 自定义函数
创建自定义函数需要实现TemplateMethodModelEx
接口:
@Component
public class MultiplyFunction implements TemplateMethodModelEx {
@Override
public Object exec(List args) throws TemplateModelException {
if (args.size() != 2) {
throw new TemplateModelException("需要两个参数");
}
Number num1 = ((SimpleNumber) args.get(0)).getAsNumber();
Number num2 = ((SimpleNumber) args.get(1)).getAsNumber();
return num1.doubleValue() * num2.doubleValue();
}
}
注册并使用:
// 在FreeMarkerConfigurer中注册
sharedVariables.put("multiply", new MultiplyFunction());
模板中使用:
<p>3乘以5等于: ${multiply(3, 5)}</p>
4.4 异常处理与调试
4.4.1 常见FreeMarker异常
异常类型 | 原因 | 解决方案 |
---|---|---|
TemplateNotFoundException | 模板文件不存在 | 检查模板路径和文件名 |
ParseException | 模板语法错误 | 检查模板语法,注意标签闭合 |
InvalidReferenceException | 引用未定义的变量 | 检查变量名或使用默认值 ${var!default} |
TemplateModelException | 类型不匹配或方法调用错误 | 检查变量类型和方法参数 |
4.4.2 调试技巧
-
启用详细错误信息:
spring.freemarker.settings.template_exception_handler=debug
-
在模板中输出调试信息:
<#-- 输出所有可用变量 --> <#list .data_model?keys as key> ${key} = ${.data_model[key]} </#list> <#-- 输出请求属性 --> <#list request?keys as key> ${key} = ${request[key]} </#list>
-
使用
?has_content
检查变量:<#if user?has_content> <p>用户名: ${user.name}</p> <#else> <p>用户不存在</p> </#if>
五、性能优化与最佳实践
5.1 性能优化策略
5.1.1 缓存配置
生产环境应启用模板缓存:
spring.freemarker.cache=true
spring.freemarker.settings.template_update_delay=3600 # 1小时更新检查
5.1.2 模板优化技巧
- 减少嵌套:避免过深的嵌套结构
- 合理使用include:将公共部分提取为单独模板
- 避免复杂计算:将复杂计算移到Java代码中
- 使用静态方法:注册静态工具类减少模板逻辑
// 注册静态工具类
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
Map<String, Object> sharedVariables = new HashMap<>();
sharedVariables.put("StringUtils", new StringUtils());
configurer.setFreemarkerVariables(sharedVariables);
return configurer;
}
模板中使用:
<p>${StringUtils.capitalize(name)}</p>
5.2 安全考虑
5.2.1 XSS防护
<!-- 自动HTML转义 -->
${userInput?html}
<!-- 禁用转义 -->
<#noescape>${trustedHtml}</#noescape>
5.2.2 敏感数据处理
<!-- 信用卡号只显示后四位 -->
<#assign cardNumber = payment.cardNumber>
${cardNumber?substring(0, 2)}****${cardNumber?substring(cardNumber?length-4)}
5.3 最佳实践总结
-
模板组织原则:
- 保持模板简洁,逻辑尽量放在Java代码中
- 使用宏和include重用代码
- 合理使用命名空间避免冲突
-
开发规范:
- 模板文件使用
.ftl
后缀 - 模板目录结构清晰,按功能模块组织
- 公共组件放在
common
或includes
目录
- 模板文件使用
-
性能规范:
- 生产环境必须启用缓存
- 避免在模板中进行大量数据查询
- 复杂计算预先在Java代码中完成
六、实战案例:电商网站商品展示
6.1 需求分析
实现一个电商网站商品列表和详情页,包含:
- 商品分类展示
- 分页功能
- 商品详情
- 用户评论
- 相关推荐
6.2 数据模型设计
public class Product {
private Long id;
private String name;
private String description;
private BigDecimal price;
private String imageUrl;
private List<String> tags;
private Date createTime;
// getters/setters
}
public class Category {
private Long id;
private String name;
private List<Product> products;
// getters/setters
}
public class Review {
private Long id;
private String author;
private String content;
private Integer rating;
private Date createTime;
// getters/setters
}
6.3 控制器实现
@Controller
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/category/{id}")
public String categoryProducts(
@PathVariable Long id,
@RequestParam(defaultValue = "1") int page,
Model model) {
Page<Product> products = productService.findByCategory(id, page);
model.addAttribute("products", products);
model.addAttribute("category", productService.getCategory(id));
return "product/list";
}
@GetMapping("/{id}")
public String productDetail(@PathVariable Long id, Model model) {
Product product = productService.findById(id);
List<Review> reviews = productService.getReviews(id);
List<Product> related = productService.getRelatedProducts(id);
model.addAttribute("product", product);
model.addAttribute("reviews", reviews);
model.addAttribute("related", related);
return "product/detail";
}
}
6.4 模板实现
6.4.1 商品列表模板list.ftl
<#include "../layout.ftl">
<@layout>
<h1>${category.name}分类</h1>
<div class="product-grid">
<#list products.content as product>
<div class="product-card">
<a href="/products/${product.id}">
<img src="${product.imageUrl}" alt="${product.name}">
<h3>${product.name}</h3>
<p>¥${product.price?string["0.00"]}</p>
</a>
</div>
</#list>
</div>
<#-- 分页控件 -->
<div class="pagination">
<#if products.hasPrevious()>
<a href="?page=${products.number}">上一页</a>
</#if>
<#list 1..products.totalPages as p>
<#if p == products.number + 1>
<span class="current">${p}</span>
<#else>
<a href="?page=${p}">${p}</a>
</#if>
</#list>
<#if products.hasNext()>
<a href="?page=${products.number + 2}">下一页</a>
</#if>
</div>
</@layout>
6.4.2 商品详情模板detail.ftl
<#include "../layout.ftl">
<@layout>
<div class="product-detail">
<div class="product-images">
<img src="${product.imageUrl}" alt="${product.name}">
</div>
<div class="product-info">
<h1>${product.name}</h1>
<p class="price">¥${product.price?string["0.00"]}</p>
<p>${product.description}</p>
<div class="tags">
<#list product.tags as tag>
<span class="tag">${tag}</span>
</#list>
</div>
<button class="add-to-cart">加入购物车</button>
</div>
</div>
<div class="product-reviews">
<h2>用户评价</h2>
<#if reviews?has_content>
<#list reviews as review>
<div class="review">
<div class="rating">
<#list 1..5 as i>
<#if i <= review.rating>
★
<#else>
☆
</#if>
</#list>
</div>
<p>${review.content}</p>
<div class="meta">
<span>${review.author}</span>
<span>${review.createTime?date}</span>
</div>
</div>
</#list>
<#else>
<p>暂无评价</p>
</#if>
</div>
<div class="related-products">
<h2>相关推荐</h2>
<div class="related-grid">
<#list related as product>
<div class="related-item">
<a href="/products/${product.id}">
<img src="${product.imageUrl}" alt="${product.name}">
<h3>${product.name}</h3>
<p>¥${product.price?string["0.00"]}</p>
</a>
</div>
</#list>
</div>
</div>
</@layout>
七、FreeMarker与其他模板引擎对比
7.1 主流模板引擎比较
特性 | FreeMarker | Thymeleaf | JSP | Velocity |
---|---|---|---|---|
语法复杂度 | 中等 | 中等 | 简单 | 简单 |
性能 | 高 | 中等 | 高 | 中等 |
与Spring集成 | 优秀 | 优秀 | 良好 | 良好 |
静态原型支持 | 有限 | 优秀 | 无 | 无 |
学习曲线 | 中等 | 较陡 | 平缓 | 平缓 |
功能丰富度 | 丰富 | 非常丰富 | 基础 | 基础 |
适合场景 | 传统Web应用 | 现代Web应用 | 传统JavaEE | 简单应用 |
7.2 选择建议
-
选择FreeMarker:
- 需要高性能模板渲染
- 项目已经使用FreeMarker
- 需要生成非HTML内容(如邮件、报表)
- 团队熟悉FreeMarker语法
-
选择Thymeleaf:
- 需要良好的静态原型支持
- 项目前后端分离程度不高
- 需要更现代的模板特性
- 与Spring生态深度集成
-
选择JSP:
- 维护传统JavaEE项目
- 需要与JSTL标签库集成
- 团队熟悉JSP语法
八、常见问题解答
8.1 FreeMarker常见问题
Q1: 如何判断变量是否存在?
<#if variable??>
<!-- 变量存在 -->
<#else>
<!-- 变量不存在 -->
</#if>
Q2: 如何处理null值?
<!-- 使用默认值 -->
${name!"默认名称"}
<!-- 安全访问对象属性 -->
${user.address.city!}
Q3: 如何遍历Map?
<#list map?keys as key>
${key} = ${map[key]}
</#list>
Q4: 如何格式化数字?
<!-- 保留两位小数 -->
${number?string["0.##"]}
<!-- 千分位分隔 -->
${largeNumber?string["#,###"]}
8.2 Spring Boot集成问题
Q1: 模板修改后不生效?
确保开发环境下关闭了缓存:
spring.freemarker.cache=false
Q2: 静态资源无法加载?
检查静态资源位置是否正确,默认应在:
src/main/resources/static/
src/main/resources/public/
Q3: 如何自定义FreeMarker配置?
创建FreeMarkerConfigurer
Bean并设置各种属性:
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/");
// 其他配置...
return configurer;
}
九、总结与扩展
9.1 关键点回顾
- FreeMarker基础:模板语法、指令、表达式
- Spring Boot集成:自动配置、自定义配置
- 高级特性:宏、命名空间、自定义指令
- 最佳实践:模板组织、性能优化、安全考虑
- 实战应用:电商网站商品展示系统
9.2 扩展学习
- FreeMarker官方文档:https://freemarker.apache.org/docs/
- Spring Boot视图技术:学习Thymeleaf、Mustache等其他模板引擎
- 前端整合:研究FreeMarker与Vue/React等前端框架的整合
- 代码生成:探索使用FreeMarker生成Java代码、配置文件等