前言
想像一下,我们做了一个Sass模式下的人事合同管理系统,每公司的合同都不是统一的,所以每家公司都有一个合同模板;要求实现下载员工合同的功能。
基于以上需求有两个解决方案:1.我博客中提到过《Spring Boot 手把手实现PDF功能》
2.就是通过poi-tl实现word内容填充,而本文就重点介绍它的基本实现。
一、目标
两个表:一个是用户表、一个用户识别表,通过用户ID,输入word文件中包含用户的基本信息、用户的识别记录。

二、数据库表




三、POM依赖
html
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>word</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>word</name>
<description>word</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<poi-tl.version>1.12.1</poi-tl.version>
<springdoc.version>1.6.15</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- OpenAPI/Swagger UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- POI-TL for Word template -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>${poi-tl.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
四、配置
4.1 YML
html
server:
port: 2025
spring:
application:
name: word
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 密码
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:/mapper/**/*.xml
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
packages-to-scan: org.example.word.controller
default-consumes-media-type: application/json
default-produces-media-type: application/json
4.2 配置类
MyBatisPlusConfig
java
@Configuration
public class MyBatisPlusConfig {
/**
* MyBatis Plus Interceptor for pagination
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
OpenApiConfig
java
@Configuration
public class OpenApiConfig {
/**
* 配置 OpenAPI 信息
*/
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Word 模板导入系统 API")
.description("基于 Spring Boot + POI-TL 的 Word 报告生成系统")
.version("1.0.0")
.contact(new Contact()
.name("开发团队")
.email("developer@example.com")
.url("https://example.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")));
}
}
五、实现
5.1 模板文件内容

