news 2026/10/1 8:34:14

FreeMarker实战指南:从模板语法到SpringBoot集成与代码生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FreeMarker实战指南:从模板语法到SpringBoot集成与代码生成

说实话,看到热搜词里的“前端开发者学习后端Java知识计划”“springmv freemarker 转成springboot项目”,我第一反应是:越来越多的人正在从前后端分离体系倒回去接触老牌模板引擎。不少刚入行的后端同学觉得FreeMarker是过时技术,毕竟现在张口闭口都是Vue、React、接口返回JSON,谁还关心服务端渲染?但等你真正接手若依这类快速开发框架,或者被安排去维护一个SpringMVC老项目,甚至要写代码生成器的时候,就会明白FreeMarker依然活在Java后端的血脉里。这篇教程我想用最直接的方式,把FreeMarker从语法到实战串一遍,让后端学习者少走弯路。

FreeMarker不是Web框架,它只是一个模板引擎引擎,负责把模板文件和数据模型拼装成最终文本。这个“职责单一”的定位决定了它既可以用在SpringMVC里渲染HTML页面,也可以脱离Web环境生成Java代码、SQL脚本、邮件正文。正因为这种灵活性,它在后端开发中的位置一直很稳。

  1. 先搞清楚一个前提:模板引擎到底在解决什么问题

很多新手第一次接触FreeMarker时会困惑:明明用String拼接也能生成动态内容,为什么还要多学一套模板语法?其实道理很简单。你把代码里的业务逻辑和展示结构混在一起,改一行展示样式就得重新编译、重新部署;而模板引擎把“长什么样”和“怎么算出来”拆开了,前端模板归模板,后端数据归数据,两边各改各的。

1.1 FreeMarker和后端框架是解耦的

这一点特别关键。FreeMarker本身不依赖Spring,也不依赖Servlet容器。你可以在一个普通的main方法里,new一个Configuration对象,指定模板目录,然后传入一个Map作为数据模型,它就能生成文本。这意味着它不止能生成HTML,还能生成任意类型的纯文本文件。

我见过有人用FreeMarker生成过:

  • 代码生成器里的实体类、Mapper接口、XML映射文件
  • 数据库初始化脚本
  • 静态站点的整站HTML
  • 复杂格式的通知邮件
  • 导出用的CSV或配置文件

相比JSP那种和Servlet容器深度绑定的方案,FreeMarker这种解耦设计简直是后端工具箱里的瑞士军刀。

1.2 前后端分离时代它为什么还没死

你可能会问:现在都是Vue、React搞前后端分离,后端只出JSON,FreeMarker还有存在的必要吗?答案是分场景。

比如你的页面需要SEO,搜索引擎爬虫对JavaScript渲染的页面不友好,服务端渲染就有天然优势。再比如后台管理系统里的代码生成器,它要动态生成Java文件,前端根本参与不了。还有邮件通知这种场景,内容由后端组装,直接跑模板输出文本比前端搞一套渲染流程高效得多。

用一张表简单对比一下常见模板方案。

方案渲染端适用场景学习成本
JSP服务端传统Java Web高,与Servlet耦合
Thymeleaf服务端SpringBoot页面渲染中,标签写法较繁琐
FreeMarker服务端HTML、代码生成、邮件低,语法简洁
Vue/React客户端SPA单页应用高,前端工程化

所以结论很明确:FreeMarker不是被淘汰了,而是它的战场更聚焦了。凡是需要在服务端按模板生成文本的地方,它依然是最顺手的工具之一。

  1. 模板与数据模型:FreeMarker执行的三个核心角色

理解FreeMarker只需要抓住三样东西:模板文件、数据模型、输出过程。模板文件负责描述输出长什么样,数据模型由Java对象组成,输出过程就是把两者合并。整个过程没有任何黑魔法。

2.1 第一个例子:从helloworld看懂模型绑定

我先给一个最朴素的需求:给新注册用户发一封欢迎邮件,内容里要带上用户名、账号和当前积分。

