首页 > 编程语言 >【Java】Java读写Excel文件

【Java】Java读写Excel文件

时间:2024-11-12 16:47:16浏览次数:1  
标签:Java 读写 Excel private class new sheet data public

image

常见的JavaExcel操作方法

Java中常用的Excel操作方式有什么呢?

  • Apache POI
  • EasyExcel

这两种方法有什么区别和联系?

Apache POI 是一个用于操作 Microsoft Office 格式文件(如 Excel、Word 等)的 Java 库。它提供了一系列的 API 来读写 Excel 文件。 POI的基本功能如下:

结构 说明
HSSF 提供读写Microsoft Excel 格式档案功能 (03版的Excel)
XSSF 提供读写Microsoft Excel OOXML 格式档案功能 (07及其以上的Excel)
HWPF 提供读写Microsft Word格式档案功能
HSLF 提供读写Microsoft PowerPoint格式档案的功能

EasyExcel 是阿里巴巴开源的一个 Java 处理 Excel 的工具库。它在功能上很强大,并且具有简单易用、性能高的特点。它可以用于读取和写入 Excel 文件,支持大数据量的 Excel 操作,能够有效地避免内存溢出等问题。 官网地址:https://easyexcel.opensource.alibaba.com/

Apache POI(以下简称POI)操作数据库更灵活,而EasyExcel能够简单的操作大数据量。

接下来,我们将分别学习POI和EasyExcel。

Apache POI 操作Excel

POI操作Excel分为两个对象HSSF和XSSF ,分别操作的是2003版和2007版的Excel。那么,03版和07版的表格有什么区别呢?

2003版和2007版的表格区别

  1. 后缀名不同。Excel 2003以.xls后缀名,Excel 2007以.xlsx为后缀名。
  2. 文件格式不同。Excel 2003二进制文件格式,数据存储结构箱单简单,对系统来说,解析这种格式可能会比较困难。Excel 2007,采用了基于XML的OpenXML格式。xlxs可能会因为XML高效压缩和重复利用而占用更少的空间。
  3. 数据存储容量不同。Excel2003工作表大小和存储数量有限(最多65536行和256列),Excel2007最多可以存储1048576行和16384列。
  4. 功能和用户体验不同。Excel2003界面相对简介,功能划分比较简单;Excel 2007采用了全新的Ribbon界面设计。
  5. 公式和函数。 Excel 2003包含了大量的基本公式和函数;Excel 2007 进一步扩展了公式和函数的功能。
  6. 图标和图形。Excel2003 提供了基本的图表类型和一些简单的图表设置;Excel2007 增强了图表和图形的功能。
  7. 兼容性不同。Excel 2003 与旧版本的操作系统兼容较好;Excel 2007 与就版本(如Excel 2003)坑你会出现一些问题。
  8. 安全性。 Excel 2003具有一定的安全机制,Excel2007加强了安全性能。

POI 操作Excel 2003 及Excel 2007 步骤

  1. 导入项目依赖
  2. 代码中操作Excel

导入项目依赖

<!--xls 2003-->
<dependency>
	<groupId>org.apache.poi</groupId>
	<artifactId>poi</artifactId>
	<version>3.17</version>
</dependency>
<!-- xls 2007-->
<dependency>
	<groupId>org.apache.poi</groupId>
	<artifactId>poi-ooxml</artifactId>
	<version>3.17</version>
</dependency>

代码中操作Excel

操作之前,我们可以理解Excel基础的概念:工作簿、工作表、行、列、单元格。

写操作

Excel 2003的写操作如下:

@Test
public void testWrite03() throws IOException {
	// 1. 创建一个工作簿
	Workbook wb = new HSSFWorkbook();
	// 2. 创建一个工作表
	Sheet sheet = wb.createSheet("每日出行里程统计表");
	// 3. 创建行/创建单元格
	Row row0 = sheet.createRow(0);
	Cell cell00 = row0.createCell(0);
	cell00.setCellValue("日期");
	Cell cell01 = row0.createCell(1);
	cell01.setCellValue("里程");
	Row row1 = sheet.createRow(1);
	Cell cell10 = row1.createCell(0);
	cell10.setCellValue("11月6日");
	Cell cell11 = row1.createCell(1);
	cell11.setCellValue(44.5);
	// 4. 生成文件流
	FileOutputStream fos = new FileOutputStream("统计表.xls");
	wb.write(fos);
	// 5. 关闭流
	fos.close();
	System.out.println("Excel写入完成");
}

可以看到,已经成功写入Excel 2003文件。
image
Excel 2007 写操作与Excel 2003类似,只要将工作簿HSSFWorkbook改为XSSFWorkbook即可。

@Test
public void testWrite07() throws IOException {
	// 1. 创建一个工作簿
	Workbook wb = new XSSFWorkbook();
	// 2. 创建一个工作表
	Sheet sheet = wb.createSheet("每日出行里程统计表");
	// 3. 创建行/创建单元格
	Row row0 = sheet.createRow(0);
	Cell cell00 = row0.createCell(0);
	cell00.setCellValue("日期");
	Cell cell01 = row0.createCell(1);
	cell01.setCellValue("里程");
	Row row1 = sheet.createRow(1);
	Cell cell10 = row1.createCell(0);
	cell10.setCellValue("11月6日");
	Cell cell11 = row1.createCell(1);
	cell11.setCellValue(44.5);
	// 4. 生成文件流
	FileOutputStream fos = new FileOutputStream("统计表.xlsx");
	wb.write(fos);
	// 5. 关闭流
	fos.close();
	System.out.println("Excel写入完成");
}

如果进行大数据量写入:

 for (int rowNum = 0; rowNum < 65536; rowNum++) {
	Row row = sheet.createRow(rowNum);
	for (int colNum = 0; colNum < 15; colNum++) {
		Cell cell = row.createCell(colNum);
		cell.setCellValue(colNum);
	}
}

Excel 2003和Excel 2007 哪个速度快?以上的例子,测试Excel 2003使用时间1481ms,Excel 2007使用时间16671ms。所以Excel 2003版写入速度原快与Excel 2007版。

Excel 2007可以使用临时文件的方式SXSSFWorkbook来提升大数据文件的写入,(注意清除临时文件)。

@Test
public void testWrite07BigDatas() throws IOException {
	long startTime = System.currentTimeMillis();
	// 1. 创建一个工作簿
	Workbook wb = new SXSSFWorkbook();
	// 2. 创建一个工作表
	Sheet sheet = wb.createSheet("Sheet1");
	// 3. 创建行/创建单元格
   for (int rowNum = 0; rowNum < 65536; rowNum++) {
	   Row row = sheet.createRow(rowNum);
	   for (int colNum = 0; colNum < 15; colNum++) {
		   Cell cell = row.createCell(colNum);
		   cell.setCellValue(colNum);
	   }
   }
	// 4. 生成文件流
	FileOutputStream fos = new FileOutputStream("大数据量1.xlsx");
	wb.write(fos);
	// 5. 关闭流
	fos.close();
	// 清除临时文件
	((SXSSFWorkbook)wb).dispose();
	long endTime = System.currentTimeMillis();
	System.out.println("使用时间:"+ (endTime - startTime) + "ms");
}

此时运行时间,大大减少:使用时间:2169ms

当使用XSSFWorkbook写入大量数据时,由于它会将所有数据都保存在内存中,这可能导致内存占用过高。而SXSSFWorkbook采用了一种流式写入的方式,它只在内存中保留一定数量的行数据(默认是 100 行),当数据量超过这个限制时,会将之前的数据刷新到磁盘上的临时文件中,从而有效地减少内存占用。

读操作

Excel 2003和Excel2007 的操作类似,只是工作簿不同。接下来已2003为例。读取单元格数据:

@Test
public void testRead03() throws IOException {
	// 获取文件流
	FileInputStream inputStream = new FileInputStream("统计表.xls");
	// 1. 创建工作簿
	Workbook workbook = new HSSFWorkbook(inputStream);
	// 2. 得到表
	Sheet sheet = workbook.getSheetAt(0);
	//3. 得到行、列
	Row row0 = sheet.getRow(0);
	Cell cell00 = row0.getCell(0);
	System.out.println("值:" +cell00.getStringCellValue());
	// 关闭流
	inputStream.close();
}

这里,我们认为单元格的值是字符串。如果,需要根据单元格不同的类型,做不同操作,应该怎么做?

