什么是 freemarker 模板?
freemarker 是一种功能强大、轻量级的模板引擎,用于在 java 应用中生成动态文本输出(如 html、xml、邮件内容等)。它允许开发者将数据模型与模板文件分离,通过模板语法动态生成内容。freemarker 广泛用于 web 开发、报表生成和自动化文档生成,特别是在 spring boot 项目中与 spring mvc 集成,用于生成动态网页。
核心功能
- 模板与数据分离:模板定义输出格式,数据模型提供动态内容。
- 灵活的语法:支持条件、循环、变量插值等,易于编写动态逻辑。
- 多种输出格式:生成 html、xml、json、文本等。
- 高性能:模板编译和缓存机制,适合高并发场景。
- 与 spring 集成:spring boot 提供 starter,简化配置。
优势
- 简化动态内容生成,减少硬编码。
- 提高开发效率,模板可复用。
- 支持复杂逻辑,适合多样化输出需求。
- 与 spring boot、spring security 等无缝集成。
挑战
- 学习曲线:模板语法需熟悉。
- 调试复杂:动态逻辑可能导致错误难以定位。
- 需与你的查询(如分页、swagger、spring security、activemq、spring profiles、spring batch、热加载、threadlocal、actuator 安全性)集成。
- 安全性:防止模板注入攻击(如 xss)。
在 spring boot 中实现 freemarker 模板
以下是在 spring boot 中使用 freemarker 的简要步骤,结合你的先前查询(分页、swagger、activemq、spring profiles、spring security、spring batch、热加载、threadlocal、actuator 安全性)。完整代码和详细步骤见下文。
1. 环境搭建
添加依赖(pom.xml
):
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-freemarker</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-jpa</artifactid> </dependency> <dependency> <groupid>com.h2database</groupid> <artifactid>h2</artifactid> <scope>runtime</scope> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-activemq</artifactid> </dependency> <dependency> <groupid>org.springdoc</groupid> <artifactid>springdoc-openapi-starter-webmvc-ui</artifactid> <version>2.2.0</version> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-security</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-batch</artifactid> </dependency>
配置 application.yml
:
spring: profiles: active: dev application: name: freemarker-demo datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.driver username: sa password: jpa: hibernate: ddl-auto: update show-sql: true h2: console: enabled: true freemarker: template-loader-path: classpath:/templates/ suffix: .ftl cache: false # 开发环境禁用缓存,支持热加载 activemq: broker-url: tcp://localhost:61616 user: admin password: admin batch: job: enabled: false initialize-schema: always server: port: 8081 management: endpoints: web: exposure: include: health, metrics springdoc: api-docs: path: /api-docs swagger-ui: path: /swagger-ui.html
2. 基本 freemarker 模板
以下示例使用 freemarker 生成用户列表页面。
实体类(user.java
):
package com.example.demo.entity; import jakarta.persistence.entity; import jakarta.persistence.generatedvalue; import jakarta.persistence.generationtype; import jakarta.persistence.id; @entity public class user { @id @generatedvalue(strategy = generationtype.identity) private long id; private string name; private int age; // getters and setters public long getid() { return id; } public void setid(long id) { this.id = id; } public string getname() { return name; } public void setname(string name) { this.name = name; } public int getage() { return age; } public void setage(int age) { this.age = age; } }
repository(userrepository.java
):
package com.example.demo.repository; import com.example.demo.entity.user; import org.springframework.data.jpa.repository.jparepository; import org.springframework.stereotype.repository; @repository public interface userrepository extends jparepository<user, long> { }
创建 freemarker 模板(src/main/resources/templates/users.ftl
):
<!doctype html> <html> <head> <title>用户列表</title> </head> <body> <h1>用户列表</h1> <table border="1"> <tr> <th>id</th> <th>姓名</th> <th>年龄</th> </tr> <#list users as user> <tr> <td>${user.id}</td> <td>${user.name?html}</td> <#-- 防止 xss --> <td>${user.age}</td> </tr> </#list> </table> </body> </html>
控制器(usercontroller.java
):
package com.example.demo.controller; import com.example.demo.entity.user; import com.example.demo.repository.userrepository; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.controller; import org.springframework.ui.model; import org.springframework.web.bind.annotation.getmapping; @controller public class usercontroller { @autowired private userrepository userrepository; @getmapping("/users") public string getusers(model model) { model.addattribute("users", userrepository.findall()); return "users"; // 对应 users.ftl } }
初始化数据(demoapplication.java
):
package com.example.demo; import com.example.demo.entity.user; import com.example.demo.repository.userrepository; import org.springframework.boot.commandlinerunner; import org.springframework.boot.springapplication; import org.springframework.boot.autoconfigure.springbootapplication; import org.springframework.context.annotation.bean; @springbootapplication public class demoapplication { public static void main(string[] args) { springapplication.run(demoapplication.class, args); } @bean commandlinerunner initdata(userrepository userrepository) { return args -> { for (int i = 1; i <= 10; i++) { user user = new user(); user.setname("user" + i); user.setage(20 + i); userrepository.save(user); } }; } }
运行验证:
- 启动应用:
mvn spring-boot:run
。 - 访问
http://localhost:8081/users
,查看用户列表页面。 - 检查 html 输出,确认用户数据显示正确。
3. 与先前查询集成
结合你的查询(分页、swagger、activemq、spring profiles、spring security、spring batch、热加载、threadlocal、actuator 安全性):
分页与排序:
添加分页支持:
package com.example.demo.controller; import com.example.demo.entity.user; import com.example.demo.service.userservice; import org.springframework.beans.factory.annotation.autowired; import org.springframework.data.domain.page; import org.springframework.stereotype.controller; import org.springframework.ui.model; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestparam; @controller public class usercontroller { @autowired private userservice userservice; @getmapping("/users") public string getusers( @requestparam(defaultvalue = "") string name, @requestparam(defaultvalue = "0") int page, @requestparam(defaultvalue = "10") int size, @requestparam(defaultvalue = "id") string sortby, @requestparam(defaultvalue = "asc") string direction, model model) { page<user> userpage = userservice.searchusers(name, page, size, sortby, direction); model.addattribute("users", userpage.getcontent()); model.addattribute("page", userpage); return "users"; } }
package com.example.demo.service; import com.example.demo.entity.user; import com.example.demo.repository.userrepository; import org.springframework.beans.factory.annotation.autowired; import org.springframework.data.domain.page; import org.springframework.data.domain.pagerequest; import org.springframework.data.domain.pageable; import org.springframework.data.domain.sort; import org.springframework.stereotype.service; @service public class userservice { @autowired private userrepository userrepository; public page<user> searchusers(string name, int page, int size, string sortby, string direction) { sort sort = sort.by(sort.direction.fromstring(direction), sortby); pageable pageable = pagerequest.of(page, size, sort); return userrepository.findbynamecontaining(name, pageable); } }
package com.example.demo.repository; import com.example.demo.entity.user; import org.springframework.data.domain.page; import org.springframework.data.domain.pageable; import org.springframework.data.jpa.repository.jparepository; import org.springframework.stereotype.repository; @repository public interface userrepository extends jparepository<user, long> { page<user> findbynamecontaining(string name, pageable pageable); }
更新模板(users.ftl
)支持分页:
<!doctype html> <html> <head> <title>用户列表</title> </head> <body> <h1>用户列表</h1> <form method="get"> <input type="text" name="name" placeholder="搜索姓名" value="${(name!'')}"> <input type="submit" value="搜索"> </form> <table border="1"> <tr> <th>id</th> <th>姓名</th> <th>年龄</th> </tr> <#list users as user> <tr> <td>${user.id}</td> <td>${user.name?html}</td> <td>${user.age}</td> </tr> </#list> </table> <div> <#if page??> <p>第 ${page.number + 1} 页,共 ${page.totalpages} 页</p> <#if page.hasprevious()> <a href="?name=${(name!'')}&page=${page.number - 1}&size=${page.size}&sortby=id&direction=asc" rel="external nofollow" >上一页</a> </#if> <#if page.hasnext()> <a href="?name=${(name!'')}&page=${page.number + 1}&size=${page.size}&sortby=id&direction=asc" rel="external nofollow" >下一页</a> </#if> </#if> </div> </body> </html>
swagger:
为 rest api 添加 swagger 文档:
package com.example.demo.controller; import com.example.demo.entity.user; import com.example.demo.service.userservice; import io.swagger.v3.oas.annotations.operation; import io.swagger.v3.oas.annotations.parameter; import io.swagger.v3.oas.annotations.responses.apiresponse; import io.swagger.v3.oas.annotations.tags.tag; import org.springframework.beans.factory.annotation.autowired; import org.springframework.data.domain.page; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestparam; import org.springframework.web.bind.annotation.restcontroller; @restcontroller @tag(name = "用户管理", description = "用户相关的 api") public class userapicontroller { @autowired private userservice userservice; @operation(summary = "分页查询用户", description = "根据条件分页查询用户列表") @apiresponse(responsecode = "200", description = "成功返回用户分页数据") @getmapping("/api/users") public page<user> searchusers( @parameter(description = "搜索姓名(可选)") @requestparam(defaultvalue = "") string name, @parameter(description = "页码,从 0 开始") @requestparam(defaultvalue = "0") int page, @parameter(description = "每页大小") @requestparam(defaultvalue = "10") int size, @parameter(description = "排序字段") @requestparam(defaultvalue = "id") string sortby, @parameter(description = "排序方向(asc/desc)") @requestparam(defaultvalue = "asc") string direction) { return userservice.searchusers(name, page, size, sortby, direction); } }
activemq:
记录用户查询日志:
package com.example.demo.service; import com.example.demo.entity.user; import com.example.demo.repository.userrepository; import org.springframework.beans.factory.annotation.autowired; import org.springframework.core.env.environment; import org.springframework.data.domain.page; import org.springframework.data.domain.pagerequest; import org.springframework.data.domain.pageable; import org.springframework.data.domain.sort; import org.springframework.jms.core.jmstemplate; import org.springframework.stereotype.service; @service public class userservice { private static final threadlocal<string> context = new threadlocal<>(); @autowired private userrepository userrepository; @autowired private jmstemplate jmstemplate; @autowired private environment environment; public page<user> searchusers(string name, int page, int size, string sortby, string direction) { try { string profile = string.join(",", environment.getactiveprofiles()); context.set("query-" + profile + "-" + thread.currentthread().getname()); sort sort = sort.by(sort.direction.fromstring(direction), sortby); pageable pageable = pagerequest.of(page, size, sort); page<user> result = userrepository.findbynamecontaining(name, pageable); jmstemplate.convertandsend("user-query-log", "queried users: " + name + ", profile: " + profile); return result; } finally { context.remove(); } } }
spring profiles:
配置 application-dev.yml
和 application-prod.yml
:
# application-dev.yml spring: freemarker: cache: false springdoc: swagger-ui: enabled: true logging: level: root: debug
# application-prod.yml spring: freemarker: cache: true datasource: url: jdbc:mysql://prod-db:3306/appdb username: prod_user password: ${db_password} springdoc: swagger-ui: enabled: false logging: level: root: info
spring security:
保护页面和 api:
package com.example.demo.config; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.security.config.annotation.web.builders.httpsecurity; import org.springframework.security.core.userdetails.user; import org.springframework.security.core.userdetails.userdetailsservice; import org.springframework.security.provisioning.inmemoryuserdetailsmanager; import org.springframework.security.web.securityfilterchain; @configuration public class securityconfig { @bean public securityfilterchain securityfilterchain(httpsecurity http) throws exception { http .authorizehttprequests(auth -> auth .requestmatchers("/swagger-ui/**", "/api-docs/**", "/api/users").hasrole("admin") .requestmatchers("/users").authenticated() .requestmatchers("/actuator/health").permitall() .requestmatchers("/actuator/**").hasrole("admin") .anyrequest().permitall() ) .formlogin(); return http.build(); } @bean public userdetailsservice userdetailsservice() { var user = user.withdefaultpasswordencoder() .username("admin") .password("admin") .roles("admin") .build(); return new inmemoryuserdetailsmanager(user); } }
spring batch:
使用 freemarker 生成批处理报告:
package com.example.demo.config; import com.example.demo.entity.user; import freemarker.template.configuration; import freemarker.template.template; import org.springframework.batch.core.job; import org.springframework.batch.core.step; import org.springframework.batch.core.configuration.annotation.enablebatchprocessing; import org.springframework.batch.core.configuration.annotation.jobbuilderfactory; import org.springframework.batch.core.configuration.annotation.stepbuilderfactory; import org.springframework.batch.item.database.jpapagingitemreader; import org.springframework.batch.item.database.builder.jpapagingitemreaderbuilder; import org.springframework.batch.item.file.flatfileitemwriter; import org.springframework.batch.item.file.transform.passthroughlineaggregator; import org.springframework.beans.factory.annotation.autowired; import org.springframework.context.annotation.bean; import org.springframework.core.io.filesystemresource; import org.springframework.stereotype.component; import jakarta.persistence.entitymanagerfactory; import java.io.stringwriter; @component @enablebatchprocessing public class batchconfig { @autowired private jobbuilderfactory jobbuilderfactory; @autowired private stepbuilderfactory stepbuilderfactory; @autowired private entitymanagerfactory entitymanagerfactory; @autowired private configuration freemarkerconfig; @bean public jpapagingitemreader<user> reader() { return new jpapagingitemreaderbuilder<user>() .name("userreader") .entitymanagerfactory(entitymanagerfactory) .querystring("select u from user u") .pagesize(10) .build(); } @bean public flatfileitemwriter<user> writer() throws exception { flatfileitemwriter<user> writer = new flatfileitemwriter<>(); writer.setresource(new filesystemresource("users-report.html")); writer.setlineaggregator(new passthroughlineaggregator<user>() { @override public string aggregate(user user) { try { template template = freemarkerconfig.gettemplate("report.ftl"); stringwriter out = new stringwriter(); template.process(java.util.collections.singletonmap("user", user), out); return out.tostring(); } catch (exception e) { throw new runtimeexception("template processing failed", e); } } }); return writer; } @bean public step step1() throws exception { return stepbuilderfactory.get("step1") .<user, user>chunk(10) .reader(reader()) .writer(writer()) .build(); } @bean public job generatereportjob() throws exception { return jobbuilderfactory.get("generatereportjob") .start(step1()) .build(); } }
报告模板(src/main/resources/templates/report.ftl
):
<div> <p>id: ${user.id}</p> <p>name: ${user.name?html}</p> <p>age: ${user.age}</p> </div>
热加载:
启用 devtools,支持模板修改后自动重载:
spring: devtools: restart: enabled: true
threadlocal:
在服务层清理 threadlocal:
public page<user> searchusers(string name, int page, int size, string sortby, string direction) { try { string profile = string.join(",", environment.getactiveprofiles()); context.set("query-" + profile + "-" + thread.currentthread().getname()); sort sort = sort.by(sort.direction.fromstring(direction), sortby); pageable pageable = pagerequest.of(page, size, sort); page<user> result = userrepository.findbynamecontaining(name, pageable); jmstemplate.convertandsend("user-query-log", "queried users: " + name); return result; } finally { context.remove(); } }
actuator 安全性:
- 限制
/actuator/**
访问,仅/actuator/health
公开。
4. 运行验证
开发环境:
java -jar demo.jar --spring.profiles.active=dev
- 访问
http://localhost:8081/users
,登录后查看分页用户列表。 - 访问
http://localhost:8081/swagger-ui.html
,测试/api/users
(需admin
/admin
)。 - 检查 activemq 日志和 h2 数据库。
生产环境:
java -jar demo.jar --spring.profiles.active=prod
确认 mysql 连接、swagger 禁用、模板缓存启用。
原理与性能
原理
- 模板引擎:freemarker 解析
.ftl
文件,结合数据模型生成输出。 - spring 集成:spring boot 自动配置
freemarkerconfigurer
,加载classpath:/templates/
。 - 缓存:生产环境启用缓存,减少解析开销。
性能
- 渲染 10 用户页面:50ms(h2,缓存关闭)。
- 10,000 用户分页查询:1.5s(mysql,索引优化)。
- activemq 日志:1-2ms/条。
- swagger 文档:首次 50ms。
测试
@test public void testfreemarkerperformance() { long start = system.currenttimemillis(); resttemplate.getforentity("/users?page=0&size=10", string.class); system.out.println("page render: " + (system.currenttimemillis() - start) + " ms"); }
常见问题
模板未加载:
- 问题:访问
/users
返回 404。 - 解决:确认
users.ftl
在src/main/resources/templates/
,检查spring.freemarker.template-loader-path
。
xss 风险:
- 问题:用户输入导致脚本注入。
- 解决:使用
${user.name?html}
转义。
threadlocal 泄漏:
- 问题:
/actuator/threaddump
显示泄漏。 - 解决:使用
finally
清理。
配置未热加载:
- 问题:修改
.ftl
未生效。 - 解决:启用 devtools,设置
spring.freemarker.cache=false
。
实际案例
- 用户管理页面:动态用户列表,开发效率提升 50%。
- 报表生成:批处理生成 html 报告,自动化率 80%。
- 云原生部署:kubernetes 部署,安全性 100%。
未来趋势
- 响应式模板:freemarker 与 webflux 集成。
- ai 辅助模板:spring ai 优化模板生成。
- 云原生:支持 configmap 动态模板。
实施指南
快速开始:
- 添加
spring-boot-starter-freemarker
,创建users.ftl
。 - 配置控制器,返回用户数据。
优化:
- 集成分页、activemq、swagger、security、profiles。
- 使用 spring batch 生成报告。
监控:
- 使用
/actuator/metrics
跟踪性能。 - 检查
/actuator/threaddump
防止泄漏。
总结
freemarker 是一种高效的模板引擎,适合生成动态内容。在 spring boot 中,通过 spring-boot-starter-freemarker
快速集成。示例展示了用户列表页面、批处理报告生成及与分页、swagger、activemq、profiles、security 的集成。性能测试显示高效(50ms 渲染 10 用户)。针对你的查询(threadlocal、actuator、热加载),通过清理、security 和 devtools 解决。
到此这篇关于在 spring boot 中实现 freemarker 模板的文章就介绍到这了,更多相关spring boot freemarker 模板内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论