模板文件 welcome.ftl 这样写:

<html> <head> <title>欢迎加入</title> </head> <body> <p>${userName},你好!</p> <p>你的账号 ${account} 已激活,当前积分 ${points}。</p> </body> </html>

后端Java代码这样构造数据模型:

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); cfg.setDefaultEncoding("UTF-8"); cfg.setClassLoaderForTemplateLoading( FreeMarkerDemo.class.getClassLoader(), "/templates" ); Template template = cfg.getTemplate("welcome.ftl"); Map<String, Object> dataModel = new HashMap<>(); dataModel.put("userName", "张三"); dataModel.put("account", "zhangsan"); dataModel.put("points", 1200); StringWriter writer = new StringWriter(); template.process(dataModel, writer); System.out.println(writer.toString());

这就是一个完整的FreeMarker独立运行流程。Configuration负责全局配置,Template代表加载后的模板对象,dataModel里的key对应模板中的变量名。输出时如果模板里引用了dataModel中不存在的变量,默认会直接抛异常。

2.2 数据模型支持哪些Java类型

FreeMarker的数据模型和Java对象的映射非常宽松,它自己定义了一套类型体系来兼容各种Java对象。常见的有这么几种:

  • 字符串对应String
  • 数字对应Integer、Long、BigDecimal等
  • 布尔值对应Boolean
  • 日期对应Date及其子类
  • 序列对应List、数组
  • 哈希对应Map、JavaBean

这意味着你几乎可以把任何业务对象直接扔进数据模型。比如有个User对象,你直接把user对象作为value放进去,模板里用${user.name}就能取到属性值。FreeMarker底层通过反射访问JavaBean的getter方法,所以你的User类必须有对应的getter,这一点很容易被忽略。

2.3 模板语法里的两类元素

模板文件里混合了两种内容:普通文本和FTL指令。普通文本会原样输出,FTL指令负责逻辑处理。指令分成两种形式。

插值表达式用${}表示,比如${userName},作用是计算表达式的值并输出到当前位置。FTL标签则用<#...>表示,比如<#if>、<#list>,相当于后端代码里的控制语句。注释的写法是<#-- 注释内容 -->,注意FreeMarker的注释不会输出到最终结果里,这一点和HTML注释不同。

写出一个直观的对照:

普通文本:原样输出 ${expr}:输出表达式的计算结果 <#directive>:执行指令 <#-- 注释 -->:不输出
  1. 把模板写成程序:指令、内建函数与宏的实战组合

模板里能不能写复杂逻辑?答案是可以,但我建议保持克制。FreeMarker提供了足够强的指令和内建函数,让你在模板层完成展示逻辑的编排。如果逻辑过于复杂,那就应该回退到Java层处理,而不是在模板里堆长表达式。

3.1 条件判断与循环遍历

最常见的控制结构就是if和list。

<#if user.vip> <p>尊贵的VIP用户,欢迎回来!</p> <#else> <p>开通VIP可享受更多权益。</p> </#if>

注意<#if>指令里的表达式直接写对象引用,不需要加${}。${}只用于输出,这一点新手很容易搞混。判断条件里可以写==、!=、>、<这些比较运算符,也可以写&&、||、!逻辑运算。

循环遍历列表:

<#list productList as product> <div>${product.name} - ${product.price}元</div> </#list>

productList是数据模型里的List对象,product是循环变量,在<#list>和</#list>之间可以任意使用。如果要输出序号,可以用product_index,这是FreeMarker内置的循环索引,从0开始。

如果productList可能为null,建议写成:

<#list productList![] as product>

加上![]的意思是:如果productList为null,就当成空列表处理,避免抛异常。

3.2 空值处理是模板开发的重灾区

FreeMarker默认对空值很敏感,访问一个不存在的变量,或者一个值为null的属性,都会直接抛异常。这个设计是为了尽早暴露问题,但实际开发中你不可能保证每个数据都有值。