@Test
public void testRead03() throws IOException {
	// 获取文件流
	FileInputStream inputStream = new FileInputStream("统计表.xls");
	// 1. 创建工作簿
	Workbook workbook = new HSSFWorkbook(inputStream);
	// 2. 得到表
	Sheet sheet = workbook.getSheetAt(0);
	//3. 获取表中内容
	for (int rowNum = 0; rowNum < sheet.getPhysicalNumberOfRows(); rowNum++) {
		Row row = sheet.getRow(rowNum);
		for (int colNum = 0; colNum < sheet.getRow(rowNum).getPhysicalNumberOfCells(); colNum++) {
			Cell cell = row.getCell(colNum);
			if(cell != null) {
				switch (cell.getCellType()) {
					case Cell.CELL_TYPE_STRING: // 字符串
						System.out.println("[" + rowNum + "][" + colNum + "][String]" + cell.getStringCellValue());
						break;
						case Cell.CELL_TYPE_NUMERIC:    // 数字(日期、普通数字)
							System.out.println("[" + rowNum + "][" + colNum + "][Numeric]" + cell.getNumericCellValue());
							if(HSSFDateUtil.isCellDateFormatted(cell)){
								System.out.println("[" + rowNum + "][" + colNum + "][Date]" + cell.getDateCellValue());
							}
							break;
						case Cell.CELL_TYPE_BLANK:  // 空
							System.out.println("[" + rowNum + "][" + colNum + "][Blank]" + cell.getStringCellValue());
							break;
						case Cell.CELL_TYPE_BOOLEAN:    // 布尔
							System.out.println("[" + rowNum + "][" + colNum + "][Boolean]" + cell.getBooleanCellValue());
							break;
						case Cell.CELL_TYPE_FORMULA:    // 公式
							System.out.println("[" + rowNum + "][" + colNum + "][Formula][" + cell.getCellFormula() + "]" ); // 公式的结果根据
							break;
						case Cell.CELL_TYPE_ERROR:  // 出错
							System.out.println("[" + rowNum + "][" + colNum + "][Error]" + cell.getErrorCellValue());
							break;
						default:
							System.out.println("[" + rowNum + "][" + colNum + "][Unknown]" + cell.getCellType());
							break;
				}
			}
		}
	}
	// 关闭流
	inputStream.close();
}

Apache POI读写入门,暂时就到这里了。

EasyExcel操作Excel

EasyExcel 官网地址:https://easyexcel.opensource.alibaba.com/ 这里有详细的说明。最近,看到官网:EasyExcel将逐步进入维护模式,并给予用户充足的时间评估并迁移到其他产品。

不过,我暂时也不知道额外学习什么Excel库?姑且学习学习,储备一下知识。

这里先贴出Demo地址:

读Excel:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/read/ReadTest.java

写Excel:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/write/WriteTest.java

Web上传、下载:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/web/WebTest.java

引入依赖

注意,移除之前测试的Excel依赖,避免冲突。

 <!-- easyExcel依赖-->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>easyexcel</artifactId>
	<version>3.3.4</version>
</dependency>

读操作

我们先声明String fileName = "E:\\MyRepository\\demo\\demo.xlsx";读取路径。

Excel的内容如下:
image
读写之前,需要定义读对象:

@Data
public class DemoData {
    private String string;
    private Date date;
    private Double doubleData;
}

最简单四种读方法

最简单的读,方法一

/**
 * 最简单的读1,不用额外写监听
 * @author lyj
 * @date 2024-11-08
 */
@Test
public void testSimpRead1(){
	EasyExcel.read(fileName, DemoData.class,new PageReadListener<DemoData>(dataList->{
		for(DemoData demoData:dataList){
			System.out.println("数据{" + JSON.toJSONString(demoData)+"}");
		}
	})).sheet().doRead();
}

运行后,我们可以看到读取的数据:
image

方法二,使用匿名内部类,自定义监听。监听中包含:每条数据解析后调用和所有数据解析完成后调用。我们可以在这里自定义方法,比如解析完成后,写入数据库中。

 /**
     *  最近单的读:匿名内部类,不用额外写DemoDataListerner
     * @author lyj
     * @date 2024-11-08
     */
    @Test
    public void testSimpRead2(){
        // 这里需要指定用哪个class读,然后读取第一个sheet,文件流会自动关闭
        EasyExcel.read(fileName, DemoData.class,new ReadListener<DemoData>(){
            /**
             * 单词缓存的数据量
             */
            public  static  final  int BATCH_COUNT = 100;
            /**
             * 临时存储
             */
            private List<DemoData> cacheDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

            /**
             * 每一条数据解析调用
             * @param data    one row value. It is same as {@link AnalysisContext#readRowHolder()}
             * @param context analysis context
             */
            @Override
            public void invoke(DemoData data, AnalysisContext context) {
                cacheDataList.add(data);
                if(cacheDataList.size() >= BATCH_COUNT){
                    saveData();
                    // 存储完成后清理list
                    cacheDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
                }
            }
            /**
             * 所有数据解析完成后调用
             * @param context
             */
            @Override
            public void doAfterAllAnalysed(AnalysisContext context) {
                saveData();
            }
            /**
             * 存储数据库
             */
            private void saveData() {
                log.info("{}条数据,开始存储数据库!", cacheDataList.size());
                log.info("存储数据库成功!");
            }
        }).sheet().doRead();
    }

接下来的方法,需要使用监听。监听方法与匿名内部类的方法类似。

@Slf4j
public class DemoDataListerner implements ReadListener<DemoData> {


    /**
     * 每各5条存储数据库,实际使用中可以100条,然后清理,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
    /**
     * 缓存的数据
     */
    private List<DemoData> cacheDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

    /**
     * dao层,如果不存储,这个对象没有用
     */
    private DemoDataDao demoDataDao;

    public DemoDataListerner(){
        demoDataDao = new DemoDataDao();
    }
    /**
     * 构造函数
     * @param demoDataDao
     */
    public DemoDataListerner(DemoDataDao demoDataDao) {
        this.demoDataDao = demoDataDao;
    }

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

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

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

}

如果多个类型使用监听,则可以传递泛型。

方法三:直接引用监听

/**
     * 简单的读,添加监听 (DemoDataListerner不能不spring管理,每次读取Excel都要new)
     * @author lyj
     * @date 2024-11-08
     */
    @Test
    public void testSimpRead3(){
        EasyExcel.read(fileName, DemoData.class,new DemoDataListerner()).sheet().doRead();
    }

方法四:一个文件,一个reader

 /**
     * 一个文件,一个reader
     * @author lyj
     * @date 2024-11-08
     */
    @Test
    public void testSimpRead4(){
        try( ExcelReader excelReader = EasyExcel.read(fileName, DemoData.class,new DemoDataListerner()).build()){
            // 构建一个sheet, 这里可以指定名字或no
            ReadSheet readSheet = EasyExcel.readSheet(0).build();
            // 读取一个sheet
            excelReader.read(readSheet);
        }
    }

指定下标或列名

看官方的demo

@Getter
@Setter
@EqualsAndHashCode
public class IndexOrNameData {
    /**
     * 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
     */
    @ExcelProperty(index = 2)
    private Double doubleData;
    /**
     * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
     */
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
}

监听器与最简单的监听器类似,只是使用了泛型。
image

读取字符串的方式也是类似的,EasyExcel.read(fileName,DemoData.class,new IndexOrNameDataListerner<DemoData>()).sheet().doRead();

读取多个sheet(工作表)

前面的方法读取一个sheet,如果需要读取多个sheeet(工作表)呢?

读取全部的sheet方法如下:

EasyExcel.read(fileName, DemoData.class , new IndexOrNameDataListerner<DemoData>()).doReadAll();

如果只读取部分sheet。可以使用之前学到的简单读方法。还可以为两个sheet传递不同的对象和监听器。

/**
 *  读取N个sheet,部分
 * @author lyj
 * @date 2024-11-08
 */
@Test
public void testReallN(){
	try(ExcelReader excelReader = EasyExcel.read(fileName).build()){
		ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new IndexOrNameDataListerner<DemoData>()).build();
		ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData.class).registerReadListener(new IndexOrNameDataListerner<DemoData>()).build();
		// 这里一定要把sheet1和sheet2一起传入。否则03版的Excel会读取多次,浪费性能
		excelReader.read(readSheet1, readSheet2);
	}
}

日期或数字的自定义读

对象设置如下:

@Data
public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;

    @ExcelProperty("日期标题")
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    private Date date;

    @ExcelProperty("数字标题")
    @NumberFormat("#.##")
    private Double doubleData;
}

可以设置日期和标题样式使用注解@DateTimeFormat@NumberFormat。监听器的试着类似。可以设置自定义转换器。

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

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

    /**
     * 这里读的时候会调用
     *
     * @param context
     * @return
     */
    @Override
    public String convertToJavaData(ReadConverterContext<?> context) {
        return "自定义:" + context.getReadCellData().getStringValue();
    }

    /**
     * 这里是写的时候会调用 不用管
     *
     * @return
     */
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
        return new WriteCellData<>(context.getValue());
    }

}

