Spring Boot 数据导入导出与报表生成
在业务开发中,数据的导入导出和报表生成是高频需求。无论是批量处理 Excel 数据,还是生成 PDF 统计报表,Spring Boot 都能提供高效的解决方案。本文将结合 Apache POI 和 JasperReports 两个主流库,演示如何在 Spring Boot 项目中实现这些功能。
核心概念与格式选择
数据导入导出本质上是系统间的数据迁移、备份或共享。常见的格式包括 CSV、Excel、JSON 和 XML。对于结构化较强的表格数据,Excel(.xlsx)是最常用的交互格式;而对于需要固定版式打印的报表,PDF 则是更好的选择。
集成这些功能通常具备易用性、高效性和可靠性的特点。在 Spring Boot 中,我们主要通过依赖注入和 RESTful 接口来封装这些逻辑。
使用 Apache POI 处理 Excel
Apache POI 是操作 Office 文档的经典库。在 Spring Boot 中集成它,主要涉及 Maven 依赖配置、实体类映射以及 Service 层的读写逻辑。
1. 依赖配置
首先需要在 pom.xml 中添加 Web、JPA、H2 数据库以及 POI 相关依赖:
<dependencies>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Data JPA 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 数据库依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Apache POI 依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2. 实体类设计
定义一个 Product 实体,用于映射数据库表结构。注意字段类型要与 Excel 单元格类型对应。
import javax.persistence.*;
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productId;
private String productName;
private double price;
private int sales;
public Product() {}
public Product(String productId, String productName, double price, int sales) {
this.productId = productId;
this.productName = productName;
this.price = price;
this.sales = sales;
}
// Getter 和 Setter 方法省略,实际开发中建议使用 Lombok 简化
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getSales() { return sales; }
public void setSales(int sales) { this.sales = sales; }
}
3. Service 层实现
Service 层负责核心的文件解析与写入逻辑。读取 Excel 时,需跳过标题行并逐行解析;导出时,则遍历集合填充单元格。
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional
public void importProducts(MultipartFile file) throws IOException {
List<Product> products = new ArrayList<>();
Workbook workbook = new XSSFWorkbook(file.getInputStream());
Sheet sheet = workbook.getSheetAt(0);
Iterator<Row> iterator = sheet.iterator();
if (iterator.hasNext()) {
iterator.next(); // 跳过标题行
}
while (iterator.hasNext()) {
Row currentRow = iterator.next();
Product product = new Product();
product.setProductId(currentRow.getCell(0).getStringCellValue());
product.setProductName(currentRow.getCell(1).getStringCellValue());
product.setPrice(currentRow.getCell(2).getNumericCellValue());
product.setSales((int) currentRow.getCell(3).getNumericCellValue());
products.add(product);
}
productRepository.saveAll(products);
workbook.close();
}
@Transactional(readOnly = true)
public byte[] exportProducts() throws IOException {
List<Product> products = productRepository.findAll();
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Products");
Row headerRow = sheet.createRow(0);
Cell headerCell0 = headerRow.createCell(0); headerCell0.setCellValue("Product ID");
Cell headerCell1 = headerRow.createCell(1); headerCell1.setCellValue("Product Name");
Cell headerCell2 = headerRow.createCell(2); headerCell2.setCellValue("Price");
Cell headerCell3 = headerRow.createCell(3); headerCell3.setCellValue("Sales");
int rowNum = 1;
for (Product product : products) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(product.getProductId());
row.createCell(1).setCellValue(product.getProductName());
row.createCell(2).setCellValue(product.getPrice());
row.createCell(3).setCellValue(product.getSales());
}
// 自动调整列宽
for (int i = 0; i < 4; i++) {
sheet.autoSizeColumn(i);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
workbook.close();
return outputStream.toByteArray();
}
@Transactional
public Product addProduct(Product product) {
return productRepository.save(product);
}
@Transactional(readOnly = true)
public List<Product> getAllProducts() {
return productRepository.findAll();
}
}
4. Controller 层封装
Controller 负责接收请求并返回响应。导出时需要设置正确的 Content-Type 和 Content-Disposition 头,确保浏览器能下载文件。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/import")
public ResponseEntity<String> importProducts(@RequestParam("file") MultipartFile file) {
try {
productService.importProducts(file);
return ResponseEntity.ok("数据导入成功");
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(500).body("数据导入失败:" + e.getMessage());
}
}
@GetMapping("/export")
public ResponseEntity<byte[]> exportProducts() {
try {
byte[] bytes = productService.exportProducts();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
headers.setContentDispositionFormData("attachment", "products.xlsx");
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
return ResponseEntity.ok().headers(headers).body(bytes);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(500).body(null);
}
}
@PostMapping("/")
public Product addProduct(@RequestBody Product product) {
return productService.addProduct(product);
}
@GetMapping("/")
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
}
使用 JasperReports 生成 PDF 报表
当需要生成固定格式的统计报表时,JasperReports 是更专业的选择。它支持复杂的布局设计和多种输出格式(如 PDF)。
1. 依赖与模板
添加 JasperReports 依赖后,你需要准备一个 .jrxml 模板文件,编译为 .jasper 文件放在 resources 目录下。
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.17.0</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-fonts</artifactId>
<version>6.17.0</version>
</dependency>
2. Service 层实现
加载模板、设置参数和数据源,然后填充生成报表。
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ReportService {
@Autowired
private ProductRepository productRepository;
@Transactional(readOnly = true)
public byte[] generateProductReport() throws JRException {
List<Product> products = productRepository.findAll();
// 加载 Jasper 模板
InputStream templateStream = getClass().getResourceAsStream("/reports/product-report.jasper");
JasperReport jasperReport = (JasperReport) JRLoader.loadObject(templateStream);
// 设置参数
Map<String, Object> parameters = new HashMap<>();
parameters.put("Title", "产品销售报表");
// 设置数据源
JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(products);
// 生成报表
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, dataSource);
// 导出为 PDF
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JasperExportManager.exportReportToPdfStream(jasperPrint, outputStream);
return outputStream.toByteArray();
}
}
3. Controller 调用
只需新增一个接口调用 ReportService 即可。
@GetMapping("/report")
public ResponseEntity<byte[]> generateProductReport() {
try {
byte[] bytes = reportService.generateProductReport();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/pdf"));
headers.setContentDispositionFormData("attachment", "product-report.pdf");
return ResponseEntity.ok().headers(headers).body(bytes);
} catch (JRException e) {
e.printStackTrace();
return ResponseEntity.status(500).body(null);
}
}
总结
通过上述示例,我们可以看到 Spring Boot 在处理数据导入导出和报表生成方面的灵活性。对于 Excel 场景,Apache POI 提供了底层的 API 控制;对于复杂报表,JasperReports 则提供了强大的模板引擎能力。在实际开发中,根据业务需求选择合适的工具,并注意异常处理和资源关闭,就能构建出稳定可靠的数据服务模块。

