Spring Boot整合FreeMarker全面指南:从基础到高级实战

一、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示例

  1. 创建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
    }
}
  1. 创建模板文件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
类中完成。关键配置点:

  1. 模板加载路径:默认classpath:/templates/
  2. 文件后缀:默认.ftl
  3. 视图解析器:自动配置FreeMarkerViewResolver
  4. 配置属性:通过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 (c) ${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 调试技巧

  1. 启用详细错误信息
  2. spring.freemarker.settings.template_exception_handler=debug
  3. 在模板中输出调试信息
  4. <#-- 输出所有可用变量 -->
    <#list .data_model?keys as key>
    ${key} = ${.data_model[key]}
    </#list>

    <#-- 输出请求属性 -->
    <#list request?keys as key>
    ${key} = ${request[key]}
    </#list>
  5. 使用?has_content检查变量
  6. <#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 模板优化技巧

  1. 减少嵌套:避免过深的嵌套结构
  2. 合理使用include:将公共部分提取为单独模板
  3. 避免复杂计算:将复杂计算移到Java代码中
  4. 使用静态方法:注册静态工具类减少模板逻辑
// 注册静态工具类
@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 最佳实践总结

  1. 模板组织原则
  2. 保持模板简洁,逻辑尽量放在Java代码中
  3. 使用宏和include重用代码
  4. 合理使用命名空间避免冲突
  5. 开发规范
  6. 模板文件使用.ftl后缀
  7. 模板目录结构清晰,按功能模块组织
  8. 公共组件放在commonincludes目录
  9. 性能规范
  10. 生产环境必须启用缓存
  11. 避免在模板中进行大量数据查询
  12. 复杂计算预先在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 选择建议

  1. 选择FreeMarker
  2. 需要高性能模板渲染
  3. 项目已经使用FreeMarker
  4. 需要生成非HTML内容(如邮件、报表)
  5. 团队熟悉FreeMarker语法
  6. 选择Thymeleaf
  7. 需要良好的静态原型支持
  8. 项目前后端分离程度不高
  9. 需要更现代的模板特性
  10. 与Spring生态深度集成
  11. 选择JSP
  12. 维护传统JavaEE项目
  13. 需要与JSTL标签库集成
  14. 团队熟悉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 关键点回顾

  1. FreeMarker基础:模板语法、指令、表达式
  2. Spring Boot集成:自动配置、自定义配置
  3. 高级特性:宏、命名空间、自定义指令
  4. 最佳实践:模板组织、性能优化、安全考虑
  5. 实战应用:电商网站商品展示系统

9.2 扩展学习

  1. FreeMarker官方文档:https://freemarker.apache.org/docs/
  2. Spring Boot视图技术:学习Thymeleaf、Mustache等其他模板引擎
  3. 前端整合:研究FreeMarker与Vue/React等前端框架的整合
  4. 代码生成:探索使用FreeMarker生成Java代码、配置文件等


头条对markdown的文章显示不太友好,想了解更多的可以关注微信公众号:“Eric的技术杂货库”,后期会有更多的干货以及资料下载。

原文链接:,转发请注明来源!