使用如下:

/**
 * 日期、数字或者自定义格式转换
 * <p>
 * 默认读的转换器{@link DefaultConverterLoader#loadDefaultReadConverter()}
 * <p>1. 创建excel对应的实体对象 参照{@link ConverterData}.里面可以使用注解{@link DateTimeFormat}、{@link NumberFormat}或者自定义注解
 * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link ConverterDataListener}
 * <p>3. 直接读即可
 */
@Test
public void converterRead() {
	String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
	// 这里 需要指定读用哪个class去读,然后读取第一个sheet 
	EasyExcel.read(fileName, ConverterData.class, new ConverterDataListener())
		// 这里注意 我们也可以registerConverter来指定自定义转换器, 但是这个转换变成全局了, 所有java为string,excel为string的都会用这个转换器。
		// 如果就想单个字段使用请使用@ExcelProperty 指定converter
		// .registerConverter(new CustomStringStringConverter())
		// 读取sheet
		.sheet().doRead();
}

多行头

对象监听器与前文类似。假设数据如下:
image

标题行为第一行:

/**
 *  多行头
 * @author lyj
 * @date 2024-11-12
 */
@Test
public void testComplexHead(){
	EasyExcel.read(fileName, DemoData.class, new IndexOrNameDataListerner<DemoData>())
			.sheet()
			.headRowNumber(1)       // 解析的行头,不传入默认第一行
			.doRead();
}

同步返回(不推荐)

/**
 * 同步的返回,不推荐使用,如果数据量大会把数据放到内存里面
 */
@Test
public void synchronousRead() {
	String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
	// 这里 需要指定读用哪个class去读,然后读取第一个sheet 同步读取会自动finish
	List<DemoData> list = EasyExcel.read(fileName).head(DemoData.class).sheet().doReadSync();
	for (DemoData data : list) {
		LOGGER.info("读取到数据:{}", JSON.toJSONString(data));
	}
	// 这里 也可以不指定class,返回一个list,然后读取第一个sheet 同步读取会自动finish
	List<Map<Integer, String>> listMap = EasyExcel.read(fileName).sheet().doReadSync();
	for (Map<Integer, String> data : listMap) {
		// 返回每条数据的键值对 表示所在的列 和所在列的值
		LOGGER.info("读取到数据:{}", JSON.toJSONString(data));
	}
}

同步读:doReadSync

读取表头

最简单的方式,只需要重写监听方法的invokeHeadMap方法即可。

/**
 * 返回标题头
 * @param headMap
 * @param context
 */
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
	log.info("解析到头数据:{}", JSON.toJSONString(headMap));
	ReadListener.super.invokeHead(headMap, context);
}

额外信息(批注、超链接、合并单元格信息读取)

这些功能我暂时用不到,可以类似字典一样。把测试复制出来,需要的时候再查看。

since :2.0.0-beta1

Excel 示例

img

对象

@Getter
@Setter
@EqualsAndHashCode
public class DemoExtraData {

    private String row1;

    private String row2;
}

监听器

在最简单的读监听器多了一个extra方法

@Slf4j
public class DemoExtraListener implements ReadListener<DemoExtraData> {

    @Override
    public void invoke(DemoExtraData data, AnalysisContext context) {}

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {}

    @Override
    public void extra(CellExtra extra, AnalysisContext context) {
        log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
        switch (extra.getType()) {
            case COMMENT:
                log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),
                    extra.getText());
                break;
            case HYPERLINK:
                if ("Sheet1!A1".equals(extra.getText())) {
                    log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
                        extra.getColumnIndex(), extra.getText());
                } else if ("Sheet2!A1".equals(extra.getText())) {
                    log.info(
                        "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"
                            + "内容是:{}",
                        extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                        extra.getLastColumnIndex(), extra.getText());
                } else {
                    Assert.fail("Unknown hyperlink!");
                }
                break;
            case MERGE:
                log.info(
                    "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
                    extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                    extra.getLastColumnIndex());
                break;
            default:
        }
    }
}

代码

    /**
     * 额外信息(批注、超链接、合并单元格信息读取)
     * <p>
     * 由于是流式读取,没法在读取到单元格数据的时候直接读取到额外信息,所以只能最后通知哪些单元格有哪些额外信息
     *
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoExtraData}
     * <p>
     * 2. 由于默认异步读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoExtraListener}
     * <p>
     * 3. 直接读即可
     *
     * @since 2.2.0-beat1
     */
    @Test
    public void extraRead() {
        String fileName = TestFileUtil.getPath() + "demo" + File.separator + "extra.xlsx";
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet
        EasyExcel.read(fileName, DemoExtraData.class, new DemoExtraListener())
            // 需要读取批注 默认不读取
            .extraRead(CellExtraTypeEnum.COMMENT)
            // 需要读取超链接 默认不读取
            .extraRead(CellExtraTypeEnum.HYPERLINK)
            // 需要读取合并单元格信息 默认不读取
            .extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
    }

读取公式和单元格

对象

@Getter
@Setter
@EqualsAndHashCode
public class CellDataReadDemoData {
    private CellData<String> string;
    // 这里注意 虽然是日期 但是 类型 存储的是number 因为excel 存储的就是number
    private CellData<Date> date;
    private CellData<Double> doubleData;
    // 这里并不一定能完美的获取 有些公式是依赖性的 可能会读不到 这个问题后续会修复
    private CellData<String> formulaValue;
}

监听器与最简单监听器类似,代码如下:

   /**
 * 读取公式和单元格类型
 *
 * <p>
 * 1. 创建excel对应的实体对象 参照{@link CellDataReadDemoData}
 * <p>
 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoHeadDataListener}
 * <p>
 * 3. 直接读即可
 *
 * @since 2.2.0-beat1
 */
@Test
public void cellDataRead() {
	String fileName = TestFileUtil.getPath() + "demo" + File.separator + "cellDataDemo.xlsx";
	// 这里 需要指定读用哪个class去读,然后读取第一个sheet
	EasyExcel.read(fileName, CellDataReadDemoData.class, new CellDataDemoHeadDataListener()).sheet().doRead();
}

数据转换异常处理

对象

@Getter
@Setter
@EqualsAndHashCode
public class ExceptionDemoData {
    /**
     * 用日期去接字符串 肯定报错
     */
    private Date date;
}

监听器

只需要监听器重写onException方法即可

/**
 * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
 *
 * @param exception
 * @param context
 * @throws Exception
 */
@Override
public void onException(Exception exception, AnalysisContext context) {
	log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
	// 如果是某一个单元格的转换异常 能获取到具体行号
	// 如果要获取头的信息 配合invokeHeadMap使用
	if (exception instanceof ExcelDataConvertException) {
		ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
		log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
			excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
	}
}

不创建对象的读

监听器

补充对象的读,实现的接口为AnalysisEventListener

@Slf4j
public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 5;
    private List<Map<Integer, String>> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

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

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

代码

/**
 * 不创建对象的读
 */
@Test
public void noModelRead() {
	String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
	// 这里 只要,然后读取第一个sheet 同步读取会自动finish
	EasyExcel.read(fileName, new NoModelDataListener()).sheet().doRead();
}

Web中读

Demo示例代码:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/web/WebTest.java

代码

/**
 * 文件上传
 * <p>
 * 1. 创建excel对应的实体对象 参照{@link UploadData}
 * <p>
 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}
 * <p>
 * 3. 直接读即可
 */
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
	EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();
	return "success";
}

EasyExcel写

需要写的通用数据,后面省略

private List<DemoData> data() {
	List<DemoData> list = ListUtils.newArrayList();
	for (int i = 0; i < 10; i++) {
		DemoData data = new DemoData();
		data.setString("字符串" + i);
		data.setDate(new Date());
		data.setDoubleData(0.56);
		list.add(data);
	}
	return list;
}

最简单的写

最简单的写适用于数据量不大的情况(5000以内,看具体情况)。数据量大的情况考虑重复多次写。

最简单的写对象,与读对象类似

@Data
public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;

    @ExcelProperty("日期标题")
    private Date date;

    @ExcelProperty("数字标题")
    private Double doubleData;
}

最简单的写方法有3种。写法一和写法而类似,指定class,直接写。

/**
 * 写法一:指定class,直接写
 * @author lyj
 * @date 2024-11-12
 */
@Test
public void testWrite1(){
	// JDK8+ . since:3.0.0-beta1
	// 需要指定用哪个class去写。默认写到第一个sheet。文件流会自动关闭。如果想使用03,则传入excelType参数即可
	EasyExcel.write(wfileName,DemoData.class)
			.sheet("模板")
			.doWrite(()->{
				return data();
			});
}
/**
 * 写法二:指定class,直接写
 * @author lyj
 * @date 2024-11-12
 */
