使用 poi-tl 生成 Word 文档并上传到 Minio

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/                           # 静态资源
  1. 启动应用后访问 http://localhost:8080/api/document/generate(POST)

  2. 服务将自动生成Word报告并上传到Minio

  3. 返回包含文件路径的JSON响应

相关推荐
我的golang之路果然有问题1 小时前
word中latex插入矩阵的语法问题
笔记·学习·矩阵·word·latex·template method·分享
程序员柒叔2 小时前
Dify知识库- Word文档处理
大模型·word·workflow·知识库·工作流·dify
VBAMatrix1 天前
新一代邮件合并!按Word模板批量生成个性化文档
word·办公自动化·邮件合并·审计·报告工具·批量生成合同
梦幻通灵2 天前
Word自动对齐答案ABCD选项的实现方案
word
缺点内向2 天前
如何在C#中为文本内容添加行号?
开发语言·c#·word·.net
LearnerForeveer2 天前
在Word中插入LaTeX风格公式
word
诸神缄默不语3 天前
Python 3中的win32com使用教程+示例:从Excel读取数据生成Word格式报告批量发邮件
python·word·excel
你挚爱的强哥3 天前
【sgSelectExportDocumentType】自定义组件:弹窗dialog选择导出文件格式word、pdf,支持配置图标和格式名称,触发导出事件
vue.js·pdf·word
温轻舟4 天前
Python自动办公工具06-设置Word文档中表格的格式
开发语言·python·word·自动化工具·温轻舟