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的资料请关注脚本之家其它相关文章!
相关文章
使用HttpServletResponse对象获取请求行信息
这篇文章主要介绍了使用HttpServletResponse对象获取请求行信息,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-02-02
SpringBoot集成WebSocket实现后台向前端推送信息
在一次项目开发中,使用到了Netty网络应用框架,以及MQTT进行消息数据的收发,这其中需要后台来将获取到的消息主动推送给前端,所以本文记录了SpringBoot集成WebSocket实现后台向前端推送信息的操作,需要的朋友可以参考下2024-02-02


最新评论