public void testWrite2(){
	// JDK8+ . since:3.0.0-beta1
	// 需要指定用哪个class去写。默认写到第一个sheet。文件流会自动关闭。如果想使用03,则传入excelType参数即可
	EasyExcel.write(wfileName,DemoData.class)
			.sheet("模板")
			.doWrite(data());
}

写法三如下:

/**
 * 写法3
 * @author lyj
 * @date 2024-11-12
 */
@Test
public void testWrite3(){
	try(ExcelWriter excelWriter = EasyExcel.write(fileName,DemoData.class).build()) {
		WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
		excelWriter.write(data(),writeSheet);
	}
}

根据参数只导出指定列

导出指定列方法

/**
 *  导出指定列
 * @author lyj
 * @date 2024-11-12
 */
@Test
public void testExculdeOrIncludeWrite(){
	// 假设我们忽略date
	Set<String> excludeColumnFiledNames = new HashSet<>();
	excludeColumnFiledNames.add("date");
	EasyExcel.write(wfileName,DemoData.class)
			.excludeColumnFieldNames(excludeColumnFiledNames)
			.sheet("模板").doWrite(data());

	// 假设我们假设只导出date
	Set<String> includeColumnFiledNames = new HashSet<>();
	includeColumnFiledNames.add("date");
	EasyExcel.write(wfileName2,DemoData.class)
			.includeColumnFieldNames(includeColumnFiledNames)
			.sheet("模板").doWrite(data());
}

指定写入的列索引

可以在对象中添加注解,指定导出列索引

@Data
public class DemoData {
    @ExcelProperty(value = "字符串标题", index = 0)
    private String string;

    @ExcelProperty(value = "日期标题", index = 1)
    private Date date;

    // 这里设置3,对导致第二列空
    @ExcelProperty(value = "数字标题", index = 3)
    private Double doubleData;
}

复杂头写入

复杂头写入,可以设置对象属性。

img

@Data
public class DemoData {
    @ExcelProperty(value = {"主标题","字符串主题"})
    private String string;

    @ExcelProperty(value = {"主标题","日期标题"})
    private Date date;

    @ExcelProperty(value = {"主标题","数字标题"})
    private Double doubleData;
}

重复多次写入

写入到同一个sheeet中

/**
 *  如果写到同一个sheet
 * @author lyj
 * @date 2024-11-12
 */
@Test
public void testRepeatWrite1(){
	try(ExcelWriter excelWriter = EasyExcel.write(wfileName,DemoData.class).build()) {
		WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
		for (int i = 0; i < 10; i++) {
			// 分页去数据库查询,这里可以去数据库查询每一页数据
			List<DemoData> data = data();
			excelWriter.write(data,writeSheet);
		}
	}
}

写到不同的sheet,同一个对象

/**
 *  不同的sheeet,同一个对象
 * @author lyj
 * @date 2024-11-12
 */
@Test
public void testRepeatWrite2(){
	try(ExcelWriter excelWriter = EasyExcel.write(wfileName,DemoData.class).build()) {
		for (int i = 0; i < 10; i++) {
			WriteSheet writeSheet = EasyExcel.writerSheet("模板" + (i + 1)).build();
			// 分页去数据库查询,这里可以去数据库查询每一页数据
			List<DemoData> data = data();
			excelWriter.write(data,writeSheet);
		}
	}
}

不同sheet,不同对象。在writeSheet方法中,设置对象。

/**
     *  不同的sheeet,不同的对象
     * @author lyj
     * @date 2024-11-12
     */
    @Test
    public void testRepeatWrite2(){
        try(ExcelWriter excelWriter = EasyExcel.write(wfileName,DemoData.class).build()) {
            for (int i = 0; i < 10; i++) {
                WriteSheet writeSheet = EasyExcel.writerSheet("模板" + (i + 1)).head(DemoData.class).build();
                // 分页去数据库查询,这里可以去数据库查询每一页数据
                List<DemoData> data = data();
                excelWriter.write(data,writeSheet);
            }
        }
    }

日期或数字自定义格式转换

日期或数字的格式化转换,只需要修改对象的注解属性即可。

@Data
public class DemoData {
    @ExcelProperty(value = {"主标题","字符串主题"})
    private String string;

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = {"主标题","日期标题"})
    private Date date;

    @NumberFormat("#.##")
    @ExcelProperty(value = {"主标题","数字标题"})
    private Double doubleData;
}

图片导出

对象

@Getter
@Setter
@EqualsAndHashCode
@ContentRowHeight(100)
@ColumnWidth(100 / 8)
public class ImageDemoData {
    private File file;
    private InputStream inputStream;
    /**
     * 如果string类型 必须指定转换器,string默认转换成string
     */
    @ExcelProperty(converter = StringImageConverter.class)
    private String string;
    private byte[] byteArray;
    /**
     * 根据url导出
     *
     * @since 2.1.1
     */
    private URL url;

    /**
     * 根据文件导出 并设置导出的位置。
     *
     * @since 3.0.0-beta1
     */
    private WriteCellData<Void> writeCellDataFile;
}

代码

   /**
     * 图片导出
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link ImageDemoData}
     * <p>
     * 2. 直接写即可
     */
    @Test
    public void imageWrite() throws Exception {
        String fileName = TestFileUtil.getPath() + "imageWrite" + System.currentTimeMillis() + ".xlsx";

        // 这里注意下 所有的图片都会放到内存 暂时没有很好的解法,大量图片的情况下建议 2选1:
        // 1. 将图片上传到oss 或者其他存储网站: https://www.aliyun.com/product/oss ,然后直接放链接
        // 2. 使用: https://github.com/coobird/thumbnailator 或者其他工具压缩图片
        
        String imagePath = TestFileUtil.getPath() + "converter" + File.separator + "img.jpg";
        try (InputStream inputStream = FileUtils.openInputStream(new File(imagePath))) {
            List<ImageDemoData> list =  ListUtils.newArrayList();
            ImageDemoData imageDemoData = new ImageDemoData();
            list.add(imageDemoData);
            // 放入五种类型的图片 实际使用只要选一种即可
            imageDemoData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
            imageDemoData.setFile(new File(imagePath));
            imageDemoData.setString(imagePath);
            imageDemoData.setInputStream(inputStream);
            imageDemoData.setUrl(new URL(
                "https://raw.githubusercontent.com/alibaba/easyexcel/master/src/test/resources/converter/img.jpg"));

            // 这里演示
            // 需要额外放入文字
            // 而且需要放入2个图片
            // 第一个图片靠左
            // 第二个靠右 而且要额外的占用他后面的单元格
            WriteCellData<Void> writeCellData = new WriteCellData<>();
            imageDemoData.setWriteCellDataFile(writeCellData);
            // 这里可以设置为 EMPTY 则代表不需要其他数据了
            writeCellData.setType(CellDataTypeEnum.STRING);
            writeCellData.setStringValue("额外的放一些文字");

            // 可以放入多个图片
            List<ImageData> imageDataList = new ArrayList<>();
            ImageData imageData = new ImageData();
            imageDataList.add(imageData);
            writeCellData.setImageDataList(imageDataList);
            // 放入2进制图片
            imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
            // 图片类型
            imageData.setImageType(ImageType.PICTURE_TYPE_PNG);
            // 上 右 下 左 需要留空
            // 这个类似于 css 的 margin
            // 这里实测 不能设置太大 超过单元格原始大小后 打开会提示修复。暂时未找到很好的解法。
            imageData.setTop(5);
            imageData.setRight(40);
            imageData.setBottom(5);
            imageData.setLeft(5);

            // 放入第二个图片
            imageData = new ImageData();
            imageDataList.add(imageData);
            writeCellData.setImageDataList(imageDataList);
            imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
            imageData.setImageType(ImageType.PICTURE_TYPE_PNG);
            imageData.setTop(5);
            imageData.setRight(5);
            imageData.setBottom(5);
            imageData.setLeft(50);
            // 设置图片的位置 假设 现在目标 是 覆盖 当前单元格 和当前单元格右边的单元格
            // 起点相对于当前单元格为0 当然可以不写
            imageData.setRelativeFirstRowIndex(0);
            imageData.setRelativeFirstColumnIndex(0);
            imageData.setRelativeLastRowIndex(0);
            // 前面3个可以不写  下面这个需要写 也就是 结尾 需要相对当前单元格 往右移动一格
            // 也就是说 这个图片会覆盖当前单元格和 后面的那一格
            imageData.setRelativeLastColumnIndex(1);

            // 写入数据
            EasyExcel.write(fileName, ImageDemoData.class).sheet().doWrite(list);
        }
    }