5.2 主程序
java
@MapperScan("org.example.word.mapper")
@SpringBootApplication
public class WordApplication {
public static void main(String[] args) {
SpringApplication.run(WordApplication.class, args);
}
}
5.3 实体类
java
**
* User Entity
*/
@Data
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String username;
private String email;
private String phone;
private Integer age;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer deleted;
}
java
@Data
@TableName("user_reg")
public class UserReg implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private Long userId;
private LocalDateTime regTime;
}
5.4 mapper
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
java
@Mapper
public interface UserRegMapper extends BaseMapper<UserReg> {
}
5.5 service
java
public interface UserService extends IService<User> {
void generateUserReport(Long userId, HttpServletResponse response);
}
java
package org.example.word.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.data.Tables;
import jakarta.servlet.http.HttpServletResponse;
import org.example.word.entity.User;
import org.example.word.entity.UserReg;
import org.example.word.mapper.UserMapper;
import org.example.word.service.UserRegService;
import org.example.word.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* User Service Implementation
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserRegService userRegService;
@Autowired
private UserMapper userMapper;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void generateUserReport(Long userId, HttpServletResponse response) {
try {
// 查询用户信息
User user = userMapper.selectById(userId);
// 判断用户是否存在
if (user == null) {
throw new RuntimeException("用户不存在,ID: " + userId);
}
// 查询用户识别信息
List<UserReg> userRegs= userRegService.list(new QueryWrapper<UserReg>().
eq("user_id", userId).orderByDesc("reg_time"));
Map<String, Object> data = buildReportData(user, userRegs);
//重点代码
// 构建模板数据
ClassPathResource resource = new ClassPathResource("templates/user_registration_template_simple.docx");
// 判断模板文件是否存在
if (!resource.exists()) {
throw new RuntimeException("Word 模板文件不存在");
}
XWPFTemplate template = XWPFTemplate.compile(resource.getInputStream()).render(data);
// 设置响应内容类型为Word文档
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
// 设置字符编码为UTF-8(支持中文文件名)
response.setCharacterEncoding("UTF-8");
// 设置下载文件名(URL编码,防止中文乱码)
String fileName = URLEncoder.encode("用户识别报告_" + user.getUsername() + ".docx", "UTF-8");
// 设置Content-Disposition响应头,告诉浏览器以附件形式下载文件
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
// 获取HTTP响应的输出流
OutputStream out = response.getOutputStream();
// 将生成的Word文档写入输出流
template.write(out);
// 刷新输出流,确保所有数据都发送到客户端
out.flush();
// 关闭输出流
out.close();
// 关闭模板对象,释放资源
template.close();
} catch (Exception e) {
throw new RuntimeException("生成用户报告失败", e);
}
}
private Map<String, Object> buildReportData(User user, List<UserReg> userRegs) {
// 创建数据映射容器
Map<String, Object> data = new HashMap<>();
// ==================== 填充用户基本信息 ====================
// 这些数据对应模板中的 {{username}}、{{email}} 等占位符
// 用户名(必填)
data.put("username", user.getUsername());
// 邮箱(可能为空,提供默认值)
data.put("email", user.getEmail() != null ? user.getEmail() : "未填写");
// 电话(可能为空,提供默认值)
data.put("phone", user.getPhone() != null ? user.getPhone() : "未填写");
// 年龄(可能为空,需要转换为字符串)
data.put("age", user.getAge() != null ? user.getAge().toString() : "未填写");
// 创建时间(格式化为字符串)
data.put("createTime", user.getCreateTime() != null ?
user.getCreateTime().format(DATE_FORMATTER) : "未知");
// ==================== 构建识别记录表格 ====================
// 使用 POI-TL 的 TableRenderData 创建真实的Word表格
// 对应模板中的 {{#regTable}} 占位符
// 创建行数据列表(包括表头和数据行)
java.util.List<com.deepoove.poi.data.RowRenderData> allRows = new java.util.ArrayList<>();
// --- 第1步:创建表头行 ---
// Rows.of(): 创建一行,参数为各列的内容
// .center(): 设置单元格内容居中对齐
// .bgColor("CCCCCC"): 设置背景色为灰色(十六进制颜色代码)
// .create(): 生成 RowRenderData 对象
allRows.add(Rows.of("序号", "用户ID", "识别时间")
.center() // 表头文字居中
.bgColor("CCCCCC") // 表头灰色背景
.create());
// --- 第2步:创建数据行 ---
// 遍历所有识别记录,每条记录生成一行
for (int i = 0; i < userRegs.size(); i++) {
UserReg reg = userRegs.get(i);
// 创建一行数据,包含3列:序号、用户ID、识别时间
allRows.add(Rows.of(
String.valueOf(i + 1), // 第1列:序号(从1开始)
String.valueOf(reg.getUserId()), // 第2列:用户ID
reg.getRegTime() != null ? // 第3列:识别时间
reg.getRegTime().format(DATE_FORMATTER) : "未知"
).create());
}
// --- 第3步:创建表格对象 ---
// Tables.of(): 将所有行数据组装成表格
// allRows.toArray(): 将List转换为数组
// .create(): 生成 TableRenderData 对象
com.deepoove.poi.data.TableRenderData table = Tables.of(
allRows.toArray(new com.deepoove.poi.data.RowRenderData[0])
).create();
// 将表格对象放入数据映射,键名为 "regTable"
// 对应模板中的 {{#regTable}} 占位符
data.put("regTable", table);
// ==================== 填充统计信息 ====================
// 总识别次数(识别记录的数量)
data.put("totalRegCount", userRegs.size());
// 报告生成时间(当前时间)
data.put("reportTime", LocalDateTime.now().format(DATE_FORMATTER));
// 返回完整的数据映射
return data;
}
}
java
public interface UserRegService extends IService<UserReg> {
}
java
@Service
public class UserRegServiceImpl extends ServiceImpl<UserRegMapper, UserReg> implements UserRegService {
}
5.6 conntroller
java
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/generate/{userId}")
public void generateReport(@PathVariable Long userId, HttpServletResponse response) {
userService.generateUserReport(userId, response);
}
}
六、效果展示

七、扩展:显示图片
用户表增加字段 与数据
sql
ALTER TABLE user ADD COLUMN photo_path VARCHAR(500) COMMENT '用户照片网络路径';
UPDATE user SET photo_path = 'https://example.com/photo.jpg';
为实体类增加成员
java
@Data
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String username;
private String email;
private String phone;
private Integer age;
private String photoPath;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer deleted;
}
创建模板标签:

实现核心方法代码
java
private Map<String, Object> buildReportData(User user, List<UserReg> userRegs) {
// 创建数据映射容器
Map<String, Object> data = new HashMap<>();
// ==================== 填充用户基本信息 ====================
// 这些数据对应模板中的 {{username}}、{{email}} 等占位符
// 用户名(必填)
data.put("username", user.getUsername());
// 邮箱(可能为空,提供默认值)
data.put("email", user.getEmail() != null ? user.getEmail() : "未填写");
// 电话(可能为空,提供默认值)
data.put("phone", user.getPhone() != null ? user.getPhone() : "未填写");
// 年龄(可能为空,需要转换为字符串)
data.put("age", user.getAge() != null ? user.getAge().toString() : "未填写");
// 创建时间(格式化为字符串)
data.put("createTime", user.getCreateTime() != null ?
user.getCreateTime().format(DATE_FORMATTER) : "未知");
// ==================== 处理用户照片 ====================
// 如果用户有照片路径,则添加照片到报告中
if (user.getPhotoPath() != null && !user.getPhotoPath().isEmpty()) {
try {
// 使用 Pictures.ofUrl() 从网络路径加载图片
// 参数:图片URL, 宽度(像素), 高度(像素)
PictureRenderData picture = Pictures.ofUrl(user.getPhotoPath())
.size(120, 160) // 设置照片尺寸:宽120px,高160px
.create();
data.put("userPhoto", picture);
} catch (Exception e) {
// 如果加载照片失败,使用默认文本
data.put("userPhoto", "照片加载失败");
}
} else {
// 如果没有照片路径,显示默认文本
data.put("userPhoto", "暂无照片");
}
// ==================== 构建识别记录表格 ====================
// 使用 POI-TL 的 TableRenderData 创建真实的Word表格
// 对应模板中的 {{#regTable}} 占位符
// 创建行数据列表(包括表头和数据行)
java.util.List<com.deepoove.poi.data.RowRenderData> allRows = new java.util.ArrayList<>();
// --- 第1步:创建表头行 ---
// Rows.of(): 创建一行,参数为各列的内容
// .center(): 设置单元格内容居中对齐
// .bgColor("CCCCCC"): 设置背景色为灰色(十六进制颜色代码)
// .create(): 生成 RowRenderData 对象
allRows.add(Rows.of("序号", "用户ID", "识别时间")
.center() // 表头文字居中
.bgColor("CCCCCC") // 表头灰色背景
.create());
// --- 第2步:创建数据行 ---
// 遍历所有识别记录,每条记录生成一行
for (int i = 0; i < userRegs.size(); i++) {
UserReg reg = userRegs.get(i);
// 创建一行数据,包含3列:序号、用户ID、识别时间
allRows.add(Rows.of(
String.valueOf(i + 1), // 第1列:序号(从1开始)
String.valueOf(reg.getUserId()), // 第2列:用户ID
reg.getRegTime() != null ? // 第3列:识别时间
reg.getRegTime().format(DATE_FORMATTER) : "未知"
).create());
}
// --- 第3步:创建表格对象 ---
// Tables.of(): 将所有行数据组装成表格
// allRows.toArray(): 将List转换为数组
// .create(): 生成 TableRenderData 对象
com.deepoove.poi.data.TableRenderData table = Tables.of(
allRows.toArray(new com.deepoove.poi.data.RowRenderData[0])
).create();
// 将表格对象放入数据映射,键名为 "regTable"
// 对应模板中的 {{#regTable}} 占位符
data.put("regTable", table);
// ==================== 填充统计信息 ====================
// 总识别次数(识别记录的数量)
data.put("totalRegCount", userRegs.size());
// 报告生成时间(当前时间)
data.put("reportTime", LocalDateTime.now().format(DATE_FORMATTER));
// 返回完整的数据映射
return data;
}
实现效果

八、关于模板标签说明
{``{变量名}}- 用于文本{``{@变量名}}- 用于图片{``{#变量名}}- 用于表格{``{*变量名}}- 用于列表*
(全文结束,原创不易,欢迎点赞收藏)