Skip to content

EasyExcel 导入 ​

简介 ​

背景 ​

在数据处理和分析的过程中,读取和解析 Excel 文件是一个非常常见的需求。在 Java 开发中,有许多库可以帮助我们实现这一功能。其中,阿里巴巴的 EasyExcel 库因其高性能和易用性,备受开发者青睐。本文将详细介绍如何使用 EasyExcel 读取 Excel 文件,并将内容转换为自定义的数据结构 TableInput。

我们将通过具体的代码示例,演示如何读取 Excel 文件中的数据,并将其转换为自定义对象,方便后续的数据处理和存储。

依赖项 ​

在开始之前,确保您的项目中已经引入了 api-table 和 EasyExcel 的依赖。以下是在 Maven 项目中添加依赖的方式:

xml
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>4.0.3</version>
</dependency>
<dependency>
  <groupId>nexus.io</groupId>
  <artifactId>api-table</artifactId>
  <version>1.6.2</version>
</dependency>

注意:EasyExcel 依赖于 Apache POI,这可能会增加项目的打包体积。在使用时请注意项目的大小和性能。

异步导入 ​

数据示例 ​

id	name	api_key	creator	create_time	updater	update_time	deleted	tenant_id	provider
1		AIzaSyDQ3		5/6/2025 7:46		5/6/2025 7:46	0	0	google
3		AIzaSyAB		5/6/2025 8:56		5/6/2025 8:56	0	0	google
4		AIzaSyC		5/6/2025 8:56		5/6/2025 8:56	0	0	google
5		AIzaSyBB		5/6/2025 8:57		5/6/2025 8:57	0	0	google
7		sk-ant-api0Zc		5/19/2025 23:31		5/19/2025 23:31	0	0	anthropic
8		sk-ant-api03-q		5/19/2025 23:31		5/19/2025 23:31	0	0	anthropic
6		sk-ant-api03-Y		5/19/2025 23:31		5/19/2025 23:31	0	0	anthropic
9		17bbdcc2		5/20/2025 5:23		5/20/2025 5:23	0	0	volcengine

数据表结构 ​

sql
drop table if exists llm_api_key;
CREATE TABLE llm_api_key (
  id BIGINT PRIMARY KEY,
  name VARCHAR,
  provider varchar,
  api_key varchar,
  "creator" VARCHAR ( 64 ) DEFAULT '',
  "create_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "updater" VARCHAR ( 64 ) DEFAULT '',
  "update_time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "deleted" SMALLINT DEFAULT 0,
  "tenant_id" BIGINT NOT NULL DEFAULT 0
);

示例代码 ​

以下是一个完整的示例,演示如何使用 EasyExcel 异步读取 Excel 文件,并处理其内容:

java
package nexus.io.manim.services;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.tio.utils.hutool.FileUtil;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LLMKeyService {

  public void importKey() {
    File file = new File("data/llm_api_key.xlsx");
    byte[] bytes = FileUtil.readBytes(file);
    ByteArrayInputStream in = new ByteArrayInputStream(bytes);

    AnalysisEventListener<Map<Integer, Object>> readListener = new AnalysisEventListener<Map<Integer, Object>>() {
      private Map<Integer, String> headMap;
      // 用到的时间格式,如果的 Excel 里是 "5/6/2025 7:46":

      private final DateTimeFormatter FMT_DASH = DateTimeFormatter.ofPattern("yyyy-M-d H:mm");
      private final DateTimeFormatter FMT_SLASH = DateTimeFormatter.ofPattern("M/d/yyyy H:mm");

      @Override
      public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext ctx) {
        this.headMap = headMap;
      }

      @Override
      public void invoke(Map<Integer, Object> data, AnalysisContext ctx) {
        Row row = Row.create();
        for (Entry<Integer, Object> e : data.entrySet()) {
          String col = headMap.get(e.getKey());
          Object val = e.getValue();
          if (val == null) {
            continue;
          }

          if ("id".equals(col) || "tenant_id".equals(col)) {
            val = Long.valueOf(val.toString());

          } else if ("deleted".equals(col)) {
            val = Integer.valueOf(val.toString());

          } else if ("create_time".equals(col) || "update_time".equals(col)) {
            if (val instanceof Date) {
              val = new Timestamp(((Date) val).getTime());
            } else if (val instanceof String) {
              String s = ((String) val).trim();
              LocalDateTime ldt;

              // 根据分隔符选择解析器
              if (s.contains("/")) {
                ldt = LocalDateTime.parse(s, FMT_SLASH);
              } else if (s.contains("-")) {
                ldt = LocalDateTime.parse(s, FMT_DASH);
              } else {
                throw new IllegalArgumentException("无法识别的日期格式: " + s);
              }
              val = Timestamp.valueOf(ldt);
            }
          }

          row.set(col, val);
        }
        Db.save("llm_api_key", row);
      }

      @Override
      public void doAfterAllAnalysed(AnalysisContext ctx) {
        log.info("所有数据解析完成");
      }
    };

    ExcelReaderBuilder reader = EasyExcel.read(in, readListener);
    reader.doReadAll();
  }
}