超链接、备注、公式、指定单元格的样式、单元格多样式

excel示例

img

对象

@Getter
@Setter
@EqualsAndHashCode
public class WriteCellDemoData {
    /**
     * 超链接
     *
     * @since 3.0.0-beta1
     */
    private WriteCellData<String> hyperlink;

    /**
     * 备注
     *
     * @since 3.0.0-beta1
     */
    private WriteCellData<String> commentData;

    /**
     * 公式
     *
     * @since 3.0.0-beta1
     */
    private WriteCellData<String> formulaData;

    /**
     * 指定单元格的样式。当然样式 也可以用注解等方式。
     *
     * @since 3.0.0-beta1
     */
    private WriteCellData<String> writeCellStyle;

    /**
     * 指定一个单元格有多个样式
     *
     * @since 3.0.0-beta1
     */
    private WriteCellData<String> richText;
}

代码

    /**
     * 超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link WriteCellDemoData}
     * <p>
     * 2. 直接写即可
     *
     * @since 3.0.0-beta1
     */
    @Test
    public void writeCellDataWrite() {
        String fileName = TestFileUtil.getPath() + "writeCellDataWrite" + System.currentTimeMillis() + ".xlsx";
        WriteCellDemoData writeCellDemoData = new WriteCellDemoData();

        // 设置超链接
        WriteCellData<String> hyperlink = new WriteCellData<>("官方网站");
        writeCellDemoData.setHyperlink(hyperlink);
        HyperlinkData hyperlinkData = new HyperlinkData();
        hyperlink.setHyperlinkData(hyperlinkData);
        hyperlinkData.setAddress("https://github.com/alibaba/easyexcel");
        hyperlinkData.setHyperlinkType(HyperlinkType.URL);

        // 设置备注
        WriteCellData<String> comment = new WriteCellData<>("备注的单元格信息");
        writeCellDemoData.setCommentData(comment);
        CommentData commentData = new CommentData();
        comment.setCommentData(commentData);
        commentData.setAuthor("Jiaju Zhuang");
        commentData.setRichTextStringData(new RichTextStringData("这是一个备注"));
        // 备注的默认大小是按照单元格的大小 这里想调整到4个单元格那么大 所以向后 向下 各额外占用了一个单元格
        commentData.setRelativeLastColumnIndex(1);
        commentData.setRelativeLastRowIndex(1);

        // 设置公式
        WriteCellData<String> formula = new WriteCellData<>();
        writeCellDemoData.setFormulaData(formula);
        FormulaData formulaData = new FormulaData();
        formula.setFormulaData(formulaData);
        // 将 123456789 中的第一个数字替换成 2
        // 这里只是例子 如果真的涉及到公式 能内存算好尽量内存算好 公式能不用尽量不用
        formulaData.setFormulaValue("REPLACE(123456789,1,1,2)");

        // 设置单个单元格的样式 当然样式 很多的话 也可以用注解等方式。
        WriteCellData<String> writeCellStyle = new WriteCellData<>("单元格样式");
        writeCellStyle.setType(CellDataTypeEnum.STRING);
        writeCellDemoData.setWriteCellStyle(writeCellStyle);
        WriteCellStyle writeCellStyleData = new WriteCellStyle();
        writeCellStyle.setWriteCellStyle(writeCellStyleData);
        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.
        writeCellStyleData.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        // 背景绿色
        writeCellStyleData.setFillForegroundColor(IndexedColors.GREEN.getIndex());

        // 设置单个单元格多种样式
        // 这里需要设置 inMomery=true 不然会导致无法展示单个单元格多种样式,所以慎用
        WriteCellData<String> richTest = new WriteCellData<>();
        richTest.setType(CellDataTypeEnum.RICH_TEXT_STRING);
        writeCellDemoData.setRichText(richTest);
        RichTextStringData richTextStringData = new RichTextStringData();
        richTest.setRichTextStringDataValue(richTextStringData);
        richTextStringData.setTextString("红色绿色默认");
        // 前2个字红色
        WriteFont writeFont = new WriteFont();
        writeFont.setColor(IndexedColors.RED.getIndex());
        richTextStringData.applyFont(0, 2, writeFont);
        // 接下来2个字绿色
        writeFont = new WriteFont();
        writeFont.setColor(IndexedColors.GREEN.getIndex());
        richTextStringData.applyFont(2, 4, writeFont);

        List<WriteCellDemoData> data = new ArrayList<>();
        data.add(writeCellDemoData);
        EasyExcel.write(fileName, WriteCellDemoData.class).inMemory(true).sheet("模板").doWrite(data);
    }

根据模板写入

    /**
     * 根据模板写入
     * <p>1. 创建excel对应的实体对象 参照{@link IndexData}
     * <p>2. 使用{@link ExcelProperty}注解指定写入的列
     * <p>3. 使用withTemplate 写取模板
     * <p>4. 直接写即可
     */
    @Test
    public void templateWrite() {
        String templateFileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        String fileName = TestFileUtil.getPath() + "templateWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 这里要注意 withTemplate 的模板文件会全量存储在内存里面,所以尽量不要用于追加文件,如果文件模板文件过大会OOM
        // 如果要再文件中追加(无法在一个线程里面处理,可以在一个线程的建议参照多次写入的demo) 建议临时存储到数据库 或者 磁盘缓存(ehcache) 然后再一次性写入
        EasyExcel.write(fileName, DemoData.class).withTemplate(templateFileName).sheet().doWrite(data());
    }

行高、列宽

行高、列宽使用注解。

@Getter
@Setter
@EqualsAndHashCode
@ContentRowHeight(10)
@HeadRowHeight(20)
@ColumnWidth(25)
public class WidthAndHeightData {
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    /**
     * 宽度为50
     */
    @ColumnWidth(50)
    @ExcelProperty("数字标题")
    private Double doubleData;
}

注解形式自定义样式

since: 2.2.0-beta1

@Getter
@Setter
@EqualsAndHashCode
// 头背景设置成红色 IndexedColors.RED.getIndex()
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 10)
// 头字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
// 内容的背景设置成绿色 IndexedColors.GREEN.getIndex()
@ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 17)
// 内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 20)
public class DemoStyleData {
    // 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
    @HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 14)
    // 字符串的头字体设置成20
    @HeadFontStyle(fontHeightInPoints = 30)
    // 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
    @ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40)
    // 字符串的内容字体设置成20
    @ContentFontStyle(fontHeightInPoints = 30)
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
}/**
 * 样式的数据类
 *
 * @author Jiaju Zhuang
 **/
@Data
// 头背景设置成红色 IndexedColors.RED.getIndex()
@HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 10)
// 头字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
// 内容的背景设置成绿色 IndexedColors.GREEN.getIndex()
@ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 17)
// 内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 20)
public class DemoStyleData {
    // 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
    @HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 14)
    // 字符串的头字体设置成20
    @HeadFontStyle(fontHeightInPoints = 30)
    // 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
    @ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40)
    // 字符串的内容字体设置成20
    @ContentFontStyle(fontHeightInPoints = 30)
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
}

