1. FreeMarker模板引擎概述
FreeMarker是一款基于Java的模板引擎,它通过分离业务逻辑与展示逻辑,实现了MVC架构中的视图层解耦。我第一次接触FreeMarker是在2012年参与一个电商平台项目时,当时需要动态生成数千种商品详情页。传统JSP方案在应对这种大规模页面渲染时显得力不从心,而FreeMarker以其简洁的语法和高效的渲染性能完美解决了这个问题。
与JSP、Thymeleaf等其他模板技术相比,FreeMarker的核心优势在于:
- 纯文本输出:不局限于HTML,可生成XML、JSON、纯文本甚至源代码
- 逻辑与展示分离:模板中只包含显示逻辑,业务计算完全由Java代码处理
- 零依赖:不强制要求Servlet容器,可在任何Java环境中运行
- 高性能:编译后的模板会缓存,重复渲染时几乎无性能损耗
实际项目中常见误区:很多新手会把业务逻辑写在模板里,这违反了FreeMarker的设计哲学。正确的做法是:所有数据准备都在Java端完成,模板只负责展示。
2. 环境搭建与基础配置
2.1 项目依赖引入
对于Maven项目,在pom.xml中添加:
<dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency>如果是Gradle项目:
implementation 'org.freemarker:freemarker:2.3.31'2.2 核心配置类详解
典型配置示例:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setDirectoryForTemplateLoading(new File("/templates")); cfg.setDefaultEncoding("UTF-8"); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); cfg.setLogTemplateExceptions(false); cfg.setWrapUncheckedExceptions(true);关键配置参数说明:
| 参数 | 推荐值 | 作用 |
|---|---|---|
| templateUpdateDelay | 5 (秒) | 模板文件检查间隔 |
| localizedLookup | false | 禁用本地化查找 |
| booleanFormat | "true,false" | 布尔值格式化 |
| dateFormat | "yyyy-MM-dd" | 日期格式 |
| datetimeFormat | "yyyy-MM-dd HH:mm:ss" | 日期时间格式 |
3. FTL模板语言深度解析
3.1 基本语法结构
一个完整的FTL模板示例:
<#-- 这是注释 --> <!DOCTYPE html> <html> <head> <title>${user.name}的主页</title> </head> <body> <#if user.gender == "male"> 尊敬的先生 <#elseif user.gender == "female"> 尊敬的女士 <#else> 尊敬的客户 </#if> <ul> <#list products as product> <li>${product.name} - ¥${product.price?string("0.00")}</li> </#list> </ul> <#include "footer.ftl"> </body> </html>3.2 数据模型处理技巧
复杂对象处理示例:
Map<String, Object> root = new HashMap<>(); root.put("user", userService.getCurrentUser()); root.put("products", productService.getFeaturedProducts()); // 处理日期 root.put("now", new Date()); // 自定义方法 root.put("highlight", (str) -> "<strong>" + str + "</strong>");模板中使用:
${highlight(product.name)} 最后更新:${now?string("yyyy-MM-dd HH:mm")}3.3 高级指令详解
3.3.1 宏定义与调用
<#macro productCard product> <div class="card"> <img src="${product.imageUrl}" alt="${product.name}"> <h3>${product.name}</h3> <p>价格:${product.price?string("0.00")}</p> <#if product.stock < 10> <p class="warning">库存紧张</p> </#if> </div> </#macro> <@productCard product=featuredProduct />3.3.2 嵌套内容处理
<#macro bordered> <div style="border: 1px solid #ccc"> <#nested> </div> </#macro> <@bordered> 这里的内容会被嵌套在边框div中 </@bordered>4. 实战应用场景
4.1 Web页面渲染
Spring Boot集成配置:
@Configuration public class FreemarkerConfig { @Bean public FreeMarkerConfigurationFactoryBean getFreeMarkerConfiguration() { FreeMarkerConfigurationFactoryBean bean = new FreeMarkerConfigurationFactoryBean(); bean.setTemplateLoaderPath("classpath:/templates"); bean.setDefaultEncoding("UTF-8"); return bean; } }Controller示例:
@Controller public class ProductController { @GetMapping("/product/{id}") public String productDetail(@PathVariable Long id, Model model) { model.addAttribute("product", productService.getById(id)); return "product/detail"; } }4.2 邮件模板生成
邮件模板示例(email_template.ftl):
<#assign currencyFormat = "0.00"> 尊敬的${user.name}: 感谢您的订单 #${order.id},详情如下: <#list order.items as item> ${item.quantity} x ${item.productName} = ¥${item.totalPrice?string(currencyFormat)} </#list> 总计:¥${order.totalAmount?string(currencyFormat)}生成代码:
public String generateOrderEmail(Order order) throws Exception { Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setClassForTemplateLoading(getClass(), "/templates/email"); Map<String, Object> model = new HashMap<>(); model.put("user", order.getUser()); model.put("order", order); Template template = cfg.getTemplate("order_confirmation.ftl"); StringWriter writer = new StringWriter(); template.process(model, writer); return writer.toString(); }4.3 代码生成器实现
数据库表转Java实体类生成器:
package ${packageName}; import java.util.Date; <#if hasBigDecimal> import java.math.BigDecimal; </#if> public class ${className} { <#list fields as field> private ${field.javaType} ${field.name}; </#list> <#list fields as field> public ${field.javaType} get${field.name?cap_first}() { return this.${field.name}; } public void set${field.name?cap_first}(${field.javaType} ${field.name}) { this.${field.name} = ${field.name}; } </#list> }5. 性能优化与最佳实践
5.1 模板缓存策略
// 生产环境推荐配置 cfg.setCacheStorage(new StrongCacheStorage()); cfg.setTemplateUpdateDelay(3600); // 1小时检查一次更新5.2 错误处理方案
全局异常处理器:
cfg.setTemplateExceptionHandler((ex, env, out) -> { if (env.isInAttemptBlock()) { throw ex; } out.write("[渲染错误: " + ex.getMessage() + "]"); log.error("模板渲染错误", ex); });5.3 安全防护措施
防止XSS攻击:
<#-- 不安全的方式 --> ${userInput} <#-- 安全的方式 --> ${userInput?html} <#-- 处理JSON输出 --> ${userInput?json_string}6. 常见问题排查指南
6.1 模板加载失败
典型错误:
Could not resolve template: "template.ftl"解决方案:
- 检查模板路径是否配置正确
- 确认文件扩展名是否为.ftl
- 检查文件读取权限
6.2 变量解析异常
错误示例:
The following has evaluated to null or missing: ==> user.name处理方案:
<#-- 安全访问方式 --> ${user.name!''} ${user.name!'默认值'} ${(user.name)!''} <#-- 条件判断 --> <#if user?? && user.name??> ${user.name} </#if>6.3 日期格式化问题
正确用法:
${someDate?string("yyyy-MM-dd")} ${someDate?datetime} ${someDate?time}时区处理:
cfg.setSQLDateAndTimeTimeZone(TimeZone.getDefault());7. 高级特性探索
7.1 自定义指令开发
实现分页指令:
public class PaginationDirective implements TemplateDirectiveModel { @Override public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) { int current = Integer.parseInt(params.get("current").toString()); int total = Integer.parseInt(params.get("total").toString()); int size = Integer.parseInt(params.get.get("size").toString()); int pages = (int) Math.ceil((double)total / size); StringBuilder html = new StringBuilder(); html.append("<div class=\"pagination\">"); for (int i = 1; i <= pages; i++) { if (i == current) { html.append("<span class=\"current\">").append(i).append("</span>"); } else { html.append("<a href=\"?page=").append(i).append("\">").append(i).append("</a>"); } } html.append("</div>"); env.getOut().write(html.toString()); } }注册指令:
cfg.setSharedVariable("pagination", new PaginationDirective());模板中使用:
<@pagination current=pageNum total=totalCount size=pageSize />7.2 模板继承机制
基础模板(base.ftl):
<!DOCTYPE html> <html> <head> <title><#block title>默认标题</#block></title> <#block head></#block> </head> <body> <#include "header.ftl"> <div class="content"> <#block content> 默认内容 </#block> </div> <#include "footer.ftl"> </body> </html>子模板(home.ftl):
<#import "base.ftl" as layout> <@layout.base> <#block title>首页</#block> <#block head> <link rel="stylesheet" href="/css/home.css"> </#block> <#block content> <h1>欢迎来到首页</h1> <p>当前用户:${user.name}</p> </#block> </@layout.base>7.3 国际化支持
多语言模板方案:
# messages_en.properties welcome=Welcome product.name=Product Name # messages_zh.properties welcome=欢迎 product.name=商品名称Java端配置:
cfg.setLocalizedLookup(true); cfg.setEncoding(Locale.CHINA, "UTF-8"); cfg.setEncoding(Locale.US, "UTF-8");模板中使用:
<#import "/spring.ftl" as spring> <@spring.message "welcome"/> <#-- 带参数的消息 --> <@spring.messageArgs "product.price", [product.price]/>8. 项目实战:电商系统模板设计
8.1 商品详情页架构
目录结构:
templates/ ├── layout/ │ ├── base.ftl │ ├── header.ftl │ └── footer.ftl ├── macro/ │ ├── product.ftl │ └── review.ftl └── product/ ├── detail.ftl └── list.ftl商品详情页核心逻辑:
<#import "../layout/base.ftl" as layout> <#import "../macro/product.ftl" as p> <@layout.base> <#block title>${product.name} - 商品详情</#block> <#block content> <div class="product-detail"> <@p.productGallery images=product.images /> <div class="info"> <h1>${product.name}</h1> <div class="price"> <span class="current">¥${product.price?string("0.00")}</span> <#if product.originalPrice??> <del>¥${product.originalPrice?string("0.00")}</del> </#if> </div> <@p.skuSelector skus=product.skus /> <div class="actions"> <button id="addToCart">加入购物车</button> </div> </div> <@p.productTabs description=product.description attributes=product.attributes reviews=product.reviews /> </div> </#block> </@layout.base>8.2 订单打印模板
PDF生成方案:
public byte[] generateOrderPdf(Order order) throws Exception { // 生成HTML String html = generateOrderHtml(order); // 使用Flying Saucer转换为PDF ITextRenderer renderer = new ITextRenderer(); renderer.setDocumentFromString(html); renderer.layout(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); renderer.createPDF(outputStream); renderer.finishPDF(); return outputStream.toByteArray(); }订单模板关键设计:
<style> .order-header { margin-bottom: 20px; } .table { width: 100%; border-collapse: collapse; } .table th, .table td { border: 1px solid #ddd; padding: 8px; } .text-right { text-align: right; } </style> <div class="order-header"> <h2>订单号:${order.id}</h2> <p>日期:${order.createTime?string("yyyy-MM-dd HH:mm")}</p> </div> <table class="table"> <thead> <tr> <th>商品</th> <th>单价</th> <th>数量</th> <th>小计</th> </tr> </thead> <tbody> <#list order.items as item> <tr> <td>${item.productName}</td> <td>¥${item.unitPrice?string("0.00")}</td> <td>${item.quantity}</td> <td>¥${item.totalPrice?string("0.00")}</td> </tr> </#list> </tbody> <tfoot> <tr> <td colspan="3" class="text-right">总计:</td> <td>¥${order.totalAmount?string("0.00")}</td> </tr> </tfoot> </table>9. 调试与测试方案
9.1 单元测试模板
使用JUnit测试模板渲染:
public class TemplateTest { private Configuration cfg; @Before public void setup() throws IOException { cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setDirectoryForTemplateLoading(new File("src/main/resources/templates")); cfg.setDefaultEncoding("UTF-8"); } @Test public void testProductTemplate() throws Exception { Map<String, Object> model = new HashMap<>(); model.put("product", new Product("iPhone 13", 5999.00)); Template template = cfg.getTemplate("product.ftl"); StringWriter writer = new StringWriter(); template.process(model, writer); String result = writer.toString(); assertTrue(result.contains("iPhone 13")); assertTrue(result.contains("5999.00")); } }9.2 日志调试技巧
启用调试日志(logback.xml配置示例):
<logger name="freemarker" level="DEBUG"/>关键日志信息解读:
TemplateLoader:模板加载路径Creating cache for template:模板缓存记录Starting to process template:模板处理开始Rendered template:渲染完成统计
9.3 在线测试工具
开发阶段快速测试方案:
@RestController public class TemplateTestController { @PostMapping("/api/template/test") public String testTemplate(@RequestBody TemplateTestRequest request) { try { Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setTemplateLoader(new StringTemplateLoader()); ((StringTemplateLoader)cfg.getTemplateLoader()) .putTemplate("test", request.getTemplate()); Template template = cfg.getTemplate("test"); StringWriter writer = new StringWriter(); template.process(request.getModel(), writer); return writer.toString(); } catch (Exception e) { return "Error: " + e.getMessage(); } } }10. 扩展与集成方案
10.1 与Spring Boot深度集成
自动配置扩展:
@Configuration @AutoConfigureAfter(FreeMarkerAutoConfiguration.class) public class FreemarkerExtConfig { @Autowired private freemarker.template.Configuration configuration; @PostConstruct public void init() throws TemplateModelException { // 添加共享变量 configuration.setSharedVariable("appVersion", "1.0.0"); // 注册静态工具类 configuration.setSharedVariable("StringUtils", new StaticModels().getTemplateModel(StringUtils.class)); } }10.2 分布式环境方案
Redis模板缓存实现:
public class RedisTemplateCache implements CacheStorage { private final RedisTemplate<String, Object> redisTemplate; public RedisTemplateCache(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public void put(String key, Object value) { redisTemplate.opsForValue().set("freemarker:" + key, value); } @Override public Object get(String key) { return redisTemplate.opsForValue().get("freemarker:" + key); } @Override public void remove(String key) { redisTemplate.delete("freemarker:" + key); } @Override public void clear() { Set<String> keys = redisTemplate.keys("freemarker:*"); if (keys != null) { redisTemplate.delete(keys); } } }10.3 微服务架构下的模板管理
模板服务设计方案:
@Service public class TemplateService { @Autowired private TemplateRepository templateRepository; private final ConcurrentMap<String, Template> templateCache = new ConcurrentHashMap<>(); public String processTemplate(String templateName, Map<String, Object> model) throws Exception { Template template = templateCache.computeIfAbsent(templateName, k -> { String content = templateRepository.getContent(templateName); return new Template(templateName, content, new Configuration(Configuration.VERSION_2_3_31)); }); StringWriter writer = new StringWriter(); template.process(model, writer); return writer.toString(); } @Scheduled(fixedRate = 60000) public void refreshCache() { templateRepository.getModifiedTemplates().forEach(name -> { templateCache.remove(name); }); } }在实际项目中使用FreeMarker多年后,我发现最关键的实践是保持模板的简洁性。模板应该只关注展示逻辑,所有业务计算都应该在Java端完成。对于复杂的展示逻辑,应该通过自定义指令或宏来实现复用,而不是在每个模板中重复编写相同的代码。另外,良好的模板目录结构和命名规范能显著提高项目的可维护性。