杰瑞科技汇

Java Web如何高效导入Excel数据?

核心思路

在 Java Web 应用中导入 Excel 的核心流程如下:

Java Web如何高效导入Excel数据?-图1
(图片来源网络,侵删)
  1. 前端页面:提供一个文件上传的表单,让用户选择本地的 Excel 文件。
  2. 后端接收:服务器端的 Servlet 或 Controller 接收前端上传的文件。
  3. 解析 Excel:使用专门的库(如 Apache POI 或 EasyExcel)来读取 Excel 文件的内容。
  4. 数据处理:将读取到的数据(通常是字符串或数字)转换成你业务需要的 Java 对象(Entity/DTO)。
  5. 业务处理:将转换后的数据列表保存到数据库,或进行其他业务逻辑操作。
  6. 响应结果:向前端返回一个操作结果,例如成功、失败、失败原因等。

技术选型

选择合适的库是成功的一半,目前最主流的两个库是:

特性 Apache POI EasyExcel (阿里巴巴)
简介 老牌、功能强大的 Java API,用于操作 Microsoft Office 格式文件。 阿里巴巴开源,基于 POI 封装,解决了 POI 内存消耗大的问题。
优点 功能极其全面,支持所有 Office 格式。 内存占用极低,性能好,适合处理大文件,API 更简洁。
缺点 内存消耗巨大,处理大文件时容易导致 OOM (Out of Memory)。 相对 POI,功能上可能稍弱(但已覆盖绝大多数场景)。
适用场景 文件较小,功能要求复杂。 强烈推荐,特别是生产环境和处理大文件时。

对于新项目,强烈推荐使用 EasyExcel,本教程将以 EasyExcel 为主进行讲解,最后也会给出 POI 的示例。


详细实现步骤 (以 EasyExcel 为例)

我们将创建一个标准的 Maven 项目,实现一个用户信息导入功能。

步骤 1:添加 Maven 依赖

在你的 pom.xml 文件中添加 EasyExcel 和 Web 相关的依赖。