自定义样式

    /**
     * 拦截器形式自定义样式
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 创建一个style策略 并注册
     * <p>
     * 3. 直接写即可
     */
    @Test
    public void handlerStyleWrite() {
        // 方法1 使用已有的策略 推荐
        // HorizontalCellStyleStrategy 每一行的样式都一样 或者隔行一样
        // AbstractVerticalCellStyleStrategy 每一列的样式都一样 需要自己回调每一页
        String fileName = TestFileUtil.getPath() + "handlerStyleWrite" + System.currentTimeMillis() + ".xlsx";
        // 头的策略
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        // 背景设置为红色
        headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setFontHeightInPoints((short)20);
        headWriteCellStyle.setWriteFont(headWriteFont);
        // 内容的策略
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
        contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        // 背景绿色
        contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
        WriteFont contentWriteFont = new WriteFont();
        // 字体大小
        contentWriteFont.setFontHeightInPoints((short)20);
        contentWriteCellStyle.setWriteFont(contentWriteFont);
        // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
        HorizontalCellStyleStrategy horizontalCellStyleStrategy =
            new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);

        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, DemoData.class)
            .registerWriteHandler(horizontalCellStyleStrategy)
            .sheet("模板")
            .doWrite(data());

        // 方法2: 使用easyexcel的方式完全自己写 不太推荐 尽量使用已有策略
        // @since 3.0.0-beta2
        fileName = TestFileUtil.getPath() + "handlerStyleWrite" + System.currentTimeMillis() + ".xlsx";
        EasyExcel.write(fileName, DemoData.class)
            .registerWriteHandler(new CellWriteHandler() {
                @Override
                public void afterCellDispose(CellWriteHandlerContext context) {
                    // 当前事件会在 数据设置到poi的cell里面才会回调
                    // 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
                    if (BooleanUtils.isNotTrue(context.getHead())) {
                        // 第一个单元格
                        // 只要不是头 一定会有数据 当然fill的情况 可能要context.getCellDataList() ,这个需要看模板,因为一个单元格会有多个 WriteCellData
                        WriteCellData<?> cellData = context.getFirstCellData();
                        // 这里需要去cellData 获取样式
                        // 很重要的一个原因是 WriteCellStyle 和 dataFormatData绑定的 简单的说 比如你加了 DateTimeFormat
                        // ,已经将writeCellStyle里面的dataFormatData 改了 如果你自己new了一个WriteCellStyle,可能注解的样式就失效了
                        // 然后 getOrCreateStyle 用于返回一个样式,如果为空,则创建一个后返回
                        WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
                        writeCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
                        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
                        writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);

                        // 这样样式就设置好了 后面有个FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到 cell里面去 所以可以不用管了
                    }
                }
            }).sheet("模板")
            .doWrite(data());

        // 方法3: 使用poi的样式完全自己写 不推荐
        // @since 3.0.0-beta2
        // 坑1:style里面有dataformat 用来格式化数据的 所以自己设置可能导致格式化注解不生效
        // 坑2:不要一直去创建style 记得缓存起来 最多创建6W个就挂了
        fileName = TestFileUtil.getPath() + "handlerStyleWrite" + System.currentTimeMillis() + ".xlsx";
        EasyExcel.write(fileName, DemoData.class)
            .registerWriteHandler(new CellWriteHandler() {
                @Override
                public void afterCellDispose(CellWriteHandlerContext context) {
                    // 当前事件会在 数据设置到poi的cell里面才会回调
                    // 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
                    if (BooleanUtils.isNotTrue(context.getHead())) {
                        Cell cell = context.getCell();
                        // 拿到poi的workbook
                        Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
                        // 这里千万记住 想办法能复用的地方把他缓存起来 一个表格最多创建6W个样式
                        // 不同单元格尽量传同一个 cellStyle
                        CellStyle cellStyle = workbook.createCellStyle();
                        cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
                        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
                        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                        cell.setCellStyle(cellStyle);

                        // 由于这里没有指定dataformat 最后展示的数据 格式可能会不太正确

                        // 这里要把 WriteCellData的样式清空, 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到
                        // cell里面去 会导致自己设置的不一样
                        context.getFirstCellData().setWriteCellStyle(null);
                    }
                }
            }).sheet("模板")
            .doWrite(data());
    }

合并单元格

方法一:注解对象的方法

@Getter
@Setter
@EqualsAndHashCode
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
    // 这一列 每隔2行 合并单元格
    @ContentLoopMerge(eachRow = 2)
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
}

方法二:自定义合并策略

   /**
     * 合并单元格
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData} {@link DemoMergeData}
     * <p>
     * 2. 创建一个merge策略 并注册
     * <p>
     * 3. 直接写即可
     *
     * @since 2.2.0-beta1
     */
    @Test
    public void mergeWrite() {
        // 方法1 注解
        String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
        // 在DemoStyleData里面加上ContentLoopMerge注解
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, DemoMergeData.class).sheet("模板").doWrite(data());

        // 方法2 自定义合并单元格策略
        fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
        // 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写
        LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
    }

使用table去写入

    /**
     * 使用table去写入
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 然后写入table即可
     */
    @Test
    public void tableWrite() {
        String fileName = TestFileUtil.getPath() + "tableWrite" + System.currentTimeMillis() + ".xlsx";
        // 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案
        // 这里 需要指定写用哪个class去写
        try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
            // 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
            WriteSheet writeSheet = EasyExcel.writerSheet("模板").needHead(Boolean.FALSE).build();
            // 这里必须指定需要头,table 会继承sheet的配置,sheet配置了不需要,table 默认也是不需要
            WriteTable writeTable0 = EasyExcel.writerTable(0).needHead(Boolean.TRUE).build();
            WriteTable writeTable1 = EasyExcel.writerTable(1).needHead(Boolean.TRUE).build();
            // 第一次写入会创建头
            excelWriter.write(data(), writeSheet, writeTable0);
            // 第二次写如也会创建头,然后在第一次的后面写入数据
            excelWriter.write(data(), writeSheet, writeTable1);
        }
    }

动态头

    /**
     * 动态头,实时生成头写入
     * <p>
     * 思路是这样子的,先创建List<String>头格式的sheet仅仅写入头,然后通过table 不写入头的方式 去写入数据
     *
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 然后写入table即可
     */
    @Test
    public void dynamicHeadWrite() {
        String fileName = TestFileUtil.getPath() + "dynamicHeadWrite" + System.currentTimeMillis() + ".xlsx";
        EasyExcel.write(fileName)
            // 这里放入动态头
            .head(head()).sheet("模板")
            // 当然这里数据也可以用 List<List<String>> 去传入
            .doWrite(data());
    }

    private List<List<String>> head() {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("字符串" + System.currentTimeMillis());
        List<String> head1 = new ArrayList<String>();
        head1.add("数字" + System.currentTimeMillis());
        List<String> head2 = new ArrayList<String>();
        head2.add("日期" + System.currentTimeMillis());
        list.add(head0);
        list.add(head1);
        list.add(head2);
        return list;
    }

自动列宽(不太精确)

对象

@Getter
@Setter
@EqualsAndHashCode
public class LongestMatchColumnWidthData {
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题很长日期标题很长日期标题很长很长")
    private Date date;
    @ExcelProperty("数字")
    private Double doubleData;
}

代码

   /**
     * 自动列宽(不太精确)
     * <p>
     * 这个目前不是很好用,比如有数字就会导致换行。而且长度也不是刚好和实际长度一致。 所以需要精确到刚好列宽的慎用。 当然也可以自己参照
     * {@link LongestMatchColumnWidthStyleStrategy}重新实现.
     * <p>
     * poi 自带{@link SXSSFSheet#autoSizeColumn(int)} 对中文支持也不太好。目前没找到很好的算法。 有的话可以推荐下。
     *
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link LongestMatchColumnWidthData}
     * <p>
     * 2. 注册策略{@link LongestMatchColumnWidthStyleStrategy}
     * <p>
     * 3. 直接写即可
     */
    @Test
    public void longestMatchColumnWidthWrite() {
        String fileName =
            TestFileUtil.getPath() + "longestMatchColumnWidthWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, LongestMatchColumnWidthData.class)
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).sheet("模板").doWrite(dataLong());
    }

    private List<LongestMatchColumnWidthData> dataLong() {
        List<LongestMatchColumnWidthData> list = new ArrayList<LongestMatchColumnWidthData>();
        for (int i = 0; i < 10; i++) {
            LongestMatchColumnWidthData data = new LongestMatchColumnWidthData();
            data.setString("测试很长的字符串测试很长的字符串测试很长的字符串" + i);
            data.setDate(new Date());
            data.setDoubleData(1000000000000.0);
            list.add(data);
        }
        return list;
    }

自定义拦截器

定义拦截器

/**
 * 自定义拦截器。对第一行第一列的头超链接到:https://github.com/alibaba/easyexcel
 *
 * @author Jiaju Zhuang
 */
@Slf4j
public class CustomCellWriteHandler implements CellWriteHandler {

    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        Cell cell = context.getCell();
        // 这里可以对cell进行任何操作
        log.info("第{}行,第{}列写入完成。", cell.getRowIndex(), cell.getColumnIndex());
        if (BooleanUtils.isTrue(context.getHead()) && cell.getColumnIndex() == 0) {
            CreationHelper createHelper = context.getWriteSheetHolder().getSheet().getWorkbook().getCreationHelper();
            Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL);
            hyperlink.setAddress("https://github.com/alibaba/easyexcel");
            cell.setHyperlink(hyperlink);
        }
    }

}
/**
 * 自定义拦截器.对第一列第一行和第二行的数据新增下拉框,显示 测试1 测试2
 *
 * @author Jiaju Zhuang
 */
@Slf4j
public class CustomSheetWriteHandler implements SheetWriteHandler {

    @Override
    public void afterSheetCreate(SheetWriteHandlerContext context) {
        log.info("第{}个Sheet写入成功。", context.getWriteSheetHolder().getSheetNo());

        // 区间设置 第一列第一行和第二行的数据。由于第一行是头,所以第一、二行的数据实际上是第二三行
        CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, 2, 0, 0);
        DataValidationHelper helper = context.getWriteSheetHolder().getSheet().getDataValidationHelper();
        DataValidationConstraint constraint = helper.createExplicitListConstraint(new String[] {"测试1", "测试2"});
        DataValidation dataValidation = helper.createValidation(constraint, cellRangeAddressList);
        context.getWriteSheetHolder().getSheet().addValidationData(dataValidation);
    }
}

