java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Boot  FreeMarker 模板

如何在 Spring Boot 中实现 FreeMarker 模板

作者:专业WP网站开发-Joyous

FreeMarker 是一种功能强大、轻量级的模板引擎,用于在 Java 应用中生成动态文本输出(如 HTML、XML、邮件内容等),本文给大家介绍在 Spring Boot 中实现 FreeMarker 模板的示例,感兴趣的朋友一起看看吧

什么是 FreeMarker 模板?

FreeMarker 是一种功能强大、轻量级的模板引擎,用于在 Java 应用中生成动态文本输出(如 HTML、XML、邮件内容等)。它允许开发者将数据模型与模板文件分离,通过模板语法动态生成内容。FreeMarker 广泛用于 Web 开发、报表生成和自动化文档生成,特别是在 Spring Boot 项目中与 Spring MVC 集成,用于生成动态网页。

核心功能

优势

挑战

在 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);
            }
        };
    }
}

运行验证

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 安全性

4. 运行验证

开发环境

java -jar demo.jar --spring.profiles.active=dev

生产环境

java -jar demo.jar --spring.profiles.active=prod

确认 MySQL 连接、Swagger 禁用、模板缓存启用。

原理与性能

原理

性能

测试

@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");
}

常见问题

模板未加载

XSS 风险

ThreadLocal 泄漏

配置未热加载

实际案例

未来趋势

实施指南

快速开始

优化

监控

总结

FreeMarker 是一种高效的模板引擎,适合生成动态内容。在 Spring Boot 中,通过 spring-boot-starter-freemarker 快速集成。示例展示了用户列表页面、批处理报告生成及与分页、Swagger、ActiveMQ、Profiles、Security 的集成。性能测试显示高效(50ms 渲染 10 用户)。针对你的查询(ThreadLocal、Actuator、热加载),通过清理、Security 和 DevTools 解决。

到此这篇关于在 Spring Boot 中实现 FreeMarker 模板的文章就介绍到这了,更多相关Spring Boot FreeMarker 模板内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文