所以空值处理语法必须熟练掌握。

${user.name!} <!-- 如果user.name为null,输出空字符串 --> ${user.name!"游客"} <!-- 如果user.name为null,输出"游客" --> ${user.name??} <!-- 返回true或false,用于判断是否存在 -->

三个符号要记牢:!是默认值,??是存在性判断,?是调用内建函数。尤其注意${}里如果只写${user.name}而user为null,会直接抛“undefined”错误,这个坑我后面专门讲。

3.3 内建函数让模板具备处理能力

内建函数是FreeMarker语法里最强大的部分,写法是在变量名后面加?,然后跟函数名。常用的有这么几个。

字符串处理:

${name?upper_case} <!-- 转大写 --> ${name?lower_case} <!-- 转小写 --> ${name?trim} <!-- 去除两端空格 --> ${name?substring(0, 3)} <!-- 截取子串 --> ${name?replace("a", "b")} <!-- 替换 -->

数字格式化:

${price?string("0.00")} <!-- 保留两位小数 --> ${count?string("#,##0")} <!-- 千分位格式化 -->

集合操作:

${list?size} <!-- 集合大小 --> ${list?join(", ")} <!-- 用逗号拼接元素 --> ${list?first} <!-- 第一个元素 -->

日期格式化:

${createTime?date} <!-- 只输出日期 --> ${createTime?time} <!-- 只输出时间 --> ${createTime?datetime} <!-- 输出日期和时间 --> ${createTime?string("yyyy-MM-dd HH:mm:ss")} <!-- 自定义格式 -->

3.4 宏定义:模板里的函数

如果同一段模板结构要在多个地方复用,可以用宏。宏相当于模板层面的函数,可以接收参数,输出公共片段。

<#macro pageHeader title> <head> <meta charset="UTF-8"> <title>${title}</title> </head> </#macro>

调用方式:

<@pageHeader title="用户管理" />

宏内部还能使用<#nested>指令嵌入调用者的内容,类似Vue里的slot插槽。比如做一个卡片组件:

<#macro card title> <div class="card"> <div class="card-title">${title}</div> <div class="card-body"> <#nested /> </div> </div> </#macro>

调用时:

<@card title="统计概览"> <p>今日新增用户:${todayCount}</p> </@card>

这个能力对管理后台类的页面非常实用。把公共的布局、卡片、分页条抽成宏,模板会干净很多。

3.5 引入与复用:include和import

include指令可以把另一个模板文件包含进来,相当于把文件内容就地展开。

<#include "/common/header.ftl">

import指令则是把另一个模板里的宏导入到当前命名空间。比如把宏单独放一个文件 macros.ftl,其他地方import之后就能直接用。

<#import "/common/macros.ftl" as ui> <@ui.card title="用户信息"> <p>内容</p> </@ui.card>

import的好处是避免不同文件里的宏命名冲突,加了as别名之后,用别名点宏名的方式调用,代码可读性更好。

  1. SpringBoot里接入FreeMarker:老项目迁移和新项目配置的区别

现在web项目里用FreeMarker,主流方式还是通过SpringBoot集成。这部分我把配置步骤和容易踩的坑一起讲清楚。

4.1 从SpringMVC到SpringBoot:迁移的核心差异

很多老项目是SpringMVC + FreeMarker,迁移到SpringBoot时最大的变化是:配置从XML或properties挪到了application.yml,依赖从手动引包变成了starter自动装配。

老项目SpringMVC里通常这样配置FreeMarker,使用spring.freemarker配置前缀,配置项几乎一样:

spring: freemarker: suffix: .ftl content-type: text/html charset: UTF-8 template-loader-path: classpath:/templates/

SpringBoot迁移后,依赖直接加:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>

starter会自动注册FreeMarkerConfigurer和FreeMarkerViewResolver。也就是说你不需要手动创建Configuration对象了,只要往application.yml里写配置,然后Controller返回视图名,SpringBoot就能自动把模板渲染结果返回给浏览器。