代码

    /**
     * 下拉,超链接等自定义拦截器(上面几点都不符合但是要对单元格进行操作的参照这个)
     * <p>
     * demo这里实现2点。1. 对第一行第一列的头超链接到:https://github.com/alibaba/easyexcel 2. 对第一列第一行和第二行的数据新增下拉框,显示 测试1 测试2
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 注册拦截器 {@link CustomCellWriteHandler} {@link CustomSheetWriteHandler}
     * <p>
     * 2. 直接写即可
     */
    @Test
    public void customHandlerWrite() {
        String fileName = TestFileUtil.getPath() + "customHandlerWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, DemoData.class).registerWriteHandler(new CustomSheetWriteHandler())
            .registerWriteHandler(new CustomCellWriteHandler()).sheet("模板").doWrite(data());
    }

插入批注

定义拦截器

/**
 * 自定义拦截器.新增注释,第一行头加批注
 *
 * @author Jiaju Zhuang
 */
@Slf4j
public class CommentWriteHandler implements RowWriteHandler {

    @Override
    public void afterRowDispose(RowWriteHandlerContext context) {
        if (BooleanUtils.isTrue(context.getHead())) {
            Sheet sheet = context.getWriteSheetHolder().getSheet();
            Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
            // 在第一行 第二列创建一个批注
            Comment comment =
                drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short)1, 0, (short)2, 1));
            // 输入批注信息
            comment.setString(new XSSFRichTextString("创建批注!"));
            // 将批注添加到单元格对象中
            sheet.getRow(0).getCell(1).setCellComment(comment);
        }
    }

}

代码

    /**
     * 插入批注
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 注册拦截器 {@link CommentWriteHandler}
     * <p>
     * 2. 直接写即可
     */
    @Test
    public void commentWrite() {
        String fileName = TestFileUtil.getPath() + "commentWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 这里要注意inMemory 要设置为true,才能支持批注。目前没有好的办法解决 不在内存处理批注。这个需要自己选择。
        EasyExcel.write(fileName, DemoData.class).inMemory(Boolean.TRUE).registerWriteHandler(new CommentWriteHandler())
            .sheet("模板").doWrite(data());
    }

可变标题处理(包括标题国际化处理等)

对象

@Getter
@Setter
@EqualsAndHashCode
public class ConverterData {
    /**
     * 我想所有的 字符串起前面加上"自定义:"三个字
     */
    @ExcelProperty(value = "字符串标题", converter = CustomStringStringConverter.class)
    private String string;
    /**
     * 我想写到excel 用年月日的格式
     */
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty("日期标题")
    private Date date;
    /**
     * 我想写到excel 用百分比表示
     */
    @NumberFormat("#.##%")
    @ExcelProperty(value = "数字标题")
    private Double doubleData;
}

代码

    /**
     * 可变标题处理(包括标题国际化等)
     * <p>
     * 简单的说用List<List<String>>的标题 但是还支持注解
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link ConverterData}
     * <p>
     * 2. 直接写即可
     */
    @Test
    public void variableTitleWrite() {
        // 写法1
        String fileName = TestFileUtil.getPath() + "variableTitleWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, ConverterData.class).head(variableTitleHead()).sheet("模板").doWrite(data());
    }

    private List<List<String>> variableTitleHead() {
        List<List<String>> list = ListUtils.newArrayList();
        List<String> head0 = ListUtils.newArrayList();
        head0.add("string" + System.currentTimeMillis());
        List<String> head1 = ListUtils.newArrayList();
        head1.add("number" + System.currentTimeMillis());
        List<String> head2 = ListUtils.newArrayList();
        head2.add("date" + System.currentTimeMillis());
        list.add(head0);
        list.add(head1);
        list.add(head2);
        return list;
    }    private List<List<String>> variableTitleHead() {
        List<List<String>> list = new ArrayList<>();
        List<String> head0 = new ArrayList<>();
        head0.add("string" + System.currentTimeMillis());
        List<String> head1 = new ArrayList<>();
        head1.add("number" + System.currentTimeMillis());
        List<String> head2 = new ArrayList<>();
        head2.add("date" + System.currentTimeMillis());
        list.add(head0);
        list.add(head1);
        list.add(head2);
        return list;
    }

不创建对象的写

    /**
     * 不创建对象的写
     */
    @Test
    public void noModelWrite() {
        // 写法1
        String fileName = TestFileUtil.getPath() + "noModelWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName).head(head()).sheet("模板").doWrite(dataList());
    }

    private List<List<String>> head() {
        List<List<String>> list = ListUtils.newArrayList();
        List<String> head0 = ListUtils.newArrayList();
        head0.add("字符串" + System.currentTimeMillis());
        List<String> head1 = ListUtils.newArrayList();
        head1.add("数字" + System.currentTimeMillis());
        List<String> head2 = ListUtils.newArrayList();
        head2.add("日期" + System.currentTimeMillis());
        list.add(head0);
        list.add(head1);
        list.add(head2);
        return list;
    }

    private List<List<Object>> dataList() {
        List<List<Object>> list = ListUtils.newArrayList();
        for (int i = 0; i < 10; i++) {
            List<Object> data = ListUtils.newArrayList();
            data.add("字符串" + i);
            data.add(0.56);
            data.add(new Date());
            list.add(data);
        }
        return list;
    }

Web中写

DEMO代码地址:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/web/WebTest.java

代码

    /**
     * 文件下载(失败了会返回一个有部分数据的Excel)
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DownloadData}
     * <p>
     * 2. 设置返回的 参数
     * <p>
     * 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
     */
    @GetMapping("download")
    public void download(HttpServletResponse response) throws IOException {
        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
    }

Web中写失败返回的JSON

since: 2.1.1

代码:

    /**
     * 文件下载并且失败的时候返回json(默认失败了会返回一个有部分数据的Excel)
     *
     * @since 2.1.1
     */
    @GetMapping("downloadFailedUsingJson")
    public void downloadFailedUsingJson(HttpServletResponse response) throws IOException {
        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
            String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            // 这里需要设置不关闭流
            EasyExcel.write(response.getOutputStream(), DownloadData.class).autoCloseStream(Boolean.FALSE).sheet("模板")
                .doWrite(data());
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            Map<String, String> map = MapUtils.newHashMap();
            map.put("status", "failure");
            map.put("message", "下载文件失败" + e.getMessage());
            response.getWriter().println(JSON.toJSONString(map));
        }
    }

EasyExcel 填充

填充,可以理解类似于Word的邮件合并。

DEMO代码地址:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/fill/FillTest.java

最简单的填充

since 2.1.1

填充模板如下:

img

模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"{","}"代替

最终效果:

img

填充对象:

@Getter
@Setter
@EqualsAndHashCode
public class FillData {
    private String name;
    private double number;
    private Date date;
}

代码

    /**
     * 最简单的填充
     *
     * @since 2.1.1
     */
    @Test
    public void simpleFill() {
        // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
        String templateFileName =
            TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "simple.xlsx";

        // 方案1 根据对象填充
        String fileName = TestFileUtil.getPath() + "simpleFill" + System.currentTimeMillis() + ".xlsx";
        // 这里 会填充到第一个sheet, 然后文件流会自动关闭
        FillData fillData = new FillData();
        fillData.setName("张三");
        fillData.setNumber(5.2);
        EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(fillData);

        // 方案2 根据Map填充
        fileName = TestFileUtil.getPath() + "simpleFill" + System.currentTimeMillis() + ".xlsx";
        // 这里 会填充到第一个sheet, 然后文件流会自动关闭
        Map<String, Object> map = MapUtils.newHashMap();
        map.put("name", "张三");
        map.put("number", 5.2);
        EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(map);
    }

填充列表

    /**
     * 填充列表
     *
     * @since 2.1.1
     */
    @Test
    public void listFill() {
        // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
        // 填充list 的时候还要注意 模板中{.} 多了个点 表示list
        // 如果填充list的对象是map,必须包涵所有list的key,哪怕数据为null,必须使用map.put(key,null)
        String templateFileName =
            TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "list.xlsx";

        // 方案1 一下子全部放到内存里面 并填充
        String fileName = TestFileUtil.getPath() + "listFill" + System.currentTimeMillis() + ".xlsx";
        // 这里 会填充到第一个sheet, 然后文件流会自动关闭
        EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(data());

        // 方案2 分多次 填充 会使用文件缓存(省内存)
        fileName = TestFileUtil.getPath() + "listFill" + System.currentTimeMillis() + ".xlsx";
        try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            excelWriter.fill(data(), writeSheet);
            excelWriter.fill(data(), writeSheet);
        }
    }

