1. Maven依赖配置 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>word-export-service</artifactId>
<version>1.0.0</version>
<properties>
<java.version>1.8</java.version>
<poi-tl.version>1.12.1</poi-tl.version>
<minio.version>8.5.7</minio.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- poi-tl Word模板引擎 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>${poi-tl.version}</version>
</dependency>
<!-- Minio对象存储 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!-- Lombok简化代码 -->
<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>
</dependencies>
<build>
<plugins>
<!-- Maven资源插件配置 - 防止Word模板文件被错误编码 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<encoding>UTF-8</encoding>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>docx</nonFilteredFileExtension>
<nonFilteredFileExtension>doc</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
<!-- Spring Boot Maven插件 -->
<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>
2. 应用配置文件 (application.yml)
# Minio对象存储配置
minio:
endpoint: http://localhost:9000
access-key: your-access-key
secret-key: your-secret-key
bucket-name: documents
# Spring配置
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
# 应用配置
application:
name: word-export-service
# 日志配置
logging:
level:
com.example: DEBUG
io.minio: INFO
3. Minio配置类
package com.example.study.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Data
@Component
public class MinIoClientConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
/**
* 注入minio 客户端
* @return
*/
@Bean
public MinioClient minioClient(){
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
4. 数据模型类
package com.example.study.controller.poitl.entity;
import com.deepoove.poi.data.ChartSingleSeriesRenderData;
import lombok.Data;
import java.util.List;
@Data
public class ReportData {
private String title;
private String date;
private String author;
private List<MemberInfo> members;
private ChartSingleSeriesRenderData pieChart;
}
package com.example.study.controller.poitl.entity;
import lombok.Data;
@Data
public class MemberInfo {
private String name;
private String empId;
private String department;
public MemberInfo() {}
public MemberInfo(String name, String empId, String department) {
this.name = name;
this.empId = empId;
this.department = department;
}
}
5. 核心服务类
package com.example.study.controller.poitl.service;
public interface WordExportService {
String generateAndUploadReport() throws Exception;
}
package com.example.study.controller.poitl.service.impl;
import com.alibaba.fastjson.JSON;
import com.deepoove.poi.data.ChartMultiSeriesRenderData;
import com.deepoove.poi.data.ChartSingleSeriesRenderData;
import com.deepoove.poi.data.Charts;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import com.example.study.controller.poitl.entity.MemberInfo;
import com.example.study.controller.poitl.entity.ReportData;
import com.example.study.controller.poitl.service.WordExportService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import springfox.documentation.spring.web.json.Json;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@Service
public class WordExportServiceImpl implements WordExportService {
private final MinioClient minioClient;
@Value("${minio.bucket-name}")
private String bucketName;
@Value("${spring.application.name:word-service}")
private String appName;
public WordExportServiceImpl(MinioClient minioClient) {
this.minioClient = minioClient;
}
/**
* 生成Word报告并上传到Minio - 优化版(只创建一次临时文件)
*
* @return Minio中的文件存储路径
*/
public String generateAndUploadReport() throws Exception {
log.info("开始生成Word报告并上传到Minio");
// 1. 准备数据模型
Map dataMap = prepareTestData();
// 2. 定义临时文件变量(用于最终输出)
File tempOutputFile = null;
// 3. 使用try-with-resources确保模板输入流自动关闭
try (InputStream templateStream = new ClassPathResource("template/report-template.docx").getInputStream()) {
// 4. 配置渲染策略:处理动态表格
// 绑定表格循环渲染策略
LoopRowTableRenderPolicy tablePolicy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder()
.bind("members", tablePolicy) // 绑定表格循环渲染策略
.build();
// 5. 编译模板并渲染数据
XWPFTemplate template = XWPFTemplate.compile(templateStream, config).render(dataMap);
// 6. 创建唯一的临时文件用于存储渲染结果(只创建这一次临时文件)
tempOutputFile = createTempFile();
log.debug("创建临时文件: {}", tempOutputFile.getAbsolutePath());
// 7. 将渲染后的内容写入临时文件
try (FileOutputStream out = new FileOutputStream(tempOutputFile)) {
template.write(out);
log.debug("Word文档渲染完成,文件大小: {} bytes", tempOutputFile.length());
} finally {
template.close(); // 确保模板资源被关闭
}
// 8. 上传到Minio
String objectName = generateObjectName();
String filePath = uploadToMinio(tempOutputFile, objectName);
log.info("Word报告生成并上传成功,路径: {}", filePath);
return filePath;
} catch (Exception e) {
log.error("生成报告失败:{}", e.getMessage(), e);
} finally {
// 9. 最终清理:删除临时输出文件(如果已创建)
if (tempOutputFile != null && tempOutputFile.exists()) {
boolean deleted = Files.deleteIfExists(tempOutputFile.toPath());
if (deleted) {
log.debug("临时文件已清理: {}", tempOutputFile.getName());
}
}
}
return "";
}
/**
* 创建临时文件
*/
private File createTempFile() throws Exception {
String prefix = "word-report-" + System.currentTimeMillis() + "-";
String suffix = ".docx";
File tempFile = File.createTempFile(prefix, suffix);
tempFile.deleteOnExit(); // JVM退出时删除
return tempFile;
}
/**
* 准备测试数据
*/
private Map<String, Object> prepareTestData() {
Map<String, Object> dataMap = new HashMap<>();
// 字符串数据
dataMap.put("title", "2025年第四季度项目报告");
dataMap.put("date", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
dataMap.put("author", appName);
// 列表数据(用于演示表格行循环)
List<MemberInfo> members = Arrays.asList(
new MemberInfo("张三", "1001", "研发部"),
new MemberInfo("李四", "1002", "市场部"),
new MemberInfo("王五", "1003", "产品部")
);
dataMap.put("members", members);
ChartSingleSeriesRenderData pie = Charts
.ofSingleSeries("ChartTitle", new String[] { "吃", "不吃" })
.series("countries", new Integer[] { 30, 70 })
.create();
dataMap.put("pieChart", pie);
log.debug("测试数据准备完成,{}", JSON.toJSONString(dataMap));
return dataMap;
}
// /**
// * 准备测试数据 TODO 饼图data.setPieChart(pie);会报错,没有解决,有图表尽量直接用Map格式
// */
// private Map prepareTestData() {
// ReportData data = new ReportData();
//
// data.setTitle("2025年第四季度项目报告");
// data.setDate(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
// data.setAuthor(appName);
// // 列表数据(用于演示表格行循环)
// List<MemberInfo> members = Arrays.asList(
// new MemberInfo("张三", "1001", "研发部"),
// new MemberInfo("李四", "1002", "市场部"),
// new MemberInfo("王五", "1003", "产品部"));
// data.setMembers(members);
//
// ChartSingleSeriesRenderData pie = Charts
// .ofSingleSeries("ChartTitle", new String[]{"美国", "中国"})
// .series("countries", new Integer[]{9826675, 9596961})
// .create();
// data.setPieChart(pie);
//
// log.debug("测试数据准备完成,{}", JSON.toJSONString(data));
// ObjectMapper mapper = new ObjectMapper();
// Map map = mapper.convertValue(data, Map.class);
// return map;
// }
/**
* 生成有结构的对象名称
*/
private String generateObjectName() {
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String uuid = UUID.randomUUID().toString().substring(0, 8);
String objectName = String.format("reports/%s/report-%s.docx", datePath, uuid);
log.debug("生成对象名称: {}", objectName);
return objectName;
}
/**
* 上传文件到Minio
*/
private String uploadToMinio(File file, String objectName) throws Exception {
// 确保存储桶存在
ensureBucketExists();
// 上传文件
try (InputStream fileStream = Files.newInputStream(file.toPath())) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(fileStream, file.length(), -1)
.contentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
.build());
}
String fullPath = String.format("%s/%s", bucketName, objectName);
log.debug("文件上传成功: {}", fullPath);
return fullPath;
}
/**
* 确保存储桶存在 - 简化写法
*/
private void ensureBucketExists() throws Exception {
// 直接使用 builder() 方法,避免复杂的 Lambda
boolean found = minioClient.bucketExists(
BucketExistsArgs.builder().bucket(bucketName).build()
);
if (!found) {
log.info("存储桶不存在,创建新存储桶: {}", bucketName);
minioClient.makeBucket(
MakeBucketArgs.builder().bucket(bucketName).build()
);
}
}
}
6. 控制器类
package com.example.study.controller.poitl.controller;
import com.example.study.controller.poitl.service.WordExportService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
// .\minio.exe server E:\minio
@RestController
@RequestMapping("/api/document")
@RequiredArgsConstructor
public class DocumentController {
private final WordExportService wordExportService;
/**
* 生成并上传Word报告
*/
@PostMapping("/generate")
public ResponseEntity<Map<String, Object>> generateReport(){
Map<String, Object> response = new HashMap<>();
try {
String filePath = wordExportService.generateAndUploadReport();
response.put("success", true);
response.put("message", "文档生成并上传成功");
response.put("filePath", filePath);
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "文档生成失败: " + e.getMessage());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.internalServerError().body(response);
}
}
}
7. 主应用类
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WordExportApplication {
public static void main(String[] args) {
SpringApplication.run(WordExportApplication.class, args);
}
}
8.模板
{{title}}
报告日期: {{date}}
生成人员: {{author}}
项目成员列表
|---------------|-----------|----------------|
| {{members}}姓名 | 工号 | 部门 |
| [name] | [empId] | [department] |

注意:

项目结构
src/main/java/
└── com/example/
├── WordExportApplication.java # 主应用类
├── config/
│ └── MinioConfig.java # Minio配置
├── controller/
│ └── DocumentController.java # REST控制器
├── service/
│ └── WordExportService.java # 核心业务服务
└── model/
└── ReportData.java # 数据模型
src/main/resources/
├── application.yml # 应用配置
├── template/
│ └── report-template.docx # Word模板文件
└── static/ # 静态资源
-
启动应用后访问
http://localhost:8080/api/document/generate(POST) -
服务将自动生成Word报告并上传到Minio
-
返回包含文件路径的JSON响应