解析说明 ​

  1. 读取 Excel 文件:通过 ResourceUtil.getResource 获取 Excel 文件的 URL,然后使用 FileUtil.readUrlAsBytes 读取文件的字节内容。

  2. 定义解析监听器:实现 AnalysisEventListener 接口,重写 invokeHeadMap、invoke 和 doAfterAllAnalysed 方法。

    • invokeHeadMap:用于处理表头数据,保存表头的映射关系。
    • invoke:用于处理每一行的数据,将行数据与表头对应,转换为 TableInput 对象。
    • doAfterAllAnalysed:在所有数据解析完成后执行,可以在此进行一些收尾工作。
  3. 开始读取:使用 EasyExcel.read 方法创建读取器,并调用 doReadAll 方法开始读取所有数据。

示例输出 ​

仅仅是示例输出,和真是的数据有较大差距,仅供参考

表头数据示例:

json
{
  "0": "id",
  "1": "orders",
  "2": "title",
  "3": "content",
  "4": "category_id",
  "5": "summary",
  "6": "locale",
  "7": "status",
  "8": "files",
  "9": "remark",
  "10": "creator",
  "11": "create_time",
  "12": "updater",
  "13": "update_time",
  "14": "deleted",
  "15": "tenant_id"
}

行数据示例:

json
{
  "1": "1",
  "2": "测试展示视频",
  "3": "",
  "4": "400537606909092000",
  "6": "zh_cn",
  "7": "1",
  "8": "[{\"uid\":\"1720576961389\",\"size\":3103640,\"name\":\"image.png\",\"id\":\"400560112936251392\",\"type\":\"image/png\",\"url\":\"https://example.com/image.png\",\"status\":\"done\"}]"
}

转换后的 TableInput 数据:

json
{
  "title": "测试展示视频",
  "locale": "zh_cn",
  "content": "",
  "category_id": "400537606909092000",
  "files": "[{\"uid\":\"1720576961389\",\"size\":3103640,\"name\":\"image.png\",\"id\":\"400560112936251392\",\"type\":\"image/png\",\"url\":\"https://example.com/image.png\",\"status\":\"done\"}]",
  "orders": "1",
  "status": "1"
}

通过上述代码和示例,我们成功地将 Excel 文件中的数据读取并转换为自定义的数据结构 TableInput,为后续的数据处理和存储做好了准备。

同步导入 ​

使用 doReadAllSync 方法同步读取 ​

除了异步读取外,EasyExcel 还提供了同步读取的方法。以下示例展示了如何使用 doReadAllSync 方法同步读取 Excel 数据:

java
import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.product.config.AdminAppConfig;
import nexus.io.table.utils.ExcelParseUtils;
import nexus.io.table.utils.PoiExcelUtils;
import nexus.io.tio.boot.testing.TioBootTest;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.tio.utils.json.JsonUtils;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

public class ReadExcelDoReadAllSyncTest {

  @Test
  public void readFirstRow() {
    byte[] bytes = FileUtil.readBytes(new File("data/kehe rough draft.xlsx"));
    List<Object> head = PoiExcelUtils.readFirstRow(bytes);
    System.out.println(JsonUtils.toJson(head));
  }