复杂的填充

模板

img

最终效果:

img

代码

    /**
     * 复杂的填充
     *
     * @since 2.1.1
     */
    @Test
    public void complexFill() {
        // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
        // {} 代表普通变量 {.} 代表是list的变量
        String templateFileName =
            TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "complex.xlsx";

        String fileName = TestFileUtil.getPath() + "complexFill" + System.currentTimeMillis() + ".xlsx";
        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            // 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。
            // forceNewRow 如果设置了true,有个缺点 就是他会把所有的数据都放到内存了,所以慎用
            // 简单的说 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
            // 如果数据量大 list不是最后一行 参照下一个
            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            excelWriter.fill(data(), fillConfig, writeSheet);
            excelWriter.fill(data(), fillConfig, writeSheet);
            Map<String, Object> map = MapUtils.newHashMap();
            map.put("date", "2019年10月9日13:28:28");
            map.put("total", 1000);
            excelWriter.fill(map, writeSheet);
        }
    }

数据量大的填充

    /**
     * 数据量大的复杂填充
     * <p>
     * 这里的解决方案是 确保模板list为最后一行,然后再拼接table.还有03版没救,只能刚正面加内存。
     *
     * @since 2.1.1
     */
    @Test
    public void complexFillWithTable() {
        // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
        // {} 代表普通变量 {.} 代表是list的变量
        // 这里模板 删除了list以后的数据,也就是统计的这一行
        String templateFileName =
            TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "complexFillWithTable.xlsx";

        String fileName = TestFileUtil.getPath() + "complexFillWithTable" + System.currentTimeMillis() + ".xlsx";

        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            // 直接写入数据
            excelWriter.fill(data(), writeSheet);
            excelWriter.fill(data(), writeSheet);

            // 写入list之前的数据
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("date", "2019年10月9日13:28:28");
            excelWriter.fill(map, writeSheet);

            // list 后面还有个统计 想办法手动写入
            // 这里偷懒直接用list 也可以用对象
            List<List<String>> totalListList = ListUtils.newArrayList();
            List<String> totalList = ListUtils.newArrayList();
            totalListList.add(totalList);
            totalList.add(null);
            totalList.add(null);
            totalList.add(null);
            // 第四列
            totalList.add("统计:1000");
            // 这里是write 别和fill 搞错了
            excelWriter.write(totalListList, writeSheet);
            // 总体上写法比较复杂 但是也没有想到好的版本 异步的去写入excel 不支持行的删除和移动,也不支持备注这种的写入,所以也排除了可以
            // 新建一个 然后一点点复制过来的方案,最后导致list需要新增行的时候,后面的列的数据没法后移,后续会继续想想解决方案
        }
    }

横向填充

模板

img

最终效果

img

代码

     /**
     * 横向的填充
     *
     * @since 2.1.1
     */
    @Test
    public void horizontalFill() {
        // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
        // {} 代表普通变量 {.} 代表是list的变量
        String templateFileName =
            TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "horizontal.xlsx";

        String fileName = TestFileUtil.getPath() + "horizontalFill" + System.currentTimeMillis() + ".xlsx";
        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
            excelWriter.fill(data(), fillConfig, writeSheet);
            excelWriter.fill(data(), fillConfig, writeSheet);

            Map<String, Object> map = new HashMap<>();
            map.put("date", "2019年10月9日13:28:28");
            excelWriter.fill(map, writeSheet);
        }
    }

多列表组合填充

模板

img

最终效果:

img

代码

    /**
     * 多列表组合填充填充
     *
     * @since 2.2.0-beta1
     */
    @Test
    public void compositeFill() {
        // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
        // {} 代表普通变量 {.} 代表是list的变量 {前缀.} 前缀可以区分不同的list
        String templateFileName =
            TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "composite.xlsx";

        String fileName = TestFileUtil.getPath() + "compositeFill" + System.currentTimeMillis() + ".xlsx";

        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
            // 如果有多个list 模板上必须有{前缀.} 这里的前缀就是 data1,然后多个list必须用 FillWrapper包裹
            excelWriter.fill(new FillWrapper("data1", data()), fillConfig, writeSheet);
            excelWriter.fill(new FillWrapper("data1", data()), fillConfig, writeSheet);
            excelWriter.fill(new FillWrapper("data2", data()), writeSheet);
            excelWriter.fill(new FillWrapper("data2", data()), writeSheet);
            excelWriter.fill(new FillWrapper("data3", data()), writeSheet);
            excelWriter.fill(new FillWrapper("data3", data()), writeSheet);

            Map<String, Object> map = new HashMap<String, Object>();
            //map.put("date", "2019年10月9日13:28:28");
            map.put("date", new Date());

            excelWriter.fill(map, writeSheet);
        }
    }

标签:Java,读写,Excel,private,class,new,sheet,data,public
From: https://www.cnblogs.com/luyj00436/p/18542236

相关文章

  • 【MonitorUtil】java报错日志工具类
     自定义报警日志打印工具importorg.apache.commons.lang.StringUtils;importjava.io.PrintWriter;importjava.io.StringWriter;/***自定义报警日志打印工具**@authorAngel挤一挤*@date2022/12/13*/publicclassMonitorUtil{/***......
  • 《Java核心技术 卷I》图形用户界面使用字体
    使用字体字体名(fontfacename)指定一种字体,字体名由字体族名(fontfamilyname)和一个可选的后缀(如果"Bold")组成,例如,“Helvetica”和"HelveticaBold"都属于名为“Helvetica”字体族的字体。要想知道某台特定计算机上可用的字体,可以调用GraphicsEnvironment类的getAvaila......
  • 深入 Java 多线程:解锁并发编程的奥秘
    在当今的软件开发世界中,性能和高并发是衡量一个应用程序成败的关键因素。无论是处理高流量的网络请求、执行复杂的数据分析任务,还是管理后台服务中的资源,Java多线程编程都是开发者必备的技能之一。本文将带领你深入Java多线程的世界,解锁并发编程的奥秘。1.并发与并行的区......
  • 2024年Java 八股文-基础篇
    1.为什么说Java语言“编译与解释并存”?高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你......
  • 【NPOI】读取Excel文件
    NPOI是一个流行的.NET库,用于读取和写入Excel文件(包括XLS和XLSX格式)。以下是一个快速的指南,展示如何使用NPOI读取Excel文件的基本步骤。1.安装NPOI首先,你需要在项目中安装NPOI库。可以通过NuGet包管理器安装:Install-PackageNPOI或者通过.NETCLI安装......
  • Java中2个Integer比较相同的4种方式
    Java中2个Integer比较相同的4种方式概要使用前切记Integer的范围是【-128~127】例如:Integera=128;Integerb=128;1,使用==比较【-128~127】区间内返回true,否则返回false//==比较if(a==b){System.out.println("a,b使用==比较返回结果:true");......
  • 20天精通Java,day1
    面向对象编程static(静态):修饰成员变量和成员方法。类变量和类方法:有static修饰,属于类,在计算机中只有一份,会被类的全部对象共享。既可以通过类名访问(推荐),也可以通过对象名访问。类变量应用场景:在开发中如果某个数据只需要一份,且希望能够被共享(访问,修改),则该数据可以被定义成类......
  • 【JAVA】Java基础—面向对象编程:构造方法-实现一个Car类,包含多个构造方法,创建不同的汽
    在Java中,构造方法则是用于创建对象的特殊方法。通过构造方法,可以在创建对象时初始化其属性。构造方法的重载允许我们根据不同的需求定义多个构造方法,从而灵活地创建对象。我们可以将汽车的构造方法比作汽车的配置选项。比如,当你购买一辆汽车时,你可以选择不同的配置,如颜色、型......
  • 新手入门Java自动化测试的利器:Selenium WebDriver
    新手入门Java自动化测试的利器:SeleniumWebDriver今天我们将深入探讨一款强大的Java自动化测试工具——SeleniumWebDriver。在正式介绍SeleniumWebDriver之前,让我们首先对Selenium本身进行简要概述,以便更好地理解其背景和功能。官方学习网站:https://www.selenium.dev/Sele......
  • excel 字符比较函数
    ‌在Excel中,用于字符比较的函数主要有以下几种‌:‌EXACT函数‌:用于比较两个文本字符串是否完全相同,包括大小写。如果两个字符串完全相同(包括大小写),则返回TRUE;否则返回FALSE。例如,=EXACT("ABC","abc")的结果为FALSE,而=EXACT("ABC","ABC")的结果为TRUE‌12。‌FIND函数‌:用......