导出导入Excel文件(详解-基于EasyExcel)

前言:

近期由于工作的需要,根据需求需要导出导入Excel模板。于是自学了一下下,在此记录并分享!!

EasyExcel:

首先我要在这里非常感谢阿里的大佬们!封装这么好用的Excel相关的API,真的是拯救了许多猿猿们!!

EasyExcel官网:关于Easyexcel | Easy Excel 官网

导出Excel(write):

接到项目之后,我负责的其中一个小需求是需要导出Excel,网上查了大量资料,发现EasyExcel包中已经包含了大量的对Excel的操作,当然对于一些特殊的需求可能需要自己写一个对Excel操作的方法~

@ExcelProperty注解:

我将这个注解作为首要讲解对象,足以看出其重要性。

示例代码:

java 复制代码
    @ExcelProperty(value = "序号",order = 1,converter = IdConverter.class)
    private Long id = 1L;

    @ExcelProperty(value = "出生日期", format = "yyyy-MM-dd",headStyle = CustomHeadStyleStrategy.class)
    private Date birthDate;

只要在对应字段上加上该注解以后,意味着该字段将作为表头出现在Excel中,其中有一些参数如下:

1. value

  • 类型String[]
  • 作用:指定 Excel 表头的名称。可以传入一个字符串数组,当存在多级表头时,数组中的元素按顺序对应各级表头。

2. index

  • 类型int
  • 作用:指定该属性对应 Excel 表格中的列索引,索引从 0 开始。当 Excel 列顺序固定时,可以使用此参数进行精确映射。

3. converter

  • 类型Class<? extends Converter<?>>
  • 作用:指定自定义的转换器,用于将 Java 对象属性与 Excel 单元格数据进行转换。当默认的转换器无法满足需求时,可以自定义转换器并通过此参数指定。

4. format

  • 类型String
  • 作用:用于指定日期、数字等类型数据的格式化模式。当读取或写入 Excel 时,会按照指定的格式进行转换。

5. order

  • 类型int
  • 作用:指定该属性在生成 Excel 表格时的列顺序,数值越小越靠前。

6. headStyle

  • 类型Class<? extends HeadStyleStrategy>
  • 作用:指定表头样式的策略类,用于自定义表头的样式,如字体、颜色、背景色等。

7. contentStyle

  • 类型Class<? extends ContentStyleStrategy>
  • 作用:指定内容样式的策略类,用于自定义表格内容的样式,如字体、颜色、对齐方式等。

当然随着EasyExcel引入的版本不一样,有些参数可能被淘汰或者是换成了新的~

着重需要说明两个参数:

1.converter参数,这个参数需要实现Converter接口

java 复制代码
public class IdConverter implements Converter<Integer> {
//todo
}

这个接口需要重写三个方法:

java 复制代码
Class<?> supportJavaTypeKey();
  • 作用 :指明此转换器所支持的 Java 类型。返回值是一个 Class 对象,代表支持转换的 Java 类型。
java 复制代码
CellDataTypeEnum supportExcelTypeKey();
  • 作用 :指定该转换器支持的 Excel 单元格数据类型。CellDataTypeEnum 是一个枚举类型,包含了各种 Excel 单元格数据类型,像字符串、数字、日期等。
java 复制代码
T convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception;
  • 作用 :把 Excel 单元格数据转换为 Java 对象属性。参数如下:
    • cellData:表示从 Excel 读取到的单元格数据。
    • contentProperty:包含单元格的一些属性信息。
    • globalConfiguration:全局配置信息。

综上示例如下:

java 复制代码
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

public class StringConverter implements Converter<String> {
    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return cellData.getStringValue();
    }

    @Override
    public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return new WriteCellData<>(value);
    }
}

当然也可以根据自身的需求进行补充,例如:希望在数字后面+单位,在人名后面+称呼,希望id是一个自增的等等行为~~

Excel中的id列自增:

这里我举例id列希望是自增的:

实体类代码:

java 复制代码
    @ExcelProperty(value = "序号",order = 1,converter = IdConverter.class)
    private Long id = 1L;

converter代码:

java 复制代码
public class IdConverter implements Converter<Integer> {
    private int currentId = 1;