  @Test
  public void importProducts() {
    TioBootTest.runWith(AdminAppConfig.class);
    // 读取 Excel 文件的字节内容
    byte[] bytes = FileUtil.readBytes(new File("data/kehe rough draft.xlsx"));

    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

    // 构建 Excel 读取器
    ExcelReaderBuilder readBuilder = EasyExcel.read(byteArrayInputStream);
    // 同步读取数据
    ExcelReaderSheetBuilder sheet = readBuilder.sheet();

    List<Map<Integer, Object>> tableInputList = sheet.doReadSync();
    //tableInputList = sheet.doReadAllSync();

    System.out.println("读取的数据条数:" + tableInputList.size());
    System.out.println("数据内容:" + JsonUtils.toJson(tableInputList.get(0)));

    List<Row> rows = new ArrayList<>();
    for (Map<Integer, Object> map : tableInputList) {
      Row row = Row.by("id", SnowflakeIdUtils.id());
      Object asin = map.get(0);
      if (asin == null) {
        continue;
      }
      row.set("asin", asin);
      row.set("upc", map.get(1));
      row.set("brand", map.get(2));
      row.set("kehe_sku", map.get(3));
      row.set("description", map.get(4));
      row.set("size", map.get(5));

      row.set("unit_of_sale", map.get(6));
      row.set("case_pack", Integer.valueOf(map.get(7).toString()));

      row.set("cost", ExcelParseUtils.toBigDecimal(map.get(8)));

      // multi_pack_quantity -> INTEGER
      row.set("multi_pack_quantity", ExcelParseUtils.toInteger(map.get(9)));
      row.set("t12_reviews", ExcelParseUtils.toInteger(map.get(10)));

      // asin_cost, price, buy_box_90_days, buy_box_current, new_90_days 都是 NUMERIC
      row.set("asin_cost", ExcelParseUtils.toBigDecimal(map.get(11)));
      row.set("price", ExcelParseUtils.toBigDecimal(map.get(12)));
      row.set("buy_box_90_days", ExcelParseUtils.toBigDecimal(map.get(13)));
      row.set("buy_box_current", ExcelParseUtils.toBigDecimal(map.get(14)));
      row.set("new_90_days", ExcelParseUtils.toBigDecimal(map.get(15)));

      // amazon_oos, bsr_90_day, listings, quantity -> Integer
      row.set("amazon_oos", ExcelParseUtils.toInteger(map.get(16)));
      row.set("bsr_90_day", ExcelParseUtils.toInteger(map.get(17)));
      row.set("listings", ExcelParseUtils.toInteger(map.get(20)));
      row.set("quantity", ExcelParseUtils.toInteger(map.get(23)));

      // fba_fees, ref_percent, net, margin, total -> BigDecimal
      row.set("fba_fees", ExcelParseUtils.toBigDecimal(map.get(18)));
      row.set("ref_percent", ExcelParseUtils.toBigDecimal(map.get(19))); // 如果要存 8% 就保留 8.00
      row.set("net", ExcelParseUtils.toBigDecimal(map.get(21)));
      row.set("margin", ExcelParseUtils.toBigDecimal(map.get(22)));
      row.set("total", ExcelParseUtils.toBigDecimal(map.get(24)));

      // note 保持 String
      row.set("note", map.get(25) == null ? "" : map.get(25).toString());
      rows.add(row);
    }

    Db.batchSave("products", rows, rows.size());
  }
}

注意事项 ​

使用 doReadAllSync 方法读取后,返回的 List<Map> 中,Map 的键是索引(如 "0", "1", "2")的形式,需要在使用前进行处理和转换。

示例输出:

json
{
  "0": "400637631022534660",
  "1": "1",
  "2": "其他数据"
}

使用自定义解析监听器同步读取 ​

为了更好地处理数据,可以使用自定义的解析监听器同步读取数据,并将其转换为我们需要的格式:

java
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.tio.utils.hutool.ResourceUtil;
import nexus.io.tio.utils.json.JsonUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ReadExcelSyncTest {

  @Test
  public void readExcelSync() {
    // 获取 Excel 文件的 URL
    URL resource = ResourceUtil.getResource("example.xlsx");
    // 读取 Excel 文件的字节内容
    byte[] bytes = FileUtil.readBytes(resource);

    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

    // 读取数据
    List<Map<String, Object>> tableInputList = doReadAllSync(byteArrayInputStream);

    System.out.println("读取的数据条数:" + tableInputList.size());
    System.out.println("数据内容:" + JsonUtils.toJson(tableInputList));
  }

  private List<Map<String, Object>> doReadAllSync(InputStream inputStream) {
    // 存放解析后的所有数据
    List<Map<String, Object>> tableInputList = new ArrayList<>();

    // 定义 Excel 解析监听器
    AnalysisEventListener<Map<Integer, Object>> readListener = new AnalysisEventListener<Map<Integer, Object>>() {
      private Map<Integer, String> headMap = null;

      @Override
      public void doAfterAllAnalysed(AnalysisContext context) {
        // 所有数据解析完成后的处理逻辑
        log.info("所有数据解析完成,总共 {} 行数据", tableInputList.size());
      }

      @Override
      public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        // 处理表头数据
        this.headMap = headMap;
      }

      @Override
      public void invoke(Map<Integer, Object> data, AnalysisContext context) {
        // 处理每一行数据并转换为 Map 对象
        Map<String, Object> map = new HashMap<>();
        for (Map.Entry<Integer, Object> e : data.entrySet()) {
          map.put(headMap.get(e.getKey()), e.getValue());
        }
        tableInputList.add(map);
      }
    };

    // 构建 Excel 读取器
    ExcelReaderBuilder readBuilder = EasyExcel.read(inputStream, readListener);
    // 开始读取
    readBuilder.doReadAll();
    return tableInputList;
  }
}

