当前位置: 代码网 > it编程>编程语言>Java > 如何在 Spring Boot 中实现 FreeMarker 模板

如何在 Spring Boot 中实现 FreeMarker 模板

2025年04月28日 Java 我要评论
什么是 freemarker 模板?freemarker 是一种功能强大、轻量级的模板引擎,用于在 java 应用中生成动态文本输出(如 html、xml、邮件内容等)。它允许开发者将数据模型与模板文

什么是 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; }
}

repositoryuserrepository.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.ymlapplication-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.ftlsrc/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 模板内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com