SpringBoot使用Apache POI实现导出Word和PPT的完整代码

 更新时间:2026年03月10日 09:02:54   作者:身如柳絮随风扬  
POI-TL(POI Template Language)是一个基于Apache POI的Word模板引擎,这篇文章主要介绍了SpringBoot如何使用Apache POI实现导出Word和PPT,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

一、为什么使用POI-TL

POI-TL(POI Template Language)是一个基于Apache POI的Word模板引擎,它通过自定义标签语法,让你能以最少的代码实现复杂文档的生成。对比直接使用POI,其优势在于:

  • 极简代码:只需几行即可完成表格、图片、列表的渲染,无需遍历段落和表格。
  • 强大标签:支持{{var}}文本替换,{{[#list]}}循环表格,{{@image}}插入图片,{{+watermark}}水印等。
  • 完美保留样式:模板中的样式(字体、颜色、布局)在渲染后完全保留,符合企业级文档对格式的高要求。
  • 社区活跃、文档完善:是Java生成Word的主流选择,适合生产环境。

PPT方面,POI-TL不支持,我们仍用Apache POI原生API + 模板方式实现。

二、为什么使用Service接口

面向接口编程是Spring框架的核心实践,原因包括:

  • 解耦:Controller直接依赖接口,不关心实现细节,便于切换实现(如后期改用其他模板引擎)。
  • 可测试性:可轻松使用Mockito模拟接口进行单元测试。
  • 事务管理:Spring的声明式事务@Transactional可以标注在接口方法上,确保数据库操作与文件生成的一致性。
  • AOP支持:便于添加日志、权限等切面。

三、环境搭建与配置

1. 项目依赖(pom.xml关键部分)

<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.5.11</spring-boot.version>
    <mybatis-plus.version>3.5.9</mybatis-plus.version>
    <poi-tl.version>1.12.2</poi-tl.version> <!-- 最新稳定版 -->
</properties>

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- POI-TL (Word模板引擎) -->
    <dependency>
        <groupId>com.deepoove</groupId>
        <artifactId>poi-tl</artifactId>
        <version>${poi-tl.version}</version>
    </dependency>

    <!-- Apache POI (用于PPT导出) -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.3.0</version>
    </dependency>

    <!-- 工具类 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 配置文件 application.yml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/car_report_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: yourpassword
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时打印SQL
  global-config:
    db-config:
      id-type: auto

四、数据库设计

我们设计一张简单的汽车信息表,用于存储报告所需数据。

CREATE DATABASE IF NOT EXISTS car_report_db DEFAULT CHARACTER SET utf8mb4;

USE car_report_db;

CREATE TABLE `car` (
    `id` BIGINT AUTO_INCREMENT COMMENT '主键ID',
    `brand` VARCHAR(50) NOT NULL COMMENT '品牌',
    `model` VARCHAR(50) NOT NULL COMMENT '车型',
    `price` DECIMAL(10,2) COMMENT '指导价(万元)',
    `engine` VARCHAR(100) COMMENT '发动机',
    `max_power` INT COMMENT '最大功率(kW)',
    `max_torque` INT COMMENT '最大扭矩(N·m)',
    `acceleration` DECIMAL(3,1) COMMENT '百公里加速(s)',
    `fuel_consumption` DECIMAL(4,1) COMMENT '综合油耗(L/100km)',
    `image_url` VARCHAR(255) COMMENT '图片URL(用于PPT插入图片)',
    `description` TEXT COMMENT '车型描述',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='汽车信息表';

-- 插入测试数据
INSERT INTO `car` (`brand`, `model`, `price`, `engine`, `max_power`, `max_torque`, `acceleration`, `fuel_consumption`, `image_url`, `description`) VALUES
('宝马', 'X5 xDrive40i', 75.99, '3.0T L6', 250, 450, 5.5, 9.1, 'https://cdn.simpleicons.org/bmw', '豪华中大型SUV,操控与舒适兼备。'),
('特斯拉', 'Model Y 长续航版', 34.99, '纯电动', 331, 559, 5.0, 0.0, 'https://cdn.simpleicons.org/tesla', '纯电动SUV,续航持久,智能科技。'),
('奔驰', 'A6L 45 TFSI', 45.89, '2.0T L4', 180, 370, 7.5, 7.5, 'https://cdn.simpleicons.org/mercedes', '商务轿车典范,空间宽敞,科技感强。');

五、实体类与Mapper

实体类 Car.java

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;

@Data
@TableName("car")
public class Car {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String brand;
    private String model;
    private BigDecimal price;
    private String engine;
    private Integer maxPower;
    private Integer maxTorque;
    private BigDecimal acceleration;
    private BigDecimal fuelConsumption;
    private String imageUrl;
    private String description;
}

Mapper CarMapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface CarMapper extends BaseMapper<Car> {
}

六、Service接口与实现类

1. 导出服务接口 ReportService.java

import jakarta.servlet.http.HttpServletResponse;

public interface ReportService {
    /**
     * 导出Word报告
     * @param carId 汽车ID
     * @param response HttpServletResponse用于输出文件
     */
    void exportWord(Long carId, HttpServletResponse response);

    /**
     * 导出PPT报告
     * @param carId 汽车ID
     * @param response HttpServletResponse用于输出文件
     */
    void exportPpt(Long carId, HttpServletResponse response);
}

2. 实现类 ReportServiceImpl.java

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.deepoove.poi.data.Texts;
import com.deepoove.poi.data.style.Style;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.xslf.usermodel.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jakarta.servlet.http.HttpServletResponse;
import java.awt.*;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class ReportServiceImpl implements ReportService {

    private final CarMapper carMapper;

    @Value("${file.template-path:templates/}") // 模板存放路径
    private String templatePath;

    @Override
    @Transactional(readOnly = true) // 只读事务,提高数据库性能
    public void exportWord(Long carId, HttpServletResponse response) {
        // 1. 查询数据
        Car car = carMapper.selectById(carId);
        if (car == null) {
            throw new RuntimeException("汽车不存在");
        }

        // 2. 构建数据模型(POI-TL要求的数据结构)
        Map<String, Object> data = new HashMap<>();
        data.put("brand", car.getBrand());
        data.put("model", car.getModel());
        data.put("price", car.getPrice() + "万元");
        data.put("engine", car.getEngine());
        data.put("maxPower", car.getMaxPower() + "kW");
        data.put("maxTorque", car.getMaxTorque() + "N·m");
        data.put("acceleration", car.getAcceleration() + "秒");
        data.put("fuelConsumption", car.getFuelConsumption() + "L/100km");
        data.put("description", Texts.of(car.getDescription()).create());

        // 如果有图片,处理图片占位符 {{@image}} (假设图片存在本地或网络)
        if (car.getImageUrl() != null && !car.getImageUrl().isEmpty()) {
            try {
                // 这里简单地从类路径读取图片(实际生产应从文件服务器或URL获取)
                InputStream imageStream = new ClassPathResource("static" + car.getImageUrl()).getInputStream();
                data.put("image", Pictures.ofStream(imageStream, PictureType.PNG)
                        .size(200, 150).create()); // 设置图片宽高
            } catch (IOException e) {
                log.warn("图片读取失败: {}", car.getImageUrl(), e);
                data.put("image", null);
            }
        }

        // 3. 加载模板并渲染
        try (InputStream templateStream = new ClassPathResource(templatePath + "car_report_template.docx").getInputStream();
             XWPFTemplate template = XWPFTemplate.compile(templateStream).render(data);
             OutputStream out = response.getOutputStream()) {

            // 4. 设置响应头
            setResponseHeader(response, "car_report_" + carId + ".docx");

            // 5. 写入输出流
            template.write(out);
            out.flush();
        } catch (IOException e) {
            log.error("导出Word失败,carId: {}", carId, e);
            throw new RuntimeException("导出Word失败", e);
        }
    }

    @Override
    @Transactional(readOnly = true)
    public void exportPpt(Long carId, HttpServletResponse response) {
        // 1. 查询数据
        Car car = carMapper.selectById(carId);
        if (car == null) {
            throw new RuntimeException("汽车不存在");
        }

        // 2. 加载PPT模板 (使用Apache POI)
        try (InputStream templateStream = new ClassPathResource(templatePath + "car_report_template.pptx").getInputStream();
             XMLSlideShow ppt = new XMLSlideShow(templateStream);
             OutputStream out = response.getOutputStream()) {

            // 3. 遍历幻灯片,替换占位符
            for (XSLFSlide slide : ppt.getSlides()) {
                // 替换文本框内容
                for (XSLFShape shape : slide.getShapes()) {
                    if (shape instanceof XSLFTextShape) {
                        XSLFTextShape textShape = (XSLFTextShape) shape;
                        String text = textShape.getText();
                        if (text != null) {
                            text = text.replace("{{brand}}", car.getBrand())
                                    .replace("{{model}}", car.getModel())
                                    .replace("{{price}}", car.getPrice() + "万元")
                                    .replace("{{engine}}", car.getEngine())
                                    .replace("{{maxPower}}", car.getMaxPower() + "kW")
                                    .replace("{{maxTorque}}", car.getMaxTorque() + "N·m")
                                    .replace("{{acceleration}}", car.getAcceleration() + "秒")
                                    .replace("{{fuelConsumption}}", car.getFuelConsumption() + "L/100km")
                                    .replace("{{description}}", car.getDescription());
                            textShape.setText(text);
                        }
                    }
                    // 如果是图片占位符,插入图片 (需要预先在模板中放置一个图片,并识别它)
                    if (shape instanceof XSLFPictureShape) {
                        // 通常我们通过形状名称来标记占位图片
                        if ("imagePlaceholder".equals(shape.getShapeName())) {
                            // 移除原图片,添加新图片(略复杂,此处仅演示思路)
                            // 实际应用中建议用文本框占位,然后新建图片
                        }
                    }
                }
            }

            // 4. 设置响应头
            setResponseHeader(response, "car_report_" + carId + ".pptx");

            // 5. 写入输出流
            ppt.write(out);
            out.flush();
        } catch (IOException e) {
            log.error("导出PPT失败,carId: {}", carId, e);
            throw new RuntimeException("导出PPT失败", e);
        }
    }

    /**
     * 设置下载响应头
     */
    private void setResponseHeader(HttpServletResponse response, String filename) {
        response.setContentType("application/octet-stream");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");
        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFilename);
    }
}

说明

  • 使用了@Transactional(readOnly = true)确保数据库查询在事务中执行,但不会锁定数据。
  • 通过ClassPathResource加载模板文件(放在resources/templates/下)。
  • 图片处理部分简化,实际生产可能涉及从文件服务器下载、缓存等。
  • 响应头设置支持中文文件名。

七、Controller测试接口

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/api/report")
@RequiredArgsConstructor
public class ReportController {

    private final ReportService reportService;

    @GetMapping("/word/{carId}")
    public void exportWord(@PathVariable Long carId, HttpServletResponse response) {
        reportService.exportWord(carId, response);
    }

    @GetMapping("/ppt/{carId}")
    public void exportPpt(@PathVariable Long carId, HttpServletResponse response) {
        reportService.exportPpt(carId, response);
    }
}

测试地址(请先配好模板)

  • Word导出:http://localhost:8080/api/report/word/1
  • PPT导出:http://localhost:8080/api/report/ppt/1

八、模板设计指南(Word和PPT)

Word模板(car_report_template.docx)

使用POI-TL标签语法,在Word中设计一个美观的报告样式。以下示例标签:

标题区{{brand}} {{model}} 汽车详细报告

基本参数表格

项目参数
品牌{{brand}}
型号{{model}}
指导价{{price}}
发动机{{engine}}
最大功率{{maxPower}}
最大扭矩{{maxTorque}}
0-100km/h{{acceleration}}
综合油耗{{fuelConsumption}}
  • 描述段落{{description}}
  • 图片区域:使用{{@image}}占位,可设置宽高(如{{@image}}前加标签{{@image}},POI-TL会自动渲染图片)。

设计建议(模板根据自己来设计Word样式)

  • 使用表格布局保持整齐。
  • 预设好字体、颜色、边框,POI-TL会完全保留。
  • 可以添加页眉页脚,包含公司Logo和日期(静态内容直接放在模板中)。

PPT模板(car_report_template.pptx)

使用Apache POI原生操作,我们通常预置占位符文本,如{{brand}},在代码中遍历形状并替换。图片替换需要更复杂的逻辑,可以预先插入一个占位图片(如一个灰色方块),并设置其形状名称为imagePlaceholder,然后在代码中定位并替换为实际图片。

设计建议(模板根据自己来设计PPT样式)

  • 首页:大标题{{brand}} {{model}} 汽车报告
  • 第二页:参数表格,使用PPT表格,每个单元格内放占位符如{{price}}
  • 第三页:描述+图片,文本框中放{{description}},图片占位符命名为imagePlaceholder

九、运行与测试

  • 启动MySQL,执行SQL建库建表并插入测试数据。
  • 将Word模板car_report_template.docx和PPT模板car_report_template.pptx放入resources/templates/目录。
  • 修改application.yml中的数据库连接信息。
  • 启动Spring Boot应用。
  • 访问测试地址(如浏览器或Postman):http://localhost:8080/api/report/word/1,应下载Word文件。
  • 打开下载的文件,检查数据是否正确渲染。

十、总结

本示例完整展示了使用Spring Boot + MyBatis-Plus + POI-TL + Apache POI实现企业级Word和PPT导出的流程。要点如下:

  • POI-TL大幅简化Word生成代码,适合复杂文档。
  • 接口+实现类结构清晰,便于维护和测试。
  • 只读事务优化数据库查询。
  • 模板+数据模式分离样式与业务。
  • 响应头设置保证中文文件名正常下载。

可以基于此扩展更多功能,如批量导出、异步处理、使用消息队列等,以满足更高并发要求。

以上就是SpringBoot使用Apache POI实现导出Word和PPT的完整代码的详细内容,更多关于Apache POI导出Word和PPT的资料请关注脚本之家其它相关文章!

相关文章

  • 详解Java volatile 内存屏障底层原理语义

    详解Java volatile 内存屏障底层原理语义

    为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现 volatile 内存语义,JMM 会分别限制这两种类型的重排序类型
    2021-09-09
  • 使用HttpServletResponse对象获取请求行信息

    使用HttpServletResponse对象获取请求行信息

    这篇文章主要介绍了使用HttpServletResponse对象获取请求行信息,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • SpringBoot集成WebSocket实现后台向前端推送信息

    SpringBoot集成WebSocket实现后台向前端推送信息

    在一次项目开发中,使用到了Netty网络应用框架,以及MQTT进行消息数据的收发,这其中需要后台来将获取到的消息主动推送给前端,所以本文记录了SpringBoot集成WebSocket实现后台向前端推送信息的操作,需要的朋友可以参考下
    2024-02-02
  • java 浅析代码块的由来及用法

    java 浅析代码块的由来及用法

    所谓代码块是指用"{}"括起来的一段代码,根据其位置和声明的不同,可以分为普通代码块、构造块、静态块、和同步代码块。如果在代码块前加上 synchronized关键字,则此代码块就成为同步代码块
    2021-10-10
  • java实现简易局域网聊天功能

    java实现简易局域网聊天功能

    这篇文章主要为大家详细介绍了java实现简易局域网聊天功能,使用UDP模式编写一个聊天程序,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-04-04
  • java字符串与格式化输出的深入分析

    java字符串与格式化输出的深入分析

    本篇文章是对java字符串与格式化输出进行了详细的分析介绍,需要的朋友参考下
    2013-06-06
  • Java NIO中的零拷贝原理

    Java NIO中的零拷贝原理

    这篇文章主要介绍了Java NIO中的零拷贝原理,零拷贝即Zero-Copy,顾名思义,零拷贝是指的一种非拷贝的方式来减少IO次数的工作方式,零拷贝的作用就是减少IO,提高IO效率,需要的朋友可以参考下
    2023-11-11
  • Springboot整合Active消息队列

    Springboot整合Active消息队列

    这篇文章主要介绍了Springboot整合Active消息队列的步骤,帮助大家更好的理解和使用springboot框架,感兴趣的朋友可以了解下
    2020-12-12
  • 浅谈mybatis中的#和$的区别

    浅谈mybatis中的#和$的区别

    下面小编就为大家带来一篇浅谈mybatis中的#和$的区别。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • SpringCloud分布式事务Seata部署和集成过程

    SpringCloud分布式事务Seata部署和集成过程

    这篇文章主要介绍了SpringCloud分布式事务Seata部署和集成过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-10-10

最新评论