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 变量处理指令
指令说明示例
?htmlHTML转义${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 © ${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. 启用详细错误信息

    spring.freemarker.settings.template_exception_handler=debug
    
  2. 在模板中输出调试信息

    <#-- 输出所有可用变量 -->
    <#list .data_model?keys as key>
        ${key} = ${.data_model[key]}
    </#list>
    
    <#-- 输出请求属性 -->
    <#list request?keys as key>
        ${key} = ${request[key]}
    </#list>
    
  3. 使用?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 模板优化技巧
  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. 模板组织原则

    • 保持模板简洁,逻辑尽量放在Java代码中
    • 使用宏和include重用代码
    • 合理使用命名空间避免冲突
  2. 开发规范

    • 模板文件使用.ftl后缀
    • 模板目录结构清晰,按功能模块组织
    • 公共组件放在commonincludes目录
  3. 性能规范

    • 生产环境必须启用缓存
    • 避免在模板中进行大量数据查询
    • 复杂计算预先在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 主流模板引擎比较

特性FreeMarkerThymeleafJSPVelocity
语法复杂度中等中等简单简单
性能中等中等
与Spring集成优秀优秀良好良好
静态原型支持有限优秀
学习曲线中等较陡平缓平缓
功能丰富度丰富非常丰富基础基础
适合场景传统Web应用现代Web应用传统JavaEE简单应用

7.2 选择建议

  1. 选择FreeMarker

    • 需要高性能模板渲染
    • 项目已经使用FreeMarker
    • 需要生成非HTML内容(如邮件、报表)
    • 团队熟悉FreeMarker语法
  2. 选择Thymeleaf

    • 需要良好的静态原型支持
    • 项目前后端分离程度不高
    • 需要更现代的模板特性
    • 与Spring生态深度集成
  3. 选择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 关键点回顾

  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代码、配置文件等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clf丶忆笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值