4.2 一个完整的Controller渲染流程

SpringBoot + FreeMarker的Controller写法和JSP时代很相似。

@Controller @RequestMapping("/user") public class UserController { @GetMapping("/list") public String list(Model model) { List<User> users = userService.listAll(); model.addAttribute("users", users); return "user/list"; } }

代码里的user/list对应src/main/resources/templates/user/list.ftl,模板里直接使用users变量。

<table> <thead> <tr> <th>ID</th> <th>姓名</th> <th>邮箱</th> </tr> </thead> <tbody> <#list users as user> <tr> <td>${user.id}</td> <td>${user.name}</td> <td>${user.email}</td> </tr> </#list> </tbody> </table>

这里有个新手容易犯的错:Controller返回的视图名不要加.ftl后缀,后缀由配置里的suffix自动拼接。如果你返回的是user/list.ftl,而配置suffix是.ftl,渲染时会去查找user/list.ftl.ftl,直接报错。

4.3 常用配置项与调试模式

application.yml里这几个配置项是项目里最常用的,我逐个解释。

spring: freemarker: suffix: .ftl template-loader-path: classpath:/templates/ charset: UTF-8 content-type: text/html; charset=utf-8 cache: false settings: number_format: 0.########## classic_compatible: true template_exception_handler: rethrow

cache: false是本地开发必须开的配置。因为模板文件是文本文件,默认情况下SpringBoot会缓存模板内容,如果不开关闭缓存,你改完模板刷新页面看不到效果,还得重启应用。生产环境再改回true,否则每次请求都重新加载模板文件,性能损耗很明显。

number_format: 0.########## 这个配置也很关键。FreeMarker默认的数字格式化会把1200输出成1,200,因为底层使用了Java的Locale格式。后端接口返回给页面时,数字带千分位分隔符经常会让人困惑,设置了number_format为0.##########就可以保持数字原样输出。

classic_compatible: true是为了兼容老项目的行为。经典兼容模式下,模板访问不存在的变量时不会直接抛异常,而是输出空字符串。如果从SpringMVC老项目迁移过来,一时半会改不掉模板里的空值隐患,可以先开这个配置过渡,但我建议最终还是要修掉模板里的空值问题,依赖全局配置兜底不是长久之计。

  1. 没人明说但最常用的场景:基于FreeMarker写代码生成器

如果你用过若依这类快速开发框架,会发现一个很普遍的设计:数据库建好表之后,后端代码不用手写,点击生成按钮就能自动创建实体类、Mapper、Service、Controller和前端页面。这个功能的底层核心就是模板引擎,而FreeMarker在其中占了相当大的比例。

5.1 为什么代码生成器偏爱FreeMarker

代码生成器的本质是:读取数据库表结构,把表名、字段名、字段类型、注释等信息组装成数据模型,然后套用预先写好的模板文件,生成对应语言的代码文本。

这个场景里模板可能长这样:

package ${packageName}.entity; import lombok.Data; import java.time.LocalDateTime; @Data public class ${className} { <#list fieldList as field> /** * ${field.comment} */ private ${field.javaType} ${field.fieldName}; </#list> }

Java代码端做的事情是构造这个数据模型并调用模板:

Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); cfg.setDefaultEncoding("UTF-8"); cfg.setClassLoaderForTemplateLoading( CodeGenerator.class.getClassLoader(), "/templates/codegen" ); Template template = cfg.getTemplate("entity.ftl"); Map<String, Object> dataModel = new HashMap<>(); dataModel.put("packageName", "com.example.demo"); dataModel.put("className", "User"); List<Map<String, Object>> fieldList = new ArrayList<>(); Map<String, Object> idField = new HashMap<>(); idField.put("comment", "用户ID"); idField.put("javaType", "Long"); idField.put("fieldName", "id"); fieldList.add(idField); Map<String, Object> nameField = new HashMap<>(); nameField.put("comment", "用户名"); nameField.put("javaType", "String"); nameField.put("fieldName", "name"); fieldList.add(nameField); dataModel.put("fieldList", fieldList); File outputDir = new File("generated-code"); if (!outputDir.exists()) { outputDir.mkdirs(); } try (FileWriter writer = new FileWriter( new File(outputDir, "User.java"))) { template.process(dataModel, writer); }

