杰瑞科技汇

java web excel导入

  • 后端: 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 处理两个请求:

  1. 显示上传页面 (GET /)。
  2. 处理文件上传 (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: 使用 XSSFSAX 模式,需要引入 poi-ooxml-lite 依赖。
  • .xls: 使用 HSSFSAX 模式。

这是一个更高级的用法,但对于生产环境处理大文件至关重要,你需要编写一个 XSSFSheetXMLHandler 来处理解析事件。


运行与测试

  1. 启动项目: 运行你的 Spring Boot 主类。
  2. 访问页面: 在浏览器中打开 http://localhost:8080,你应该能看到上传页面。
  3. 下载模板: 点击 "下载模板" 按钮,下载 template.xlsx
  4. 填写数据: 在下载的模板中填写几条数据,可以故意写一些错误数据(如负数年龄、错误邮箱)来测试校验逻辑。
  5. 上传文件: 填写好数据后,选择该文件并点击 "上传并导入"。
  6. 查看结果:
    • 页面上会显示导入成功或失败的信息。
    • 后台控制台会打印出成功导入的数据和跳过的错误信息。

实现 Java Web Excel 导入的核心步骤如下:

  1. 选择工具: 使用 Apache POI。
  2. 创建前端: 使用 enctype="multipart/form-data" 的表单接收文件。
  3. 处理后端:
    • MultipartFile 接收文件流。
    • XSSFWorkbookHSSFWorkbook 解析 Excel。
    • 遍历 SheetRow,读取单元格数据到实体类。
    • 关键: 使用 DataFormatter 获取单元格原始值。
  4. 增强功能:
    • 校验: 对每行数据进行严格的业务校验。
    • 反馈: 提供清晰的导入成功/失败信息。
    • 模板: 提供模板文件下载。
    • 性能: 对大文件使用 SAX 模式解析。

这个方案已经非常完整,可以直接用于大多数中小型项目的开发中。

分享:
扫描分享到社交APP
上一篇
下一篇