    @Override
    public Class<?> supportJavaTypeKey() {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.NUMBER;
    }

    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {
        return new WriteCellData<>(String.valueOf(currentId++));
    }
}

效果如下:

复杂表头:

如果想要一个多级表头,使用上述的注解一样可以做到:

只不过value的值要变成层级关系:

java 复制代码
    @ExcelProperty(value = {"项目交付信息","牵头部门名称"},order = 14)
    private String leadingDepartment;

    @ExcelProperty(value = {"项目交付信息","交付部门名称"},order = 15)
    private String deliveryDepartment;

    @ExcelProperty(value = {"项目交付信息","项目PO"},order = 16)
    private String projectPo;

    @ExcelProperty(value = {"项目交付信息","项目经理"},order = 17)
    private String projectManager;

@DateTimeFormat注解:

该注解也是一个确定时间格式的注解,由于版本的迭代,上述的@ExcelProperty的**format**

属性已经被废除,建议使用该注解进行时间格式的限制。

@DateTimeFormat 注解是 Spring 框架提供的一个用于日期和时间格式化的注解,它主要用于将字符串类型的日期时间数据绑定到 Java 对象的日期时间类型字段上,或者将 Java 对象中的日期时间类型字段按照指定格式输出为字符串。

pattern

  • 类型String
  • 作用 :指定日期时间的格式化模式。模式遵循 Java 的 SimpleDateFormatDateTimeFormatter 的规则。例如,"yyyy-MM-dd" 表示年 - 月 - 日的格式,"yyyy-MM-dd HH:mm:ss" 表示年 - 月 - 日 时:分: 秒的格式。

iso

  • 类型ISO 枚举类型
  • 作用 :使用预定义的 ISO 日期时间格式。ISO 枚举包含了几个常用的 ISO 日期时间格式,如 ISO.DATE 表示 yyyy-MM-dd 格式,ISO.TIME 表示 HH:mm:ss.SSSXXX 格式,ISO.DATE_TIME 表示 yyyy-MM-dd'T'HH:mm:ss.SSSXXX 格式。

示例如下:

java 复制代码
 @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthDate;

@ContentRowHeight,@HeadRowHeight,@ColumnWidth

设置行高与列宽,上述的三个注解可以实现设置操作,注意,上述注解@ColumnWidth可以使用在类上面,也可以单独注解在属性上面。

示例如下:

java 复制代码
@ContentRowHeight(20)//内容行高
@HeadRowHeight(20)//表头行高
@ColumnWidth(25)//列宽
public class DeriveExcelDTO {
    @ColumnWidth(40)
    @ExcelProperty(value = "项目名称",order = 2)
    private String projectName;
}

这里就不展示效果图了,大家有兴趣可以自己进行尝试!!!

示例完整导出代码:

java 复制代码
// 实体类,用于映射Excel列
public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;
    
    @ExcelProperty("日期标题")
    private Date date;
    
    @ExcelProperty("数字标题")
    private Double doubleData;

    // 构造函数、getter和setter
    public DemoData() {}
    
    public DemoData(String string, Date date, Double doubleData) {
        this.string = string;
        this.date = date;
        this.doubleData = doubleData;
    }
    
    // getters and setters
    public String getString() { return string; }
    public void setString(String string) { this.string = string; }
    public Date getDate() { return date; }
    public void setDate(Date date) { this.date = date; }
    public Double getDoubleData() { return doubleData; }
    public void setDoubleData(Double doubleData) { this.doubleData = doubleData; }
}

// 控制器示例(Spring MVC)
@RestController
public class ExcelExportController {

    @GetMapping("/export")
    public void export(HttpServletResponse response) throws IOException {
        // 准备数据
        List<DemoData> data = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            DemoData demoData = new DemoData();
            demoData.setString("字符串" + i);
            demoData.setDate(new Date());
            demoData.setDoubleData(0.56 + i);
            data.add(demoData);
        }

        // 设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

        // 导出Excel
        EasyExcel.write(response.getOutputStream(), DemoData.class)
                .sheet("模板")
                .doWrite(data);
    }
}

导入Excel(read):

