将 FreeMarker 模板引擎集成到 Spring Boot 应用中是构建动态 Web 应用的常见选择。Spring Boot 提供了强大的自动配置功能,极大地简化了集成过程。本指南将深入探讨 Spring Boot 集成 FreeMarker 的方方面面,涵盖核心概念、详细操作步骤、常见错误、注意事项、使用技巧以及最佳实践与性能优化。
1. 核心概念
spring-boot-starter-freemarker
: Spring Boot 提供的启动器依赖,它包含了freemarker
库以及 Spring 对 FreeMarker 的集成支持(spring-context-support
)。添加此依赖是集成的第一步。- 自动配置 (
FreeMarkerAutoConfiguration
): Spring Boot 的核心特性。当检测到freemarker
类在类路径上时,FreeMarkerAutoConfiguration
会自动配置一个FreeMarkerConfigurer
和一个FreeMarkerViewResolver
。 FreeMarkerConfigurer
: Spring 的FactoryBean
,负责创建和配置 FreeMarker 的核心Configuration
对象。它读取application.properties
/application.yml
中的配置,并应用 Spring 特定的设置(如模板加载路径、默认编码、共享变量等)。FreeMarkerViewResolver
: Spring MVC 的ViewResolver
实现,负责将控制器返回的逻辑视图名称(如"home"
)解析为实际的 FreeMarker 模板文件(如classpath:/templates/home.ftl
),并创建FreeMarkerView
来渲染视图。FreeMarkerView
: Spring MVC 的View
实现,它使用FreeMarkerConfigurer
提供的Configuration
来获取Template
对象,并将数据模型(Model)传递给 FreeMarker 引擎进行渲染。@Controller
: Spring MVC 注解,标记一个类为 Web 控制器,处理 HTTP 请求。Model
/ModelMap
/ModelAndView
: Spring MVC 用于在控制器和视图之间传递数据的接口/类。控制器将数据放入Model
,FreeMarker 视图通过${variableName}
访问这些数据。- 模板位置 (
spring.freemarker.template-loader-path
): 配置 FreeMarker 从哪里加载模板文件。默认是classpath:/templates/
。 - 模板后缀 (
spring.freemarker.suffix
): 配置模板文件的后缀。默认是.ftl
。控制器返回的视图名"home"
会被解析为home.ftl
。 - 共享变量 (Shared Variables): 在
Configuration
级别定义的变量,可以在所有模板中直接访问,无需在每个Model
中重复添加。常用于全局配置、工具函数、常量等。 @Bean
配置: 可以通过定义@Bean
方法来覆盖或扩展自动配置的FreeMarkerConfigurer
或FreeMarkerViewResolver
,实现更精细的控制。
2. 操作步骤 (非常详细)
以下是 Spring Boot 集成 FreeMarker 的详细步骤:
步骤 1: 添加依赖 (Maven)
在 pom.xml
文件中添加 spring-boot-starter-freemarker
依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ... 其他配置 ... -->
<dependencies>
<!-- Spring Boot Web Starter (包含 Spring MVC) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot FreeMarker Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- Lombok (可选,用于简化 POJO) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring Boot Test Starter (可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- ... 其他配置 ... -->
</project>
步骤 2: 配置 FreeMarker (可选,使用默认值也可工作)
在 src/main/resources/application.yml
(或 application.properties
) 中配置 FreeMarker。
# application.yml
spring:
# FreeMarker 配置
freemarker:
# 模板文件的加载路径 (classpath: 前缀可省略,但建议保留)
template-loader-path: classpath:/templates/
# 模板文件的后缀
suffix: .ftl
# 内容类型 (MIME type)
content-type: text/html
# 默认编码
charset: UTF-8
# 是否启用缓存 (生产环境建议开启)
cache: true
# 模板更新延迟 (毫秒) - 0 表示每次检查 (开发), 正数表示延迟检查 (生产)
# 开发环境设为 0,生产环境设为 60000 (1分钟) 或更高
# 注意: cache: true 时,此设置才生效
template-update-delay: 0ms # 开发环境
# template-update-delay: 60000ms # 生产环境
# 设置日志异常处理器 (便于调试)
# template-exception-handler: rethrow
# template-exception-handler: html_debug # 开发环境友好
# 设置模板的默认编码
settings:
# 这里可以设置 FreeMarker Configuration 的原生属性
# 例如,设置 number_format
number_format: '0.##'
# boolean_format (true/false -> yes/no)
# boolean_format: yes,no
# locale (影响日期/数字格式化)
# locale: zh_CN
# 额外的设置 (等同于 settings)
# settings:
# classic_compatible: true
# Web 服务器配置 (可选)
# web:
# resources:
# static-locations: classpath:/static/,classpath:/public/ # 静态资源路径
# 自定义配置示例 (可选)
app:
title: "My Spring Boot App"
version: "1.0.0"
application.properties
等效配置:
# application.properties
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.content-type=text/html
spring.freemarker.charset=UTF-8
spring.freemarker.cache=true
# 开发环境
spring.freemarker.template-update-delay=0ms
# 生产环境
# spring.freemarker.template-update-delay=60000ms
# FreeMarker 原生设置
spring.freemarker.settings.number_format=0.##
# spring.freemarker.settings.boolean_format=yes,no
# spring.freemarker.settings.locale=zh_CN
app.title=My Spring Boot App
app.version=1.0.0
步骤 3: 创建数据模型 (POJO)
创建用于在控制器和视图之间传递数据的 Java 类。
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import lombok.Data;
@Data // Lombok 注解,生成 getter, setter, toString, equals, hashCode
public class User {
private Long id;
private String username;
private String email;
private String fullName;
}
步骤 4: 创建控制器 (@Controller
)
创建处理 HTTP 请求并返回视图名称的控制器。
// src/main/java/com/example/demo/controller/HomeController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@Controller // 标记为 Spring MVC 控制器
public class HomeController {
/**
* 处理根路径 "/" 的 GET 请求
* @param model 用于向视图传递数据
* @return 逻辑视图名称 "home"
*/
@GetMapping("/")
public String home(Model model) {
// 添加单个对象到模型
User currentUser = new User();
currentUser.setId(1L);
currentUser.setUsername("john_doe");
currentUser.setEmail("john.doe@example.com");
currentUser.setFullName("John Doe");
model.addAttribute("user", currentUser);
// 添加列表到模型
List<User> recentUsers = Arrays.asList(
new User() {{ setId(2L); setUsername("jane_smith"); setFullName("Jane Smith"); }},
new User() {{ setId(3L); setUsername("bob_johnson"); setFullName("Bob Johnson"); }}
);
model.addAttribute("recentUsers", recentUsers);
// 添加基本类型和日期
model.addAttribute("pageTitle", "Welcome Page");
model.addAttribute("currentDate", new Date());
// 返回逻辑视图名,FreeMarkerViewResolver 会解析为 templates/home.ftl
return "home";
}
/**
* 处理 "/about" 路径的 GET 请求
*/
@GetMapping("/about")
public String about(Model model) {
model.addAttribute("title", "About Us");
// 返回 templates/about.ftl
return "about";
}
}
步骤 5: 创建 FreeMarker 模板
在 src/main/resources/templates/
目录下创建 .ftl
文件。
创建基础布局模板 (
templates/layout/base.ftl
):<#-- templates/layout/base.ftl --> <#macro layout title="Default Title" extraHead=""> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${title} - ${appTitle!}</title> <!-- Bootstrap CSS (假设已下载或使用 CDN) --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- 页面特定的额外 head 内容 --> ${extraHead!} </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <div class="container"> <a class="navbar-brand" href="/">My App</a> <div class="navbar-nav"> <a class="nav-link" href="/">Home</a> <a class="nav-link" href="/about">About</a> </div> </div> </nav> <main class="container mt-4"> <#-- 主要内容区域 --> <#nested> </main> <footer class="bg-dark text-white text-center py-3 mt-5"> © ${.now?string("yyyy")} ${appTitle!} v${appVersion!}. All rights reserved. </footer> <!-- Bootstrap JS (可选) --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> </body> </html> </#macro>
创建首页模板 (
templates/home.ftl
):<#-- 导入基础布局宏 --> <#import "layout/base.ftl" as layout> <#-- 使用 app 配置 (来自 application.yml) --> <#assign appTitle = app.title> <#assign appVersion = app.version> <@layout.layout title=pageTitle extraHead=""> <h1 class="mb-4">${pageTitle}</h1> <div class="row"> <div class="col-md-6"> <div class="card"> <div class="card-body"> <h5 class="card-title">Current User</h5> <p><strong>ID:</strong> ${user.id}</p> <p><strong>Username:</strong> ${user.username}</p> <p><strong>Email:</strong> ${user.email}</p> <p><strong>Full Name:</strong> ${user.fullName}</p> <p><strong>Joined:</strong> ${currentDate?string.medium} at ${currentDate?string.time}</p> </div> </div> </div> <div class="col-md-6"> <div class="card"> <div class="card-body"> <h5 class="card-title">Recent Users</h5> <ul class="list-group"> <#list recentUsers as u> <li class="list-group-item d-flex justify-content-between align-items-center"> ${u.fullName} (<em>${u.username}</em>) <span class="badge bg-primary rounded-pill">${u.id}</span> </li> </#list> </ul> </div> </div> </div> </div> </</@layout.layout>
创建关于页模板 (
templates/about.ftl
):<#import "layout/base.ftl" as layout> <#assign appTitle = app.title> <@layout.layout title=title> <h1>About Us</h1> <p>This is the about page of <strong>${appTitle!}</strong>.</p> <p>We are building awesome applications with Spring Boot and FreeMarker!</p> </</@layout.layout>
步骤 6: 配置共享变量 (可选但推荐)
将应用配置等全局数据作为共享变量注入 Configuration
,使其在所有模板中可用。
// src/main/java/com/example/demo/config/FreeMarkerConfig.java
package com.example.demo.config;
import freemarker.template.TemplateException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class FreeMarkerConfig {
@Value("${app.title}")
private String appTitle;
@Value("${app.version}")
private String appVersion;
/**
* 配置 FreeMarkerConfigurer,可以覆盖自动配置或添加共享变量
* @return FreeMarkerConfigurer
*/
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
// 可以在这里设置 templateLoaderPath, defaultEncoding 等
// configurer.setTemplateLoaderPath("classpath:/my-templates/");
// configurer.setDefaultEncoding("UTF-8");
// 添加共享变量 (可在所有模板中直接使用,如 ${appTitle})
Map<String, Object> sharedVars = new HashMap<>();
sharedVars.put("appTitle", appTitle);
sharedVars.put("appVersion", appVersion);
// 可以添加工具类实例 (需要确保线程安全)
// sharedVars.put("dateUtil", new DateUtil());
configurer.setFreemarkerVariables(sharedVars);
// 可以设置 FreeMarker 原生 Configuration 的 Settings
// Map<String, Object> settings = new HashMap<>();
// settings.put("number_format", "0.##");
// configurer.setFreemarkerSettings(settings);
return configurer;
}
// 注意: 通常不需要手动配置 FreeMarkerViewResolver,自动配置已足够。
// 如果需要自定义 (如 viewNames, order),可以定义此 Bean
/*
@Bean
public FreeMarkerViewResolver freeMarkerViewResolver() {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setPrefix(""); // 通常与 template-loader-path 结合使用
resolver.setSuffix(".ftl");
resolver.setContentType("text/html; charset=UTF-8");
resolver.setCache(true);
resolver.setOrder(1); // 视图解析器顺序
return resolver;
}
*/
}
注意: 在 FreeMarkerConfig
类中,我们通过 @Value
注解读取 application.yml
中的 app.title
和 app.version
,然后在 freeMarkerConfigurer()
方法中将它们作为共享变量 (freemarkerVariables
) 添加。这样,在任何 .ftl
模板中都可以直接使用 ${appTitle}
和 ${appVersion}
,无需在每个控制器的 Model
中重复添加。
步骤 7: 启动应用并测试
- 确保
DemoApplication.java
(主类) 位于正确的包下并有@SpringBootApplication
。// src/main/java/com/example/demo/DemoApplication.java package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
- 运行
DemoApplication.main()
方法启动应用。 - 打开浏览器,访问
http://localhost:8080/
,应看到首页。 - 访问
http://localhost:8080/about
,应看到关于页。
3. 常见错误
Whitelabel Error Page
/Template not found
:- 原因: FreeMarker 模板文件不存在或路径错误。控制器返回的视图名
"home"
无法解析为templates/home.ftl
。 - 解决: 检查
templates/
目录下是否存在home.ftl
文件。检查spring.freemarker.template-loader-path
配置是否正确。确认文件后缀是否匹配spring.freemarker.suffix
。
- 原因: FreeMarker 模板文件不存在或路径错误。控制器返回的视图名
ClassNotFound
/NoClassDefFoundError
for FreeMarker classes:- 原因:
spring-boot-starter-freemarker
依赖未正确添加或未下载。 - 解决: 检查
pom.xml
,确保依赖正确。执行mvn clean compile
或刷新 Maven 项目。
- 原因:
共享变量 (
appTitle
) 无法访问:- 原因:
FreeMarkerConfig
类未被 Spring 扫描到(包路径问题)。@Configuration
注解缺失。@Value
注解的属性名与application.yml
不匹配。 - 解决: 确保
FreeMarkerConfig.java
在主应用类 (DemoApplication
) 的包或其子包内。检查@Configuration
和@Value
注解。检查application.yml
中的属性名拼写。
- 原因:
Model
中的数据在模板中显示为null
或未定义:- 原因: 控制器中
model.addAttribute("key", value)
的key
与模板中${key}
的名称不匹配。传递的对象属性名错误。 - 解决: 仔细核对键名。使用 Lombok 的
@Data
确保 getter 方法存在。在模板中使用??
操作符检查变量是否存在:<#if user??>${user.username}</#if>
。
- 原因: 控制器中
中文乱码:
- 原因: 编码配置不一致。
spring.freemarker.charset
、spring.freemarker.settings.output_encoding
、HTTP 响应头、HTML<meta charset>
、文件本身编码不统一。 - 解决: 确保
application.yml
中spring.freemarker.charset=UTF-8
。确保.ftl
文件本身以 UTF-8 编码保存(IDE 设置)。检查FreeMarkerViewResolver
的contentType
是否包含charset=UTF-8
(自动配置通常已设置)。
- 原因: 编码配置不一致。
@Bean freeMarkerConfigurer()
未生效:- 原因: Spring Boot 的自动配置
FreeMarkerAutoConfiguration
可能会覆盖你的自定义@Bean
。通常是因为条件不满足或顺序问题。 - 解决: Spring Boot 的自动配置设计为可被用户自定义
@Bean
覆盖。确保你的@Bean
方法名是freeMarkerConfigurer
。如果仍有问题,可以尝试使用@Primary
注解(不推荐,除非必要)或检查是否有其他配置冲突。
- 原因: Spring Boot 的自动配置
4. 注意事项
- 自动配置优先: Spring Boot 的自动配置非常强大。在大多数情况下,只需添加依赖和基本配置即可工作。优先使用
application.yml
配置,而不是过早地编写自定义@Bean
。 @Controller
vs@RestController
: 使用@Controller
返回视图名称。@RestController
是@Controller
+@ResponseBody
,用于返回 JSON/XML 数据,不适用于返回 FreeMarker 模板。- 模板路径:
spring.freemarker.template-loader-path
默认是classpath:/templates/
。路径必须以/
结尾。#import
和#include
中的相对路径是相对于当前模板文件的。 - 共享变量线程安全: 注入到
Configuration
的共享变量(如工具类实例)必须是线程安全的,因为Configuration
是单例,会被多个请求线程共享。 - 开发环境热重载: 结合
spring-boot-devtools
,修改.ftl
文件后应用会自动重启(或使用 LiveReload),修改会立即生效。确保spring.freemarker.cache=false
或template-update-delay=0
。 - 静态资源: CSS, JS, 图片等静态资源放在
src/main/resources/static/
目录下,可通过/
直接访问(如/css/style.css
)。 FreeMarkerViewResolver
顺序: 如果应用中存在多个ViewResolver
(如 Thymeleaf),可以通过setOrder()
设置优先级。数字越小,优先级越高。
5. 使用技巧
利用
@Value
注解: 在控制器或服务中,使用@Value("${property.name}")
直接注入配置属性,减少Model
中的重复添加。@Controller public class MyController { @Value("${app.title}") private String appTitle; // 注入配置 @GetMapping("/page") public String page(Model model) { model.addAttribute("appTitle", appTitle); // 仍需手动添加到 Model // ... 其他逻辑 return "page"; } }
注意:这仍需手动添加到
Model
。共享变量是更好的全局方案。创建自定义宏库: 将常用的 UI 组件(按钮、表单、卡片)定义为宏,放在
templates/macros/
目录下,并在FreeMarkerConfig
中作为共享变量导入或在基础布局中统一#import
。使用
#attempt
/#recover
: 在模板中处理可能出错的操作(如访问可能为null
的对象属性)。<#attempt> <p>User's bio: ${user.bio?html}</p> <#recover> <p>User has no bio.</p> </#attempt>
集成 Spring Security (可选): 如果使用
spring-boot-starter-security
,可以集成thymeleaf-extras-springsecurity6
的 FreeMarker 等效库(可能需要自定义标签或使用其他方式处理权限)。单元测试: 使用
@WebMvcTest
注解测试控制器。@WebMvcTest(HomeController.class) class HomeControllerTest { @Autowired private MockMvc mockMvc; @Test void shouldReturnHomePage() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(view().name("home")) .andExpect(model().attributeExists("user", "recentUsers")); } }
6. 最佳实践与性能优化
最佳实践:
- 遵循 MVC 模式: 控制器处理请求和业务逻辑,准备数据模型;模板负责展示。
- 模板逻辑最小化: 避免在模板中进行复杂计算或业务逻辑。在控制器或服务层处理好数据,传递给模板的是“准备好”的数据。
- 使用共享变量: 将全局配置、常量、线程安全的工具函数作为共享变量注入,避免在每个控制器中重复添加。
- 模块化模板: 使用
#import
、#include
、#macro
实现模板分离与模块化(参考前一指南)。 - 清晰的命名: 视图名、模型属性名、宏名、文件名都应清晰、一致。
- 配置文件化: 将 FreeMarker 配置(如
template-update-delay
)放在application-{profile}.yml
中,实现不同环境(dev/prod)的差异化配置。 - 利用 IDE 支持: 使用支持 FreeMarker 的 IDE(如 IntelliJ IDEA Ultimate)获得语法高亮、自动补全和错误检查。
性能优化:
- 启用缓存 (
cache: true
): 生产环境中务必设置spring.freemarker.cache=true
和一个合理的template-update-delay
(如60000ms
),让 FreeMarker 的模板缓存充分发挥作用。 - 优化
template-update-delay
: 在生产环境,设置一个足够长的延迟(如 5-15 分钟),最大化缓存命中率,避免频繁的文件检查 I/O。 - 预热缓存 (可选): 在应用启动后,可以主动访问几个关键页面,使常用模板提前加载到缓存中。
- 避免在模板中进行昂贵操作: 不要在循环内进行数据库查询模拟或复杂计算。
- JVM 调优: 确保 JVM 有足够的堆内存,选择合适的垃圾回收器。
- 监控: 监控应用的响应时间、模板处理时间、内存使用情况。
- 启用缓存 (
通过遵循这些步骤和最佳实践,你可以高效地在 Spring Boot 应用中集成并使用 FreeMarker,构建出功能强大且易于维护的 Web 应用。