Java Web如何高效导入Excel数据?-图2
(图片来源网络,侵删)
<dependencies>
    <!-- Spring Boot Starter Web (如果你用 Spring Boot) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- EasyExcel 核心依赖 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.3.2</version> <!-- 建议使用最新版本 -->
    </dependency>
    <!-- Lombok (简化 Java 代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

步骤 2:准备 Excel 模板和数据

假设我们要导入用户信息,Excel 文件 users.xlsx 内容如下:

姓名 年龄 邮箱
张三 25 zhangsan@example.com
李四 30 lisi@example.com
王五 28 wangwu@example.com

步骤 3:创建实体类

创建一个与 Excel 行数据对应的 Java 实体类,使用 EasyExcel 的注解来映射列。

User.java

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
@Data // Lombok 自动生成 getter, setter, toString 等
public class User {
    // value 对应 Excel 中的列名,index 对应列的索引(从0开始)
    // 两者指定一个即可,推荐使用 value
    @ExcelProperty("姓名")
    private String name;
    @ExcelProperty("年龄")
    private Integer age;
    @ExcelProperty("邮箱")
    private String email;
}

步骤 4:创建 Controller 处理文件上传

这是核心部分,负责接收文件、调用解析器并返回结果。

ExcelImportController.java

import com.alibaba.excel.EasyExcel;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/api/excel")
public class ExcelImportController {
    /**
     * 导入 Excel
     * @param file 前端上传的文件
     */
    @PostMapping("/import")
    public String importExcel(@RequestParam("file") MultipartFile file) throws IOException {
        // 1. 参数校验
        if (file == null || file.isEmpty()) {
            return "文件不能为空!";
        }
        // 2. 获取文件名
        String originalFilename = file.getOriginalFilename();
        if (!originalFilename.endsWith(".xlsx") && !originalFilename.endsWith(".xls")) {
            return "文件格式不正确,请上传 Excel 文件!";
        }
        try {
            // 3. 使用 EasyExcel 读取文件
            // file.getInputStream() 获取文件输入流
            // User.class 指定映射的实体类
            // new PageReadListener() 监听器,每读取一行数据,就会回调一次
            List<User> userList = EasyExcel.read(file.getInputStream(), User.class, new PageReadListener<User>(dataList -> {
                // dataList 是每次读取到的数据列表(默认100条一批)
                System.out.println("解析到一条数据: " + dataList.size());
                // 在这里可以进行业务处理,比如批量保存到数据库
                // userService.saveBatch(dataList);
            })).sheet().doRead();
            System.out.println("所有数据解析完成,总条数: " + userList.size());
            return "成功导入 " + userList.size() + " 条数据!";
        } catch (Exception e) {
            e.printStackTrace();
            return "导入失败: " + e.getMessage();
        }
    }
}

步骤 5:创建前端页面 (HTML + JavaScript)

创建一个简单的 HTML 页面用于上传文件。

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">Excel 导入示例</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .container { max-width: 600px; margin: auto; padding: 20px; border: 1px solid #ccc; border-radius: 5px; }
        input[type="file"] { margin-bottom: 10px; }
        button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background-color: #0056b3; }
        #result { margin-top: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
    </style>
</head>
<body>
    <div class="container">
        <h2>Excel 用户信息导入</h2>
        <input type="file" id="fileInput" accept=".xlsx, .xls">
        <button onclick="uploadFile()">上传并导入</button>
        <div id="result"></div>
    </div>
    <script>
        function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const resultDiv = document.getElementById('result');
            if (fileInput.files.length === 0) {
                resultDiv.innerHTML = '<p style="color: red;">请先选择一个文件!</p>';
                return;
            }
            const file = fileInput.files[0];
            const formData = new FormData();
            formData.append('file', file);
            resultDiv.innerHTML = '<p>正在上传...</p>';
            fetch('/api/excel/import', {
                method: 'POST',
                body: formData
            })
            .then(response => response.text())
            .then(data => {
                resultDiv.innerHTML = `<p style="color: green;">${data}</p>`;
            })
            .catch(error => {
                resultDiv.innerHTML = `<p style="color: red;">上传失败: ${error}</p>`;
            });
        }
    </script>
</body>
</html>

步骤 6:配置 Spring Boot (如果使用)

application.propertiesapplication.yml 中配置文件上传的大小限制,默认是 1MB,对于 Excel 文件来说太小了。

application.properties

# 配置单个文件上传大小
spring.servlet.multipart.max-file-size=10MB
# 配置总请求大小(如果同时上传多个文件)
spring.servlet.multipart.max-request-size=10MB

高级功能与最佳实践

复杂表头处理

Excel 表头是分层的,可以使用 Head 对象。

Excel 表头: | 用户信息 | | | | :--- | :--- | :--- | | 姓名 | 年龄 | 邮箱 |

实体类修改:

@Data
public class User {
    // 第一列表头是 "用户信息",第二列表头是 "姓名"
    @ExcelProperty({"用户信息", "姓名"})
    private String name;
    @ExcelProperty({"用户信息", "年龄"})
    private Integer age;
    @ExcelProperty({"用户信息", "邮箱"})
    private String email;
}

数据校验与自定义转换

EasyExcel 支持集成 Spring Validation 进行数据校验。

实体类添加校验注解:

import javax.validation.constraints.*;
@Data
public class User {
    @NotBlank(message = "姓名不能为空")
    @ExcelProperty("姓名")
    private String name;
    @Min(value = 18, message = "年龄不能小于18岁")
    @Max(value = 100, message = "年龄不能大于100岁")
    @ExcelProperty("年龄")
    private Integer age;
    @Email(message = "邮箱格式不正确")
    @ExcelProperty("邮箱")
    private String email;
}

Controller 修改 (使用 @Validated):

