- 后端: Spring Boot + Apache POI
- 前端: Thymeleaf (模板引擎) + Bootstrap (UI框架)
- 构建工具: Maven
技术选型与准备工作
为什么选择 Apache POI?
Apache POI 是 Java 操作 Office 格式文件最强大的开源库,它支持 Excel 的 .xls (旧版) 和 .xlsx (新版) 格式。
.xls: 使用HSSFWorkbook类,属于 POI 的 HSSF 项目。.xlsx: 使用XSSFWorkbook类,属于 POI 的 XSSF 项目。
项目创建与依赖
创建一个 Spring Boot 项目,并添加以下依赖 (pom.xml):
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Apache POI for Excel (支持 .xlsx) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version> <!-- 建议使用较新版本 -->
</dependency>
<!-- Lombok (简化 getter/setter) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
前端页面实现
前端需要一个文件上传表单,我们使用 Thymeleaf 和 Bootstrap 来创建一个美观的页面。
1 创建 Excel 模板文件
为了让用户知道要导入什么格式的数据,我们先创建一个模板文件 template.xlsx。
假设我们要导入用户信息,包含 姓名、年龄 和 邮箱 三列。
| 姓名 | 年龄 | 邮箱 |
|---|---|---|
| 张三 | 25 | zhangsan@example.com |
| 李四 | 30 | lisi@example.com |
将这个 template.xlsx 文件放在 src/main/resources/templates/ 目录下。
2 创建 HTML 上传页面 (upload.html)
在 src/main/resources/templates/ 目录下创建 upload.html 文件。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">Excel 导入示例</title>
<!-- 引入 Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { padding-top: 50px; }
.container { max-width: 600px; }
</style>
</head>
<body>
<div class="container">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">用户信息导入</h3>
</div>
<div class="panel-body">
<!-- 提示信息 -->
<div th:if="${message}" class="alert alert-info" th:text="${message}"></div>
<!-- 上传表单 -->
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="file">选择 Excel 文件:</label>
<input type="file" id="file" name="file" class="form-control" accept=".xlsx, .xls" required>
</div>
<br>
<div class="form-group">
<a th:href="@{/downloadTemplate}" class="btn btn-default">下载模板</a>
<button type="submit" class="btn btn-primary">上传并导入</button>
</div>
</form>
</div>
</div>
</div>
<!-- 引入 Bootstrap JS (可选) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
后端核心逻辑实现
1 定义数据模型
创建一个 User 类,与 Excel 中的列对应。
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import lombok.Data;
@Data // Lombok 自动生成 getter, setter, toString 等
public class User {
private String name;
private Integer age;
private String email;
}
2 创建 Controller
Controller 处理两个请求:
- 显示上传页面 (
GET /)。 - 处理文件上传 (
POST /upload)。
// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@Controller
public class UserController {
@GetMapping("/")
public String index() {
return "upload"; // 返回 upload.html 模板
}
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file, Model model) {
String message = "";
List<User> users = new ArrayList<>();
// 1. 检查文件是否为空
if (file.isEmpty()) {
message = "请选择一个文件!";
model.addAttribute("message", message);
return "upload";
}
// 2. 检查文件类型
String contentType = file.getContentType();
if (!"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet".equals(contentType) &&
!"application/vnd.ms-excel".equals(contentType)) {
message = "请上传 Excel 文件 (.xlsx 或 .xls)!";
model.addAttribute("message", message);
return "upload";
}
// 3. 使用 POI 读取 Excel 文件
try (InputStream inputStream = file.getInputStream();
Workbook workbook = new XSSFWorkbook(inputStream)) { // 使用 XSSF 支持 .xlsx
Sheet sheet = workbook.getSheetAt(0); // 获取第一个工作表
DataFormatter dataFormatter = new DataFormatter(); // 用于读取单元格原始值,避免数字格式问题
// 从第二行开始遍历 (跳过表头)
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
continue; // 跳过空行
}
User user = new User();
// 读取姓名 (第一列)
user.setName(dataFormatter.formatCellValue(row.getCell(0)));
// 读取年龄 (第二列)
String ageStr = dataFormatter.formatCellValue(row.getCell(1));
try {
user.setAge(Integer.parseInt(ageStr));
} catch (NumberFormatException e) {
user.setAge(null); // 如果年龄不是数字,设为 null
}
// 读取邮箱 (第三列)
user.setEmail(dataFormatter.formatCellValue(row.getCell(2)));
users.add(user);
}
// 4. 处理数据 (这里只是打印到控制台,实际项目中应该保存到数据库)
System.out.println("成功导入 " + users.size() + " 条数据:");
users.forEach(System.out::println);
message = "成功导入 " + users.size() + " 条数据!";
} catch (IOException e) {
message = "文件处理失败: " + e.getMessage();
e.printStackTrace();
} catch (Exception e) {
message = "发生未知错误: " + e.getMessage();
e.printStackTrace();
}
model.addAttribute("message", message);
model.addAttribute("users", users); // 可以将导入的数据传回前端显示
return "upload";
}
@GetMapping("/downloadTemplate")
public void downloadTemplate() {
// 这里使用 HttpServletResponse 来实现文件下载
// 为了简化示例,这里只返回一个提示,实际开发中需要实现下载逻辑
// 具体实现见下方 "进阶功能" 部分
}
}
代码解析:
@RequestParam("file") MultipartFile file: Spring MVC 自动将上传的文件绑定到MultipartFile对象。Workbook workbook = new XSSFWorkbook(inputStream): 创建XSSFWorkbook对象来处理.xlsx文件,如果是.xls,则使用HSSFWorkbook。Sheet sheet = workbook.getSheetAt(0): 获取 Excel 文件中的第一个工作表。for (int i = 1; ...): 我们通常从第二行开始读取,因为第一行是表头。DataFormatter: 这是一个非常实用的工具类,它能以字符串形式返回单元格的原始值,避免了getCell().getNumericCellValue()这样的方法会因为单元格格式(如日期、百分比)而返回非预期值的问题。try-catch: 文件操作必须放在try-catch块中,并妥善处理可能发生的IOException。
进阶功能与最佳实践
1 下载模板文件
用户需要一个模板文件来参考格式,我们需要提供一个下载链接。
修改 UserController,添加下载方法:
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
// ... 其他 import
@Controller
public class UserController {
// ... 其他方法
@GetMapping("/downloadTemplate")
public ResponseEntity<Resource> downloadTemplate() {
// 模板文件在 resources/templates/ 目录下
Resource resource = new ClassPathResource("templates/template.xlsx");
if (!resource.exists()) {
// 如果文件不存在,返回 404 错误
return ResponseEntity.notFound().build();
}
String contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
String headerValue = "attachment; filename=\"" + resource.getFilename() + "\"";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, headerValue)
.body(resource);
}
}
2 数据校验
导入数据时,必须进行校验,年龄不能为负数,邮箱格式必须正确。
在上面的 UserController 的循环读取部分,加入校验逻辑:
// ... 在 for 循环内部
User user = new User();
user.setName(dataFormatter.formatCellValue(row.getCell(0)));
String ageStr = dataFormatter.formatCellValue(row.getCell(1));
try {
int age = Integer.parseInt(ageStr);
if (age < 0) {
System.err.println("第 " + (i+1) + " 行: 年龄不能为负数,已跳过。");
continue; // 跳过这一行
}
user.setAge(age);
} catch (NumberFormatException e) {
System.err.println("第 " + (i+1) + " 行: 年龄格式不正确,已跳过。");
continue; // 跳过这一行
}
user.setEmail(dataFormatter.formatCellValue(row.getCell(2)));
// 邮箱格式校验 (简单示例)
if (!user.getEmail().matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
System.err.println("第 " + (i+1) + " 行: 邮箱格式不正确,已跳过。");
continue;
}
users.add(user);
3 错误信息回显
如果某一行数据格式错误,我们不应该中断整个导入过程,而是应该记录下所有错误行和错误信息,并在导入完成后统一反馈给用户。
可以创建一个 ImportResult 类来封装导入结果:
// src/main/java/com/example/demo/model/ImportResult.java
package com.example.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ImportResult<T> {
private List<T> successData = new ArrayList<>();
private List<String> errorMessages = new ArrayList<>();
private int successCount;
private int errorCount;
public void addSuccess(T data) {
successData.add(data);
successCount++;
}
public void addError(String errorMessage) {
errorMessages.add(errorMessage);
errorCount++;
}
}
然后在 Controller 中使用这个类来收集结果,并最终将错误信息展示在页面上。
4 大文件处理与性能优化
对于非常大的 Excel 文件(如几十万行),一次性加载到内存可能会导致 OutOfMemoryError。
解决方案:使用 SAX 模式 (Event API) POI 提供了基于 SAX 的解析模式,它不会一次性将整个文件加载到内存,而是逐行解析,内存占用非常小。
.xlsx: 使用XSSF和SAX模式,需要引入poi-ooxml-lite依赖。.xls: 使用HSSF和SAX模式。
这是一个更高级的用法,但对于生产环境处理大文件至关重要,你需要编写一个 XSSFSheetXMLHandler 来处理解析事件。
运行与测试
- 启动项目: 运行你的 Spring Boot 主类。
- 访问页面: 在浏览器中打开
http://localhost:8080,你应该能看到上传页面。 - 下载模板: 点击 "下载模板" 按钮,下载
template.xlsx。 - 填写数据: 在下载的模板中填写几条数据,可以故意写一些错误数据(如负数年龄、错误邮箱)来测试校验逻辑。
- 上传文件: 填写好数据后,选择该文件并点击 "上传并导入"。
- 查看结果:
- 页面上会显示导入成功或失败的信息。
- 后台控制台会打印出成功导入的数据和跳过的错误信息。
实现 Java Web Excel 导入的核心步骤如下:
- 选择工具: 使用 Apache POI。
- 创建前端: 使用
enctype="multipart/form-data"的表单接收文件。 - 处理后端:
- 用
MultipartFile接收文件流。 - 用
XSSFWorkbook或HSSFWorkbook解析 Excel。 - 遍历
Sheet和Row,读取单元格数据到实体类。 - 关键: 使用
DataFormatter获取单元格原始值。
- 用
- 增强功能:
- 校验: 对每行数据进行严格的业务校验。
- 反馈: 提供清晰的导入成功/失败信息。
- 模板: 提供模板文件下载。
- 性能: 对大文件使用 SAX 模式解析。
这个方案已经非常完整,可以直接用于大多数中小型项目的开发中。