这段代码就完成了一个最小可用的实体类生成器。你只需把模板按Controller、Service、Mapper等角色各写一份,再封装一下JDBC表结构读取逻辑,就是一个五脏俱全的代码生成器。

5.2 生产级代码生成器的关键细节

参考生成工具后,我发现实际项目里代码生成器要考虑的问题远不止拼字符串那么简单。总结下来有这几个关键点。

数据库类型到Java类型的映射要单独做一张表。MySQL的varchar映射String,bigint映射Long,datetime映射LocalDateTime,decimal映射BigDecimal。不同数据库方言差异很大,这个映射逻辑最好独立维护。

模板文件要支持版本管理。生成的代码不是一次性消耗品,项目后续迭代会不断修改生成的代码,也可能在表结构调整后重新生成。模板如果频繁变动,会重写出和现有代码差异很大的内容。

生成时要处理包名、缩进、注释风格。很多团队对代码规范有严格要求,模板里写死缩进风格会很难维护。我的做法是让模板尽量贴近团队的Java代码规范,生成后再用IDE的格式化功能统一处理。

5.3 代码生成器里FreeMarker相对于其他方案的优势

现在也有一些基于JavaPoet或者直接字符串拼接的实现方案,但碰到复杂模板依然头疼。FreeMarker的优势在于模板文件和Java代码完全分离,让会写模板的人不一定要懂Java,懂业务的人也能直接改模板结构。特别是在生成前端Vue页面这类长文件时,字符串拼接的可读性会迅速恶化,而FreeMarker模板结构一目了然。

  1. 日常开发中反复踩到的坑与排查思路

最后这部分是我最想写的,因为语法看文档就能学会,但有些坑不亲自踩一遍真的很难定位。我挑了六个高频问题,附上排查思路和解决方案。

6.1 模板变量不存在,页面直接500

最经典的问题:从Controller传了一个user对象到模板,模板里写${user.name},结果user对象里没有name这个属性,或者user为null,页面直接抛错。而且错误信息经常是一大串堆栈,新手根本不知道去哪看。

排查思路是先看异常信息里有没有类似“The following has evaluated to null or missing”的语句,这句话后面会跟着具体的表达式,比如user.name。定位到表达式之后,再回到Controller看对应的数据是否真的塞进了Model。

解决办法有两种:一是在模板里加默认值${user.name!"未命名"},二是在Java端保证user不为空。我的建议是能用默认值就用默认值,模板毕竟只是展示层,健壮性比严格报错更重要。

6.2 数字输出带了千分位分隔符

后台页面显示订单金额,数据结构里是10000,页面却显示10,000。很多人第一反应是数据库里的值有问题,实际上这是FreeMarker的数字格式化规则在起作用。

解决办法在前文配置里提到过,设置:

settings: number_format: 0.##########

如果你不想动全局配置,也可以在具体位置用?string("0")强制格式化:

${price?string("0")}

这个坑在金额、ID这类需要在页面原样展示的数字上特别常见。

6.3 日期显示成了时间戳或乱码

模板里直接输出Date对象,往往得到一长串看不懂的数字,或者格式完全不对。因为FreeMarker对Date类型的处理依赖对象的java.util.Date类型,如果后端传的是LocalDateTime,直接输出时会出错。

解决办法是在模板里显式指定格式:

${createTime?string("yyyy-MM-dd HH:mm:ss")}

如果是LocalDateTime类型,建议在Java端先转成Date,或者直接格式化为字符串再放入数据模型。我见过不少团队为了省事,统一在VO/DTO里把日期字段格式化为String,再传给模板,这个方案虽然不算优雅,但确实能少踩很多类型转换的坑。