通过这种方式,我们可以自定义数据的解析和转换过程,更加灵活地处理 Excel 中的数据。

映射为实体类 ​

原理说明 ​

在实际开发中,我们通常希望将 Excel 中的数据直接映射为 Java 实体类,方便后续的业务处理。EasyExcel 提供了这种功能,我们只需要定义与 Excel 表头对应的实体类,并使用注解或命名规则,EasyExcel 就能自动将 Excel 数据映射到实体类对象中。

实体类定义 ​

首先,定义一个与 Excel 表头对应的实体类,例如学生信息:

java
import com.alibaba.excel.annotation.ExcelProperty;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentExcelVo {
  @ExcelProperty("name")
  private String name;
  @ExcelProperty("email")
  private String email;
}
  • @Data:来自 Lombok,用于自动生成 getter、setter、toString 等方法。
  • @NoArgsConstructor:生成无参构造函数。
  • @AllArgsConstructor:生成全参构造函数。
  • @ExcelProperty不可省略

服务层实现 ​

在服务层,我们编写导入方法,将 Excel 数据读取并转换为实体类列表:

java
import java.io.ByteArrayInputStream;
import java.util.List;

import com.alibaba.excel.EasyExcel;
import nexus.io.model.body.RespBodyVo;
import nexus.io.open.chat.model.ExcelImportStudentVo;

public class StudentService {

  public RespBodyVo importByAppId(Long appId, byte[] data) {
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
    List<StudentExcelVo> list = EasyExcel.read(byteArrayInputStream)
        .head(ExcelImportStudentVo.class) // 指定映射的实体类
        .sheet()
        .doReadSync(); // 同步读取

    return RespBodyVo.ok(list);
  }
}

控制层接口 ​

在控制层,我们定义接口接收上传的文件,并调用服务层的方法:

java
import nexus.io.annotation.EnableCORS;
import nexus.io.annotation.RequestPath;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.open.chat.services.student.StudentService;
import nexus.io.model.upload.UploadFile;

@RequestPath("/api/v1/student")
@EnableCORS
public class ApiV1StudentController {
  public RespBodyVo importByAppId(Long appId, UploadFile file) {
    return Aop.get(StudentService.class).importByAppId(appId, file.getData());
  }
}
  • @RequestPath:指定接口路径。
  • @EnableCORS:开启跨域支持。

运行结果 ​

假设我们上传了一个包含学生姓名和邮箱的 Excel 文件,Excel 文件中的数据如下:

nameemail
Tong Lilitong@hawaii.edu
CheChenchenche@hawaii.edu

即 Excel 文件中有两行数据,分别是 Tong Li 和 CheChen 的姓名和邮箱。

接口返回的数据如下:

json
{
  "data": [
    {
      "name": "Tong Li",
      "email": "litong@hawaii.edu"
    },
    {
      "name": "CheChen",
      "email": "chenche@hawaii.edu"
    }
  ],
  "msg": null,
  "code": 1,
  "ok": true
}

原理解析 ​

  • 实体类映射:EasyExcel 通过读取实体类的字段名称,自动与 Excel 表头进行匹配。如果字段名称与表头名称一致,则会自动映射。

  • 同步读取:使用 doReadSync 方法,EasyExcel 会将读取的数据直接返回为实体类的列表,方便我们直接使用。

  • 数据验证和处理:在实际应用中,我们可以在实体类中添加数据校验注解,或者在读取后对数据进行校验和处理,确保数据的正确性。

总结 ​

通过本文的介绍,我们学习了如何使用 EasyExcel 读取 Excel 文件,并将其内容转换为自定义的数据结构或实体类。我们分别演示了异步读取和同步读取的方法,以及如何将 Excel 数据映射为实体类对象。

EasyExcel 的强大之处在于其高性能和易用性,能够简化我们对 Excel 数据的处理。在实际开发中,我们可以根据业务需求,选择合适的读取方式,并对数据进行相应的处理和校验。