Spring Boot 数据导入导出与报表生成实战
在企业级开发中,数据的交互往往比业务逻辑本身更繁琐。无论是从 Excel 批量导入用户信息,还是导出销售报表供管理层查看,都是高频需求。Spring Boot 生态提供了丰富的工具来简化这些工作,核心在于选择合适的库并规范代码结构。
数据导入导出的核心概念
数据导入导出本质上是在不同系统或格式间迁移数据的过程。常见的场景包括数据备份、系统迁移和共享。支持的格式通常有 CSV、Excel、JSON 和 XML。对于 Java 开发者而言,易用性、高效性和可靠性是选型的关键指标。
集成 Apache POI 处理 Excel
Apache POI 是操作 Office 文档的老牌库,在 Spring Boot 中集成它非常成熟。我们主要关注 .xlsx 格式的读写。
依赖配置
首先需要在 pom.xml 中添加必要的依赖。除了基础的 Web 和 JPA 支持外,POI 的核心包和 OOXML 包必不可少。为了支持文件上传测试,建议补充 Commons FileUpload 依赖。
<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>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- 文件上传支持 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
应用配置
H2 内存数据库适合演示,配置简单。记得开启控制台以便调试。
server.port=8080
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
实体与仓库层
定义一个产品实体,注意字段映射要准确。Repository 接口继承 JpaRepository 即可获得基础 CRUD 能力。
import javax.persistence.*;
import java.util.Objects;
@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
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; }
@Override
public String toString() {
return "Product{" +
"id=" + id +
", productId='" + productId + '\'' +
", productName='" + productName + '\'' +
", price=" + price +
", sales=" + sales +
'}';
}
}
业务逻辑实现
Service 层是核心。导入时需要注意跳过表头,导出时要设置列宽以便阅读。资源关闭(Workbook)务必放在 finally 块或使用 try-with-resources,防止内存泄漏。
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 = null;
try {
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);
} finally {
if (workbook != null) {
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();
}
}
控制器接口
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 报表
当需要生成固定格式的正式报表(如财务报表)时,Excel 可能不够美观,此时 JasperReports 是不错的选择。它支持复杂的布局设计和多种输出格式(PDF, HTML 等)。
依赖与模板
同样需要在 pom.xml 中添加 JasperReports 相关依赖。注意,JasperReports 通常需要预编译好的 .jasper 模板文件,或者在运行时加载 .jrxml 源码。
<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>
报表服务实现
Service 层负责加载模板、填充数据并导出为 PDF。这里假设模板文件位于 src/main/resources/reports/product-report.jasper。
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 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;
public byte[] generateProductReport() throws JRException {
List<Product> products = productRepository.findAll();
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);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JasperExportManager.exportReportToPdfStream(jasperPrint, outputStream);
return outputStream.toByteArray();
}
}
报表接口
Controller 中增加一个端点来触发报表生成。
@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");
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
return ResponseEntity.ok().headers(headers).body(bytes);
} catch (JRException e) {
e.printStackTrace();
return ResponseEntity.status(500).body(null);
}
}
实际应用场景总结
在实际项目中,这些功能通常组合使用。例如,允许管理员通过 Excel 批量更新库存,同时定期导出月度销售报表给财务部门。
- 产品信息管理:支持 Excel 导入导出,方便线下整理后批量入库。
- 用户信息管理:类似地用于会员数据迁移。
- 订单统计:结合 JasperReports 生成带图表的 PDF 订单汇总。
访问以下接口即可体验完整流程:
GET /api/products/export:下载产品信息 Excel 文件。POST /api/products/import:上传产品信息 Excel 文件,导入数据。GET /api/products/report:下载产品销售报表(PDF 格式)。
开发建议
- 异常处理:导入大文件时务必做好事务管理和资源释放,避免 OOM。
- 权限控制:导入导出接口涉及敏感数据,记得加上 Spring Security 拦截。
- 异步处理:如果数据量超过万条,建议将导入导出改为异步任务,避免阻塞主线程。
- 模板维护:JasperReports 的模板修改较为复杂,建议建立专门的模板设计流程。