6.4 修改模板不生效,一直显示旧页面

这个问题十有八九是模板缓存没关。SpringBoot默认在生产模式下会缓存模板内容。如果你在开发环境没设置cache: false,改完.ftl文件刷新页面,看到的还是上一次渲染的结果。

排查思路分两步:先看application.yml里有没有设置cache: false,再看IDE里target目录下有没有生成旧的模板副本。有时候你明明改了src/main/resources/templates下的文件,但编译时target目录里的旧文件没被覆盖,也会导致页面不更新。

6.5 include路径找不到文件

include和import路径写错时,异常信息会提示模板文件不存在。这里要注意路径是相对template-loader-path配置的,比如templates/common/header.ftl,在模板里写:

<#include "/common/header.ftl">

路径最开头的斜杠表示从模板根目录开始算,不加斜杠则是相对于当前模板文件所在目录。这个规则经常被忽略,文件一多就容易混乱。

6.6 模板里的逻辑写得太重,性能下降

有些同事习惯把复杂计算也放进模板,比如在<#list>循环里嵌套调用很多内建函数,甚至用宏做递归。模板引擎毕竟不是编程语言运行时,逻辑太复杂会拖慢渲染速度。

我的建议是:模板只做展示和简单判断,凡是涉及集合过滤、排序、统计的逻辑,一律在Java层算好后再传入模板。这也是FreeMarker的设计初衷,保持模板简单,利于维护也利于性能。

  1. 写在最后的实践建议

FreeMarker这东西,你说它难,语法一天就能看完;你说它简单,真上手做项目又会踩出一堆坑。我见过不少人学完语法就丢到一边,直到接手老项目才被迫捡回来。我的经验是,与其等踩坑再去翻文档,不如主动把它当成一个“通用文本生成工具”来用。

比如你下次需要生成一堆重复的类文件、写定时任务导出报表、甚至生成运维脚本的时候,都可以想一下:这个场景用模板是不是比字符串拼接更优雅?一旦建立起这种思维,FreeMarker就不再是一个单纯的Web页面技术,而是一个能随时拿出来提升效率的工具。

最后说一个我自己常干的蠢事:在模板里忘了写<#-- 注释 -->而是写了HTML注释 ,结果把调试信息泄漏到了生成的文件里。所以模板里的调试信息一定要用FreeMarker注释,千万别用HTML注释。这算是和其他人一样容易忽略的小细节,但真的值得记下来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 8:33:19

初学前端的第一篇笔记

一、准备知识 1.两位先驱&#xff1a;图灵与冯诺依曼 2.计算机由硬件和软件组成&#xff0c;软件分为系统软件与应用软件 3.应用软件又分为两大类&#xff1a; C/S架构&#xff0c;特点&#xff1a;需要安装、偶尔更新、不跨平台、开发更具针对性。 B/S架构&#xff0c;特点&am…

作者头像 李华
网站建设 2026/10/1 8:32:32

打通全渠道收款,首选聚合支付

聚合支付为商户提供一站式收款解决方案&#xff0c;统一整合微信、支付宝、云闪付等主流支付渠道。商户完成接入后&#xff0c;顾客无需受支付方式约束&#xff0c;可随心选择付款渠道&#xff0c;大幅提升收银效率&#xff0c;优化消费者支付体验。支持收银台、API接口两种接入…

作者头像 李华
网站建设 2026/10/1 8:32:08

umi后台管理项目实战:从工程搭建到生产构建

1. 引言本文以 umi 为技术底座&#xff0c;完整讲解一个后台管理项目的实战落地过程&#xff0c;覆盖从基本工程搭建、登录注册、菜单权限配置、页面跳转、用户操作埋点&#xff0c;到开发调试与生产构建的完整链路。通过本文&#xff0c;你可以掌握一套可复用的 umi 后台管理项…

作者头像 李华