导出Excel其实和导入是差不多的,但是在使用EasyExcel时,需要注意表头的书写是否和读取时一致(例如读取时读二级表头,Excel中对应的数据的表头必须也是二级的)!后面会详细介绍到。

设置监听器:

读取Excel表格时,必须设置一个监听器,但是需要注意的是该监听器不能被Spring管理,每次使用时必须手动new。

java 复制代码
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {

    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
    /**
     * 缓存的数据
     */
    private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    /**
     * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private DemoDAO demoDAO;

    public DemoDataListener() {
        // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
        demoDAO = new DemoDAO();
    }

    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param demoDAO
     */
    public DemoDataListener(DemoDAO demoDAO) {
        this.demoDAO = demoDAO;
    }

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        log.info("所有数据解析完成!");
    }

    /**
     * 加上存储数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", cachedDataList.size());
        demoDAO.save(cachedDataList);
        log.info("存储数据库成功!");
    }
}

当然阿里为了更加方便猿猿们,也就行了二次封装,封装后的ReadListener可以交给Spring进行管理,也是非常方便了~

没错就是这个AnalysisEventListener类,里面继承了ReadListener,也是更加简便清晰了~

可以重写一下的方法:

每个方法的作用以及参数如下:

onException(Exception exception, AnalysisContext context)

  • 作用:读取过程中发生异常时触发

  • 参数

    • exception:异常对象
    • context:读取上下文
      doAfterAllAnalysed(AnalysisContext context)
  • 作用:整个 Excel 文件读取完成后触发

  • 参数:读取上下文
    invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context)

  • 作用:解析表头时触发(默认表头占 1 行)

  • 参数

    • headMap:表头数据(key 为列索引,value 为单元格数据)
    • context:读取上下文
      hasNext(AnalysisContext context)
  • 作用:控制是否继续读取下一行(返回 false 时终止读取)

  • 参数:读取上下文

完整示例如下:

java 复制代码
public class DemoDataListener extends AnalysisEventListener<DemoData> {
    private List<DemoData> dataList = new ArrayList<>();
    
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        // 每行数据解析后调用
        dataList.add(data);
        System.out.println("解析第" + context.readRowHolder().getRowIndex() + "行:" + data.getName());
    }
    
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 所有数据解析完成后调用
        System.out.println("共解析" + dataList.size() + "行数据");
    }
    
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        // 表头解析后调用
        System.out.println("解析表头:" + headMap.get(0).getStringValue());
    }
    
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        // 异常处理
        System.err.println("解析失败,行号:" + context.readRowHolder().getRowIndex());
        exception.printStackTrace();
    }
    
    public List<DemoData> getDataList() {
        return dataList;
    }
}

完整代码:

java 复制代码
public String importUser(@RequestParam("file") MultipartFile file) {
        try {
            EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener(userService)).sheet().doRead();
            return "导入并更新成功";
        } catch (Exception e) {
            return "导入并更新失败:" + e.getMessage();
        }
    }
相关推荐
czhaii7 分钟前
PLC脉冲位置 单片机跟踪读取记录显示
开发语言·c#
骑牛小道士7 分钟前
Java基础 集合框架 Collection接口和抽象类AbstractCollection
java
alden_ygq33 分钟前
当java进程内存使用超过jvm设置大小会发生什么?
java·开发语言·jvm
码农黛兮_4643 分钟前
SQL 索引优化指南:原理、知识点与实践案例
数据库·sql
爆肝疯学大模型1 小时前
SQL server数据库实现远程跨服务器定时同步传输数据
运维·服务器·数据库
triticale1 小时前
【Java】网络编程(Socket)
java·网络·socket
淘源码d1 小时前
什么是ERP?ERP有哪些功能?小微企业ERP系统源码,SpringBoot+Vue+ElementUI+UniAPP
java·源码·erp·erp源码·企业资源计划·企业erp·工厂erp
源码方舟1 小时前
【基于ALS模型的教育视频推荐系统(Java实现)】
java·python·算法·音视频
蜗牛沐雨1 小时前
Rust 中的 `PartialEq` 和 `Eq`:深入解析与应用
开发语言·后端·rust
Python私教1 小时前
Rust快速入门:从零到实战指南
开发语言·后端·rust