@PostMapping("/import")
public String importExcel(@RequestParam("file") MultipartFile file, BindingResult result) {
    // ... 前面的代码 ...
    EasyExcel.read(..., new AnalysisEventListener<User>() {
        @Override
        public void invoke(User user, AnalysisContext context) {
            // 这里可以手动校验
            if (user.getName() == null || user.getName().trim().isEmpty()) {
                // 可以将错误信息收集起来,最后统一返回
                throw new RuntimeException("第 " + context.readRowHolder().getRowIndex() + " 行姓名不能为空");
            }
            // ... 业务处理 ...
        }
        // ... 其他方法 ...
    });
}

错误信息收集与返回

实际应用中,我们希望知道具体哪一行、哪个字段出了错,而不是直接抛出异常中断。

修改后的监听器:

// 在 Controller 中
public String importExcel(@RequestParam("file") MultipartFile file) throws IOException {
    // ...
    List<String> errorMessages = new ArrayList<>();
    EasyExcel.read(file.getInputStream(), User.class, new AnalysisEventListener<User>() {
        // 每解析一行都会调用
        @Override
        public void invoke(User user, AnalysisContext context) {
            // 模拟业务校验
            if (user.getAge() != null && user.getAge() < 18) {
                int rowNum = context.readRowHolder().getRowIndex() + 1; // Excel 行号从1开始
                errorMessages.add("第 " + rowNum + " 行,年龄不能小于18岁");
                return; // 跳过这一行,不继续处理
            }
            // ... 正常处理 ...
        }
        // 所有数据解析完成后调用
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            // 解析完成,可以在这里进行数据库批量保存等操作
            if (errorMessages.isEmpty()) {
                System.out.println("所有数据校验通过,准备入库...");
            }
        }
    }).sheet().doRead();
    if (!errorMessages.isEmpty()) {
        return "导入失败,原因如下:<br>" + String.join("<br>", errorMessages);
    }
    return "导入成功!";
}

使用 Apache POI 示例

如果你因为某些原因必须使用 POI,以下是核心代码对比。

Maven 依赖

<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>

POI 解析代码 (事件模型 SAX)

为了不 OOM,POI 也推荐使用事件模型(SAX)来解析大文件,这和 EasyExcel 的监听器模式非常相似。

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFReader.SheetIterator;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class PoiExcelReader {
    public static List<User> read(InputStream inputStream) throws Exception, InvalidFormatException {
        List<User> userList = new ArrayList<>();
        XSSFReader xssfReader = new XSSFReader(inputStream);
        SharedStringsTable sst = xssfReader.getSharedStringsTable();
        SheetIterator sheetIterator = (SheetIterator) xssfReader.getSheetsData();
        if (sheetIterator.hasNext()) {
            InputStream sheetStream = sheetIterator.next();
            SheetHandler sheetHandler = new SheetHandler(sst, userList);
            XMLReader parser = XMLReaderFactory.createXMLReader();
            parser.setContentHandler(sheetHandler);
            parser.parse(new InputSource(sheetStream));
        }
        return userList;
    }
}
// 自定义的 SheetHandler
class SheetHandler implements ContentHandler {
    // ... 省略了复杂的 SAX 解析逻辑 ...
    // 需要自己处理 XML 事件,解析出单元格数据和字符串表,
    // 然后映射到 User 对象中,代码量比 EasyExcel 多很多。
}

可以看到,POI 的原生实现非常繁琐,这也是为什么 EasyExcel 等封装库如此受欢迎的原因。

功能点 EasyExcel (推荐) Apache POI
易用性 极高,注解 + 监听器模式,代码简洁。 较低,原生 API 复杂,大文件解析需要自己实现 SAX。
性能 优秀,内存占用低,流式读取。 一般,HSSF (.xls) 内存占用大,XSSF (.xlsx) 可以用 SAX 模式优化。
功能 覆盖 90%+ 常见需求,如复杂表头、数据校验等。 功能最全,支持所有 Office 特有格式。
学习成本

对于绝大多数 Java Web 项目,直接选择 EasyExcel 可以让你快速、高效、稳定地完成 Excel 导入功能。

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