Easy Excel


Easy Excel

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。

easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。


网站:

1. 写Excel

写Excel,简单来说就是将数据写出到系统外部,比如导出Excel,就属于写Execl。

在正式编写写Excel的代码之前,我们先要了解一些Easy Excel提供给我们的基础知识,比如注解、以及参数的了解。

1.1 注解

使用注解很简单,只要在对应的实体类上面加上注解即可。

1.1.1 ExcelProperty注解

用于匹配excel和实体类的匹配,参数如下:

名称 默认值 描述
value 用于匹配excel中的头,必须全匹配,如果有多行头,会匹配最后一行头
order Integer.MAX_VALUE 优先级高于value,会根据order的顺序来匹配实体和excel中数据的顺序
index -1 优先级高于valueorder,会根据index直接指定到excel中具体的哪一列
converter 自动选择 指定当前字段用什么转换器,默认会自动选择。写的情况下只要实现com.alibaba.excel.converters.Converter#convertToExcelData(com.alibaba.excel.converters.WriteConverterContext<T>)方法即可

1.1.2 ExcelIgnore注解

默认所有字段都会和excel去匹配,加了这个注解会忽略该字段

1.1.3 ExcelIgnoreUnannotated

默认不管加不加ExcelProperty的注解的所有字段都会参与读写,加了ExcelIgnoreUnannotated注解以后,不加ExcelProperty注解的字段就不会参与

1.1.4 DateTimeFormat注解

日期转换,用String去接收excel日期格式的数据会调用这个注解,参数如下:

名称 默认值 描述
value 参照java.text.SimpleDateFormat书写即可
use1904windowing 自动选择 excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始

1.1.5 NumberFormat注解

数字转换,用String去接收excel数字格式的数据会调用这个注解。

名称 默认值 描述
value 参照java.text.DecimalFormat书写即可
roundingMode RoundingMode.HALF_UP 格式化的时候设置舍入模式

1.2 参数

1.2.1 概念介绍

  • WriteWorkbook 可以理解成一个excel
  • WriteSheet 理解成一个excel里面的一个表单
  • WriteTable 一个表单里面如果有多个实际用的表格,则可以用WriteTable

1.2.2 通用参数

WriteWorkbook,WriteSheet ,WriteTable都会有的参数,如果为空,默认使用上级。

名称 默认值 描述
converter 默认加载了很多转换器,这里可以加入不支持的字段
writeHandler 写的处理器。可以实现WorkbookWriteHandlerSheetWriteHandlerRowWriteHandlerCellWriteHandler,在写入excel的不同阶段会调用
relativeHeadRowIndex 0 写入到excel和上面空开几行
head clazz二选一。读取文件头对应的列表,会根据列表匹配数据,建议使用class
clazz head二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据
autoTrim true 会对头、读取到的数据等进行自动trim
use1904windowing false excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始
useScientificFormat false 数字转文本的时候在较大的数值的是否是否采用科学计数法
needHead true 是否需要写入头到excel
useDefaultStyle true 是否使用默认的样式
automaticMergeHead true 自动合并头,头中相同的字段上下左右都会去尝试匹配
excludeColumnIndexes 需要排除对象中的index的数据
excludeColumnFieldNames 需要排除对象中的字段的数据
includeColumnIndexes 只要导出对象中的index的数据
includeColumnFieldNames 只要导出对象中的字段的数据
orderByIncludeColumn false @since 3.3.0 在使用了参数includeColumnFieldNames 或者 includeColumnIndexes的时候,会根据传入集合的顺序排序
filedCacheLocation THREAD_LOCAL @since 3.3.0 解析classfield会有缓存,以前全局放到Map里面,3.3.0 以后默认放到ThreadLocal ,也就是说每次读写都会重新解析class,可以反射修改class的注解,并发场景不会相互影像。 THREAD_LOCAL:默认,每次读写都会缓存,但是不是同一次不会影响
MEMORY:放到全局的内存里面,理论上性能会更好,但是无法通过反射、排除等方法修改导出的对象
NONE:不缓存,性能会变差,涉及到读的同时要写,而且还要反射、排除等方法区修改对象的情况下可以考虑使用。

1.2.3 WriteWorkbook

书写位置如下:

 EasyExcel.write(fileName, DemoData.class)
            // 在 write 方法之后, 在 sheet方法之前都是设置WriteWorkbook的参数
            .sheet("模板")
            .doWrite(() -> {
                // 分页查询数据
                return data();
            });

WriteWorkbook独有的参数:

名称 默认值 描述
excelType 当前excel的类型,支持XLS、XLSX、CSV
outputStream file二选一。写入文件的流
file outputStream二选一。写入的文件
templateInputStream 模板的文件流
templateFile 模板文件
charset Charset#defaultCharset 只有csv文件有用,写入文件的时候使用的编码
autoCloseStream true 自动关闭写入的流。
password 读取文件的密码
inMemory false 是否在内存处理,默认会生成临时文件以节约内存。内存模式效率会更好,但是容易OOM
writeExcelOnException false 写入过程中抛出异常了,是否尝试把数据写入到excel

[!tip]

这里展示的参数是WriteWorkbook独有的参数,还有一部分参数参考1.2.2小节。

1.2.4 WriteSheet

书写位置如下:

 EasyExcel.write(fileName, DemoData.class)
            .sheet("模板")
             // 在 sheet 方法之后, 在 doWrite方法之前都是设置WriteSheet的参数
            .doWrite(() -> {
                // 分页查询数据
                return data();
            });

WriteSheet独有的参数:

名称 默认值 描述
sheetNo 0 需要写入的编码
sheetName 需要写的Sheet名称,默认同sheetNo

[!tip]

这里展示的参数是WriteSheet独有的参数,还有一部分参数参考1.2.2小节。

1.2.5 WriteTable

书写位置如下:

EasyExcel.write(fileName, DemoData.class)
            .sheet("模板")
            .table()
            // 在 table 方法之后, 在 doWrite方法之前都是设置WriteTable的参数
            .doWrite(() -> {
                // 分页查询数据
                return data();
            });

WriteTable独有的参数:

名称 默认值 描述
tableNo 0 需要写入的编码

[!tip]

这里展示的参数是WriteTable独有的参数,还有一部分参数参考1.2.2小节。

1.3 写Excel代码编写

在你了解了前面几个小节,这里我们给出写Excel的准备工作:

  1. 导入Easy Excel相关依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>选择自己合适的版本</version>
    </dependency>
    

    [!note]

    其他的依赖:MySQL、Hutool等基本依赖这里就不讲究了。

  2. 数据库文件准备

    这里我准备的数据库文件如下:

  3. 相关实体类:

    /**
     * 用户表
     * @TableName user
     */
    @TableName(value ="user")
    @Data
    public class User implements Serializable {
        @TableId(value = "id", type = IdType.AUTO)
        private Long id;
    
        @TableField(value = "user_name")
        private String userName;
        
        @TableField(value = "user_account")
        private String userAccount;
        
        @TableField(value = "avatar_url")
        private String avatarUrl;
    
        @TableField(value = "gender")
        private Integer gender;
    
        @TableField(value = "profile")
        private String profile;
    
        @TableField(value = "phone")
        private String phone;
    
        @TableField(value = "email")
        private String email;
    
        @TableField(value = "user_password")
        private String userPassword;
    
        @TableField(value = "create_time")
        private Date createTime;
    
        @TableField(value = "update_time")
        private Date updateTime;
    
        @TableField(value = "is_delete")
        private Integer isDelete;
    
        @TableField(value = "user_role")
        private Integer userRole;
    
        @TableField(value = "planet_code")
        private String planetCode;
    
        @TableField(value = "tags")
        private String tags;
    
        @TableField(value = "user_status")
        private Integer userStatus;
    
        @TableField(exist = false)
        private static final long serialVersionUID = 1L;
    }
    
  4. 写Excel的实体类:

    @Data
    public class UserData {
        @ExcelProperty("用户ID")
        private Long id;
        @ExcelProperty("用户昵称")
        private String userName;
        @ExcelProperty("创建时间")
        private Date createTime;
        /**
         * 忽略这个字段
         */
        @ExcelIgnore
        private String phone;
    }
    

    [!note]

    这里的注解的含义就不说了,前面几小节已经讲解了。

  5. 数据获取相关方法:

    @RestController
    @RequestMapping("test")
    @Slf4j
    public class TestController {
        @Resource
        private UserService userService;
    
        @GetMapping("getUserDataList")
        public List<UserData> getUserDataList(){
            return userService.getUserDataList();
        }
    
    }
    
    public interface UserService extends IService<User> {
    
        List<UserData> getUserDataList();
    }
    
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService{
    
        @Override
        public List<UserData> getUserDataList() {
            List<User> userList = this.list();
            List<User> rangeUserList = userList.stream().skip(0).limit(10).collect(Collectors.toList());
            return BeanUtil.copyToList(rangeUserList,UserData.class);
        }
    }
    

    [!note]

    这里是基本数据的查询,就不多说了。

  6. 文件路径相关工具类:

    import org.apache.commons.collections4.CollectionUtils;
    
    public class TestFileUtil {
    
        public static InputStream getResourcesFileInputStream(String fileName) {
            return Thread.currentThread().getContextClassLoader().getResourceAsStream("" + fileName);
        }
    
        public static String getPath() {
            return TestFileUtil.class.getResource("/").getPath();
        }
    
        public static TestPathBuild pathBuild() {
            return new TestPathBuild();
        }
    
        public static File createNewFile(String pathName) {
            File file = new File(getPath() + pathName);
            if (file.exists()) {
                file.delete();
            } else {
                if (!file.getParentFile().exists()) {
                    file.getParentFile().mkdirs();
                }
            }
            return file;
        }
    
        public static File readFile(String pathName) {
            return new File(getPath() + pathName);
        }
    
        public static File readUserHomeFile(String pathName) {
            return new File(System.getProperty("user.home") + File.separator + pathName);
        }
    
        /**
         * build to test file path
         **/
        public static class TestPathBuild {
            private TestPathBuild() {
                subPath = new ArrayList<>();
            }
    
            private final List<String> subPath;
    
            public TestPathBuild sub(String dirOrFile) {
                subPath.add(dirOrFile);
                return this;
            }
    
            public String getPath() {
                if (CollectionUtils.isEmpty(subPath)) {
                    return TestFileUtil.class.getResource("/").getPath();
                }
                if (subPath.size() == 1) {
                    return TestFileUtil.class.getResource("/").getPath() + subPath.get(0);
                }
                StringBuilder path = new StringBuilder(TestFileUtil.class.getResource("/").getPath());
                path.append(subPath.get(0));
                for (int i = 1; i < subPath.size(); i++) {
                    path.append(File.separator).append(subPath.get(i));
                }
                return path.toString();
            }
    
        }
    
    }
    

数据和基本的方法都准备好了之后,现在我们正式开始使用Java代码来向Excel来写数据。

1.3.1 最简单的写

现在给出最简单的向Excel写数据的方法:

@Test
public void simpleWrite() {
    // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    EasyExcel.write(fileName, UserData.class)// 参数1:可以是文件名(包含路径),也可以是流;参数2:写到的目标类
        .sheet("最简单的写Excel")//sheet的名称
        .doWrite(() -> {
            // 分页查询数据
            return userService.getUserDataList();
        });
}

执行看看:

执行结果

可见是否和要求的。


上面的代码,使用的链式调用,下面对上面的一些方法简单解读一下:

  1. EasyExcel.write

    EasyExcel.write方法执行之后,返回一个ExcelWriterBuilder对象。该方法有多个重载方法,对应的重载方法如下:

    部分源码

    部分源码

  2. ExcelWriterSheetBuilder的sheet方法

    部分源码

    如果你同时指定了sheetNo和sheetName,sheetName会覆盖掉之前的sheetNo。

  3. ExcelWriterSheetBuilder的doWrite方法

    部分源码

    其实,只要你再往里面跟一步,就能看到真正的write方法,点击excelWriter.write方法,跟进去看看:

    ExcelWrite部分源码

[!tip]

这里的方法只是把代码给展示出来了,并没有仔细解读,后面使用到了再一一讲解。

1.3.2 根据参数只导出指定列

示例代码:

@Test
public void excludeOrIncludeWrite() {
    String fileName = TestFileUtil.getPath() + "excludeWrite.xlsx";

    // 根据用户传入字段 假设我们要忽略 userName
    Set<String> excludeColumnFiledNames = new HashSet<>();
    excludeColumnFiledNames.add("userName");

    EasyExcel.write(fileName, UserData.class)
        .excludeColumnFieldNames(excludeColumnFiledNames) //排除excludeColumnFiledNames中的字段
        .sheet("根据参数只导出指定列-忽略")
        .doWrite(()->userService.getUserDataList());

    fileName = TestFileUtil.getPath() + "includeWrite.xlsx";
    // 根据用户传入字段 假设我们只要导出 userName
    Set<String> includeColumnFiledNames = new HashSet<>();
    includeColumnFiledNames.add("userName");

    EasyExcel.write(fileName, UserData.class)
        .includeColumnFieldNames(includeColumnFiledNames) //仅要includeColumnFiledNames中的字段
        .sheet("根据参数只导出指定列-包含")
        .doWrite(()->userService.getUserDataList());
}

执行结果:

执行结果

这里使用了AbstractExcelWriterParameterBuilder这个抽象类中的excludeColumnFieldNamesincludeColumnIndexes方法:

类关系图

部分源码

1.3.3 指定写入的列

示例代码:

@Data
public class UserData {
    @ExcelProperty(value = "用户ID",index = 0)
    private Long id;
    @ExcelProperty(value = "用户昵称",index = 1)
    private String userName;

    /**
     * 这里设置3,跳了一个数 会导致index=2也就是第三列为空
     */
    @ExcelProperty(value = "创建时间",index = 3)
    private Date createTime;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String phone;
}

这里我使用了ExcelProperty注解中的index参数,它表示写入的列的序号,从0开始

上诉我的实体类中,设置了0、1,之后就直接到3了,也就是第4列,它会导致第3列为空。

@Test
public void indexWrite() {
    String fileName = TestFileUtil.getPath() + "indexWrite.xlsx";
    EasyExcel.write(fileName, UserData.class)
        .sheet("写入指定的列")
        .doWrite(()->userService.getUserDataList());
}

执行结果:

执行结果

可见,将字段写入指定的列,是依靠ExcelProperty注解中的index参数来控制的

1.3.4 复杂头写入

示例代码:

@Data
public class UserData {
    @ExcelProperty(value = {"主标题","二级标题-1","用户ID"})
    private Long id;
    @ExcelProperty(value = {"主标题","二级标题-1","用户昵称"})
    private String userName;

    @ExcelProperty(value = {"主标题","二级标题-2","创建时间"})
    private Date createTime;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String phone;
}

执行结果:

执行结果

可见,对于复杂头的写入是依靠ExcelProperty中的value参数来指定

1.3.5 重复多次写入(写到单个或者多个Sheet)

这里需要新增部分代码,为方便本小节测试。

新增实体类UserOtherData

@Data
public class UserOtherData {
    /**
     * 用户邮箱
     */
    @ExcelProperty(value = "邮箱")
    private String email;

    /**
     * 密码
     */
    @ExcelProperty(value = "密码")
    private String userPassword;

    @ExcelProperty(value = "用户标签")
    private String tags;
}

新增实现方法:

@Override
public List<UserOtherData> getUserOtherDataList() {
    List<User> userList = this.list();
    List<User> rangeUserList = userList.stream().skip(0).limit(10).collect(Collectors.toList());
    return BeanUtil.copyToList(rangeUserList,UserOtherData.class);
}

还原UserData实体类:

@Data
public class UserData {
    @ExcelProperty(value = "用户ID")
    private Long id;
    @ExcelProperty(value = "用户昵称")
    private String userName;

    @ExcelProperty(value = "创建时间")
    private Date createTime;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String phone;
}

示例代码:

@Test
public void repeatedWrite() {
    // 方法1: 如果写到同一个sheet
    String fileName = TestFileUtil.getPath() + "repeatedWrite-1.xlsx";
    // 这里 需要指定写用哪个class去写
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build()) {
        // 这里注意 如果同一个sheet只要创建一次
        WriteSheet writeSheet = EasyExcel.writerSheet("重复多次写入").build();
        // 去调用写入,这里我调用了三次,实际使用时根据数据库分页的总的页数来
        for (int i = 0; i < 3; i++) {
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<UserData> data = userService.getUserDataList();
            excelWriter.write(data, writeSheet);
        }
    }

    // 方法2: 如果写到不同的sheet 同一个对象
    fileName = TestFileUtil.getPath() + "repeatedWrite-2.xlsx";
    // 这里 指定文件
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build()) {
        // 去调用写入,这里我调用了三次,实际使用时根据数据库分页的总的页数来。这里最终会写到3个sheet里面
        for (int i = 0; i < 3; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
            WriteSheet writeSheet = EasyExcel.writerSheet(i, "重复多次写入-" + i).build();
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<UserData> data = userService.getUserDataList();
            excelWriter.write(data, writeSheet);
        }
    }

    // 方法3 如果写到不同的sheet 不同的对象
    fileName = TestFileUtil.getPath() + "repeatedWrite-3.xlsx";
    // 这里 指定文件
    try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
        // 去调用写入,这里我调用了三次,实际使用时根据数据库分页的总的页数来。这里最终会写到3个sheet里面
        for (int i = 0; i < 3; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。
            // 这里注意UserData.class 可以每次都变,我这里为了方便 所以用的同一个class
            // 实际上可以一直变
            if (i % 2 == 0){
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "重复多次写入-" + i)
                    .head(UserData.class)
                    .build();
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<UserData> data = userService.getUserDataList();
                excelWriter.write(data, writeSheet);
            }else {
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "重复多次写入-" + i)
                    .head(UserOtherData.class)
                    .build();
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<UserOtherData> data = userService.getUserOtherDataList();
                excelWriter.write(data, writeSheet);
            }

        }
    }
}

执行结果:

执行结果

[!tip]

可能会有人看不懂本小节的代码,建议你看看1.3.1小节-最简单的写,后面的参数解读,自己去看看源码,你大概就能明白一些了。

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

实体类数据:

@Data
public class UserData {
    @ExcelProperty(value = "用户ID")
    private Long id;
    @ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class)
    private String username;

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = "创建时间")
    private Date createTime;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String phone;
}

[!tip]

可见指定日期类型使用注解:@DateTimeFormat。自定义格式转换使用注解@ExcelProperty中的参数converter。

这里没有给出数字的转换,其实使用@NumberFormat注解即可。可自行学习使用。

自定义转化类

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;

/**
 * @author 念心卓
 * @version 1.0
 * @description: TODO
 * @date 2024/7/16 21:31
 */
public class CustomStringStringConverter implements Converter<String> {
    /**
     * 返回本类支持的Java类型键。
     * 该方法用于指示本类处理的Java类型。在本例中,返回的是String.class,表示这个类支持的Java类型是String。
     * 这种类型的支持通常用于类型转换、数据绑定或其他需要明确指定支持类型的情境。
     *
     * @return 支持的Java类型的Class对象。在这个方法中,返回的是String.class。
     */
    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    }

    /**
     * 返回本类支持的Excel单元格数据类型。
     *
     * @return 返回CellDataTypeEnum.STRING,表示本类处理字符串类型的Excel单元格数据。
     */
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    /**
     * 读excel文件转为Java对象的时候会自动调用
     *
     * @param context read converter context
     * @return
     * @throws Exception
     */
    @Override
    public String convertToJavaData(ReadConverterContext<?> context) throws Exception {
        return context.getReadCellData().getStringValue();
    }

    /**
     * 写excel文件时自动调用
     *
     * @param context write context
     * @return
     * @throws Exception
     */
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) throws Exception {
        return new WriteCellData<>("自定义:" + context.getValue());
    }
}

[!caution]

这里实现的Converter类是com.alibaba.excel类下的,包别导错了。

实现代码:

@Test
public void converterWrite() {
    String fileName = TestFileUtil.getPath() + "converterWrite.xlsx";
    // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    EasyExcel.write(fileName, UserData.class)
        .sheet("日期、数字或者自定义格式转换")
        .doWrite(userService.getUserDataList());
}

执行结果:

执行结果

1.3.7 图片导出

再开始讲解本小节之前,先来了解一下一个Easy Excel中新的类:WriteCellData

WriteCellData 类是 EasyExcel 库中用于写入 Excel 单元格数据的扩展类,它继承了 CellData 类,并添加了一些特定于写操作的属性和方法。该类允许用户在写入 Excel 文件时,设置单元格的数据、样式、富文本、图片、评论和超链接等。

类的结构:

@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
public class WriteCellData<T> extends CellData<T> {
    // 用于支持日期类型的数据(仅在写入时支持)
    private LocalDateTime dateValue;

    // 用于支持富文本字符串(仅在写入时支持)
    private RichTextStringData richTextStringDataValue;

    // 用于支持图片
    private List<ImageData> imageDataList;

    // 用于支持评论
    private CommentData commentData;

    // 用于支持超链接
    private HyperlinkData hyperlinkData;

    // 用于设置单元格样式
    private WriteCellStyle writeCellStyle;

    // 原始单元格样式,如果为空,将创建一个新的。如果 writeCellStyle 和 originCellStyle 都存在,将从 writeCellStyle 复制到 originCellStyle
    private CellStyle originCellStyle;

    // 构造函数,根据传入的字符串值创建 WriteCellData 对象,并设置类型为 STRING
    public WriteCellData(String stringValue) {
        //...
    }

    // 构造函数,根据传入的类型创建 WriteCellData 对象
    public WriteCellData(CellDataTypeEnum type) {
        //...
    }

    // 构造函数,根据传入的类型和字符串值创建 WriteCellData 对象,并设置类型为 STRING 或 ERROR
    public WriteCellData(CellDataTypeEnum type, String stringValue) {
        //...
    }

    // 构造函数,根据传入的数字值创建 WriteCellData 对象,并设置类型为 NUMBER
    public WriteCellData(BigDecimal numberValue) {
        //...
    }

    // 构造函数,根据传入的布尔值创建 WriteCellData 对象,并设置类型为 BOOLEAN
    public WriteCellData(Boolean booleanValue) {
        //...
    }

    // 构造函数,根据传入的日期值创建 WriteCellData 对象,并设置类型为 DATE
    public WriteCellData(Date dateValue) {
        //...
    }

    // 构造函数,根据传入的 LocalDateTime 值创建 WriteCellData 对象,并设置类型为 DATE
    public WriteCellData(LocalDateTime dateValue) {
        //...
    }

    // 构造函数,根据传入的图片字节数组创建 WriteCellData 对象,并设置类型为 EMPTY
    public WriteCellData(byte[] image) {
        //...
    }

    // 获取或创建单元格样式
    public WriteCellStyle getOrCreateStyle() {
        //...
    }
}

详细解释:

  • LocalDateTime dateValue 用于存储日期时间值,仅在写入 Excel 时支持
  • RichTextStringData richTextStringDataValue: 用于存储富文本字符串,仅在写入 Excel 时支持
  • List<ImageData> imageDataList: 用于存储图片数据。
  • CommentData commentData: 用于存储单元格的评论数据。
  • HyperlinkData hyperlinkData: 用于存储单元格的超链接数据。
  • WriteCellStyle writeCellStyle: 用于存储单元格的样式信息。
  • CellStyle originCellStyle: 用于存储原始单元格样式,如果 writeCellStyleoriginCellStyle 都存在,则从 writeCellStyle 复制到 originCellStyle

构造函数:

  • WriteCellData(String stringValue): 根据字符串值创建 WriteCellData 对象,并将类型设置为 STRING
  • WriteCellData(CellDataTypeEnum type): 根据指定类型创建 WriteCellData 对象。
  • WriteCellData(CellDataTypeEnum type, String stringValue): 根据类型和字符串值创建 WriteCellData 对象,仅支持 STRINGERROR 类型。
  • WriteCellData(BigDecimal numberValue) 根据数字值创建 WriteCellData 对象,并将类型设置为 NUMBER
  • WriteCellData(Boolean booleanValue): 根据布尔值创建 WriteCellData 对象,并将类型设置为 BOOLEAN
  • WriteCellData(Date dateValue): 根据日期值创建 WriteCellData 对象,并将类型设置为 DATE
  • WriteCellData(LocalDateTime dateValue): 根据 LocalDateTime 值创建 WriteCellData 对象,并将类型设置为 DATE
  • WriteCellData(byte[] image): 根据图片字节数组创建 WriteCellData 对象,并将类型设置为 EMPTY

方法:

  • getOrCreateStyle(): 获取或创建单元格样式。如果 writeCellStyle 为空,将创建一个新的 WriteCellStyle 对象。

WriteCellData 类提供了丰富的功能来处理单元格的数据和样式,使用户能够更灵活地操作 Excel 文件中的单元格内容。


WriteCellData的源码可知,它继承于CellData类,我们现在再来看看CellData类。

CellData 类是 EasyExcel 库中用于表示 Excel 单元格数据的类,它继承了 AbstractCell 类,并封装了单元格的数据类型和值。该类提供了一些基础属性和方法,用于存储和操作单元格中的数据。

类的结构:

@Getter
@Setter
@EqualsAndHashCode
public class CellData<T> extends AbstractCell {
    // 单元格的数据类型
    private CellDataTypeEnum type;

    // 数字类型的单元格值,对应 CellDataTypeEnum.NUMBER
    private BigDecimal numberValue;

    // 字符串或错误类型的单元格值,对应 CellDataTypeEnum.STRING 和 CellDataTypeEnum.ERROR
    private String stringValue;

    // 布尔类型的单元格值,对应 CellDataTypeEnum.BOOLEAN
    private Boolean booleanValue;

    // 转换后的结果数据
    private T data;

    // 单元格的公式数据
    private FormulaData formulaData;

    // 确保对象不为空的方法
    public void checkEmpty() {
        //...
    }
}

详细解释:

  • CellDataTypeEnum type: 表示单元格的数据类型。可能的值包括 STRING, NUMBER, BOOLEAN, ERROR, EMPTY 等。
  • BigDecimal numberValue: 表示数字类型的单元格值,对应 CellDataTypeEnum.NUMBER
  • String stringValue: 表示字符串或错误类型的单元格值,对应 CellDataTypeEnum.STRINGCellDataTypeEnum.ERROR
  • Boolean booleanValue: 表示布尔类型的单元格值,对应 CellDataTypeEnum.BOOLEAN
  • T data: 表示转换后的结果数据。这个属性在一些数据转换操作中会被用到。
  • FormulaData formulaData: 表示单元格的公式数据。

方法:

  • checkEmpty(): 该方法用于确保对象的类型和值不为空。如果类型为空,则将类型设置为 CellDataTypeEnum.EMPTY。然后根据类型检查相应的值是否为空,如果为空则将类型设置为 CellDataTypeEnum.EMPTY

CellData 类是 EasyExcel 库中表示 Excel 单元格数据的基础类,提供了单元格的数据类型和值的封装。它允许开发者灵活地操作 Excel 单元格中的数据,包括数字、字符串、布尔值和公式等。通过 checkEmpty() 方法,还可以确保单元格的数据类型和值不为空,从而提高数据处理的健壮性。


现在再来看看本章的例子。

实体类对象:

@Data
@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;
}

实现方法:

@Test
public void imageWrite() throws Exception {
    String fileName = TestFileUtil.getPath() + "imageWrite.xlsx";

    // 这里注意下 所有的图片都会放到内存 暂时没有很好的解法,大量图片的情况下建议 2选1:
    // 1. 将图片上传到oss 或者其他存储网站: https://www.aliyun.com/product/oss ,然后直接放链接
    // 2. 使用: https://github.com/coobird/thumbnailator 或者其他工具压缩图片

    String imagePath = TestFileUtil.getPath() + "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://md.cqwulyj.cn//Java/%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6/EasyExcel/image-20240718204950221.png"));

        // 这里演示
        // 需要额外放入文字
        // 而且需要放入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(ImageData.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(ImageData.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);
    }
}

执行结果:

执行结果

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

实体类:

@Data
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;
}

实现方法:

@Test
public void writeCellDataWrite() {
    String fileName = TestFileUtil.getPath() + "writeCellDataWrite.xlsx";
    WriteCellDemoData writeCellDemoData = new WriteCellDemoData();

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

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


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


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


    // 设置单个单元格多种样式
    // 这里需要设置 inMomery=true 不然会导致无法展示单个单元格多种样式,所以慎用
    WriteCellData<String> richTest = new WriteCellData<>();
    richTest.setType(CellDataTypeEnum.RICH_TEXT_STRING);
    RichTextStringData richTextStringData = new 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);
    richTest.setRichTextStringDataValue(richTextStringData);
    writeCellDemoData.setRichText(richTest);


    List<WriteCellDemoData> data = new ArrayList<>();
    data.add(writeCellDemoData);
    EasyExcel.write(fileName, WriteCellDemoData.class)
        .inMemory(true)
        .sheet("超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式")
        .doWrite(data);
}

执行结果:

执行结果


上诉是官网的例子,当你看懂了之后自己也能写一个:

package com.example.easyexcel;

import com.alibaba.excel.metadata.data.WriteCellData;
import lombok.Data;

import java.util.Date;

/**
 * @author 念心卓
 * @version 1.0
 * @description: TODO
 * @date 2024/7/18 21:46
 */
@Data
public class UserDataWriteCell {

    /**
     * 我要给用户id加上备注
     */
    private WriteCellData<Long> id;

    /**
     * 我要给用户昵称单元格设置背景
     */
    private WriteCellData<String> username;

    /**
     * 我要给创建时间单元格设置链接
     */
    private WriteCellData<Date> createTime;
}

实现方法:

@Test
public void writeCellDataWrite2(){
    List<UserData> userDataList = userService.getUserDataList();

    List<UserDataWriteCell> userDataWriteCellList = new ArrayList<>();
    userDataList.forEach(item->{
        //处理ID数据
        UserDataWriteCell userDataWriteCell = new UserDataWriteCell();

        WriteCellData<Long> id = new WriteCellData<>(new BigDecimal(item.getId()));
        CommentData commentData = new CommentData();
        commentData.setAuthor("念心卓");
        commentData.setRichTextStringData(new RichTextStringData("这是用户ID哟"));
        id.setCommentData(commentData);
        userDataWriteCell.setId(id);


        //处理username数据
        WriteCellData<String> username = new WriteCellData<>(item.getUsername());
        WriteCellStyle writeCellStyle = new WriteCellStyle();
        writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        writeCellStyle.setFillForegroundColor(IndexedColors.ORANGE.getIndex());
        WriteFont writeFont = new WriteFont();
        writeCellStyle.setWriteFont(writeFont);
        username.setWriteCellStyle(writeCellStyle);
        userDataWriteCell.setUsername(username);

        //处理creatTime数据
        WriteCellData<Date> creatTime = new WriteCellData<>(item.getCreateTime());
        HyperlinkData hyperlinkData = new HyperlinkData();
        hyperlinkData.setAddress("https://github.com/alibaba/easyexcel");
        hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
        creatTime.setHyperlinkData(hyperlinkData);
        userDataWriteCell.setCreateTime(creatTime);

        userDataWriteCellList.add(userDataWriteCell);
    });

    String fileName = TestFileUtil.getPath() + "myWriteCellDataWrite.xlsx";
    EasyExcel.write(fileName,UserDataWriteCell.class)
        .sheet("我的超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式")
        .doWrite(userDataWriteCellList);
}

[!caution]

这里对于WriteCellData这个类,其实是有坑的。

因为你查看它的源码,你会发现:

WriteCellData部分源码

CellData部分源码

你可能会将每个单元格的数据存放到这个data中,你想根据自己实体类中属性的类型,来使用泛型,将数据存到这个data中,但是后续你会发现,当你执行到EasyExcel...doWrite(数据集合)的时候,程序会报空指针错误,其实就是你的单元格没值了,导致空指针。

你可能会疑惑,为啥会没值呢?明明自己把他存放到了data中去了呀,调试也发现,确实是存进去了,那么这是怎么回事呢?

这里其实使用CellDatacheckEmpty方法你就会明白:

CellData中的checkEmpty方法

这个空指针其实就是将你的单元格的类型设置为了EMPTY,所以后面程序获取 row的值的时候,就报空指针了。

你可能会疑惑,明明我的数据类型是Long的时候,我执行下面的代码:

WriteCellData<Long> writeCellData = new WriteCellData<>();
id.setData(10L);

这你应该明白为啥你的numberValuenull了把,你根本就没有设置numberValue,你看WriteCellData这个类的源码你就会明白了:

可见,只有将数据的值交给WriteCellData的构造函数,来构建,才不会被后面的置为EMPTY。

所以不要直接将值交给CellData中的data属性,应该交给WriteCellData使用构造函数来创建

执行结果:

执行结果

1.3.9 根据模板写入

[!note]

展示不做讲解

1.3.10 列宽、行高

其实列宽和行高主要是受到这几个注解的影响:

  1. @ColumnWidth:该注解用于设置列的宽度。其参数是一个整数,表示列宽的字符数。
  2. @HeadRowHeight:该注解用于设置表头行的高度。其参数也是一个整数,表示行高的点数。
  3. @ContentRowHeight:该注解用于设置内容行(即数据行)的高度。其参数是一个整数,表示行高的点数。

使用示例:

@Data
@ContentRowHeight(30)
@HeadRowHeight(20)
@ColumnWidth(25)
public class UserData {
    @ExcelProperty(value = "用户ID")
    private Long id;
    @ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class)
    private String username;

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = "创建时间")
    private Date createTime;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String phone;
}
@Test
public void widthAndHeightWrite() {
    String fileName = TestFileUtil.getPath() + "widthAndHeightWrite.xlsx";
    EasyExcel.write(fileName, UserData.class)
        .sheet("列宽、行高")
        .doWrite(userService.getUserDataList());
}

执行结果:

执行结果

1.3.11 注解形式自定义样式

  1. @ContentFontStyle

    @ContentFontStyle 注解用于设置单元格内容的字体样式。

    属性:

    • fontName: 字体名称。
    • fontHeightInPoints: 字体大小。
    • bold: 是否加粗。
    • italic: 是否斜体。
    • strikeout: 是否有删除线。
    • typeOffset: 字体偏移(上标、下标)。
    • underline: 是否有下划线。
    • color: 字体颜色。
    • charset:设置字符编码

  2. @ContentStyle

    @ContentStyle 注解用于设置单元格内容的样式。

    属性:

    • dataFormat: 数据格式。
    • hidden: 是否隐藏单元格。
    • locked: 是否锁定单元格。
    • quotePrefix: 是否使用引号前缀。
    • horizontalAlignment: 水平对齐方式。
    • verticalAlignment: 垂直对齐方式。
    • rotation: 旋转角度。
    • wrapText: 是否自动换行。
    • shrinkToFit: 是否缩小字体填充。
    • fillPatternType: 填充模式。
    • fillForegroundColor: 前景色。
    • fillBackgroundColor: 背景色。
    • borderLeft, borderRight, borderTop, borderBottom: 边框类型。
    • leftBorderColor, rightBorderColor, topBorderColor, bottomBorderColor: 边框颜色。

  3. @HeadFontStyle

    @HeadFontStyle 注解用于设置表头的字体样式。

    属性:

    • fontName: 字体名称。
    • fontHeightInPoints: 字体大小。
    • bold: 是否加粗。
    • italic: 是否斜体。
    • strikeout: 是否有删除线。
    • typeOffset: 字体偏移(上标、下标)。
    • underline: 是否有下划线。
    • color: 字体颜色。

  4. **@HeadStyle **

    @HeadStyle 注解用于设置表头的样式。

    属性:

    • dataFormat: 数据格式。
    • hidden: 是否隐藏单元格。
    • locked: 是否锁定单元格。
    • quotePrefix: 是否使用引号前缀。
    • horizontalAlignment: 水平对齐方式。
    • verticalAlignment: 垂直对齐方式。
    • rotation: 旋转角度。
    • wrapText: 是否自动换行。
    • shrinkToFit: 是否缩小字体填充。
    • fillPatternType: 填充模式。
    • fillForegroundColor: 前景色。
    • fillBackgroundColor: 背景色。
    • borderLeft, borderRight, borderTop, borderBottom: 边框类型。
    • leftBorderColor, rightBorderColor, topBorderColor, bottomBorderColor: 边框颜色。


    示例:

    @Data
    // 头背景设置成红色 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(value = "用户ID")
        private Long id;
        @ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class)
        private String username;
    
        @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
        @ExcelProperty(value = "创建时间")
        private Date createTime;
    }
    
    @Test
    public void annotationStyleWrite() {
        String fileName = TestFileUtil.getPath() + "annotationStyleWrite.xlsx";
        EasyExcel.write(fileName, DemoStyleData.class)
            .sheet("注解形式自定义样式")
            .doWrite(userService.getUserDataList());
    }
    

    执行结果:

    执行结果

1.3.12 自定义样式

@Test
public void handlerStyleWrite() {
    // 方法1 使用已有的策略 推荐
    // HorizontalCellStyleStrategy 每一行的样式都一样 或者隔行一样
    // AbstractVerticalCellStyleStrategy 每一列的样式都一样 需要自己回调每一页
    String fileName = TestFileUtil.getPath() + "handlerStyleWrite.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, UserData.class)
        .registerWriteHandler(horizontalCellStyleStrategy)
        .sheet("自定义样式-推荐")
        .doWrite(userService.getUserDataList());

    // 方法2: 使用easyexcel的方式完全自己写 不太推荐 尽量使用已有策略
    // @since 3.0.0-beta2
    fileName = TestFileUtil.getPath() + "handlerStyleWrite-2.xlsx";
    EasyExcel.write(fileName, UserData.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(userService.getUserDataList());

    // 方法3: 使用poi的样式完全自己写 不推荐
    // @since 3.0.0-beta2
    // 坑1:style里面有dataformat 用来格式化数据的 所以自己设置可能导致格式化注解不生效
    // 坑2:不要一直去创建style 记得缓存起来 最多创建6W个就挂了
    fileName = TestFileUtil.getPath() + "handlerStyleWrite-3.xlsx";
    EasyExcel.write(fileName, UserData.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(userService.getUserDataList());
}

1.3.13 合并单元格

在 EasyExcel 中,@OnceAbsoluteMerge@ContentLoopMerge 注解用于实现单元格的合并操作。

  1. @OnceAbsoluteMerge@OnceAbsoluteMerge 注解用于一次性合并绝对位置的单元格。它通常用于合并表格中的特定区域,而不依赖于数据内容

    属性:

    • firstRowIndex: 合并区域的起始行索引(从0开始)。
    • lastRowIndex: 合并区域的结束行索引(从0开始)。
    • firstColumnIndex: 合并区域的起始列索引(从0开始)。
    • lastColumnIndex: 合并区域的结束列索引(从0开始)。
  2. @ContentLoopMerge@ContentLoopMerge 注解用于根据内容循环合并单元格。它通常用于合并具有相同内容的连续单元格。

    属性:

    • eachRow: 每隔多少行进行一次合并。
    • columnExtend: 向右扩展合并的列数,其实就是多少列合并为1列。

示例:

@Data
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {

    @ExcelProperty(value = "用户ID")
    private Long id;

    //这一列 每隔2行 合并单元格
    @ContentLoopMerge(eachRow = 2)
    @ExcelProperty(value = "用户昵称")
    private String username;

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = "创建时间")
    private Date createTime;
}

合并方法一:使用注解合并

@Test
public void mergeWrite() {
    String fileName = TestFileUtil.getPath() + "mergeWrite-1.xlsx";
    // 在DemoStyleData里面加上ContentLoopMerge注解
    EasyExcel.write(fileName, DemoMergeData.class)
        .sheet("合并单元格")
        .doWrite(userService.getUserDataList());
}

执行结果:

执行结果

合并方法二:自定义合并策略合并

@Test
public void mergeWrite() {
    // 方法2 自定义合并单元格策略
    fileName = TestFileUtil.getPath() + "mergeWrite-2.xlsx";

    LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 1);
    EasyExcel.write(fileName, UserData.class)
        .registerWriteHandler(loopMergeStrategy)
        .sheet("合并单元格")
        .doWrite(userService.getUserDataList());
}

其中LoopMergeStrategy就是合并策略,这是EasyExcel自带的合并策略。其中LoopMergeStrategy的构造函数有解读:

[!tip]

这里我选择最多的一个构造参数。

public LoopMergeStrategy(int eachRow, int columnExtend, int columnIndex)
  1. eachRow:每隔多少行进行一次合并。这个参数决定了在每个合并区域中包含多少行。例如,如果 eachRow 设置为2,那么每两行会合并成一个单元格。
  2. columnExtend:从指定列开始,向右扩展的列数。这意味着当前列以及向右的 columnExtend 列都会被一起合并。例如,如果 columnExtend 设置为1,那么当前列和它右边的一列都会被合并。
  3. columnIndex:需要应用合并策略的列索引(从0开始)。这个参数指定了从哪一列应用合并策略。例如,如果 columnIndex 设置为2,那么从第3列开始应用合并策略。

执行结果:

执行结果

1.3.14 使用table去写入

示例:

@Test
public void tableWrite() {
    String fileName = TestFileUtil.getPath() + "tableWrite.xlsx";
    // 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案
    // 这里 需要指定写用哪个class去写
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build()) {
        // 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
        WriteSheet writeSheet = EasyExcel
            .writerSheet("使用table去写入")
            .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(userService.getUserDataList(), writeSheet, writeTable0);
        // 第二次写如也会创建头,然后在第一次的后面写入数据
        excelWriter.write(userService.getUserDataList(), writeSheet, writeTable1);
    }
}

执行结果:

执行结果

[!tip]

其实这里的使用table写入,其实就是一个sheet中,存在多个table这种场景。

1.3.15 动态头,实时生成头写入

示例:

@Test
public void dynamicHeadWrite() {
    String fileName = TestFileUtil.getPath() + "dynamicHeadWrite.xlsx";
    EasyExcel.write(fileName)
        // 这里放入动态头
        .head(head()).sheet("动态头,实时生成头写入")
        // 当然这里数据也可以用 List<List<String>> 去传入
        .doWrite(userService.getUserDataList());
}

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

执行结果:

执行结果

1.3.16 自动列宽(不太精确)

为了演示方便,我们这里我们给某一列的表头弄宽一点。

实体类:

@Data
public class UserData {
    @ExcelProperty(value = "用户ID")
    private Long id;
    @ExcelProperty(value = "用户昵称特别特别特别特别特别特别长长长")
    private String username;

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = "创建时间")
    private Date createTime;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String phone;
}

示例方法:

/**
     * 自动列宽(不太精确)
     * <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.xlsx";
    EasyExcel.write(fileName, UserData.class)
        .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
        .sheet("自动列宽(不太精确)")
        .doWrite(dataLong());
}

private List<UserData> dataLong() {
    List<UserData> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        UserData data = new UserData();
        data.setUsername("测试很长的字符串测试很长的字符串测试很长的字符串" + i);
        data.setCreateTime(new Date());
        data.setId((long) i);
        list.add(data);
    }
    return list;
}

执行结果:

执行结果

1.3.17 自定义拦截器(上面几点都不符合但是要对单元格进行操作的参照这个)

单元格拦截器接口CellWriteHandler

package com.alibaba.excel.write.handler;

import java.util.List;

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;


public interface CellWriteHandler extends WriteHandler {

    default void beforeCellCreate(CellWriteHandlerContext context) {
        beforeCellCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRow(),
            context.getHeadData(), context.getColumnIndex(), context.getRelativeRowIndex(), context.getHead());
    }

    default void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
        Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {}

    default void afterCellCreate(CellWriteHandlerContext context) {
        afterCellCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getCell(),
            context.getHeadData(), context.getRelativeRowIndex(), context.getHead());
    }

    default void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
        Head head, Integer relativeRowIndex, Boolean isHead) {}

    default void afterCellDataConverted(CellWriteHandlerContext context) {
        WriteCellData<?> writeCellData = CollectionUtils.isNotEmpty(context.getCellDataList()) ? context
            .getCellDataList().get(0) : null;
        afterCellDataConverted(context.getWriteSheetHolder(), context.getWriteTableHolder(), writeCellData,
            context.getCell(), context.getHeadData(), context.getRelativeRowIndex(), context.getHead());
    }

    default void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
        WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {}

    default void afterCellDispose(CellWriteHandlerContext context) {
        afterCellDispose(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getCellDataList(),
            context.getCell(), context.getHeadData(), context.getRelativeRowIndex(), context.getHead());
    }

    default void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
        List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {}
}

方法解释:

beforeCellCreate方法解释

afterCellCreate方法解释

afterCellDataConverted方法解释

afterCellDispose方法解释

Sheet拦截器接口SheetWriteHandler

public interface SheetWriteHandler extends WriteHandler {
    default void beforeSheetCreate(SheetWriteHandlerContext context) {
        beforeSheetCreate(context.getWriteWorkbookHolder(), context.getWriteSheetHolder());
    }

    default void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {}

    default void afterSheetCreate(SheetWriteHandlerContext context) {
        afterSheetCreate(context.getWriteWorkbookHolder(), context.getWriteSheetHolder());
    }

    default void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {}
}

方法解释:

  1. beforeSheetCreate:在创建工作表之前调用
  2. afterSheetCreate:在工作表创建之后调用

**定义拦截器 **:

package com.example.easyexcel;

import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.handler.context.SheetWriteHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.util.CellRangeAddressList;

@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);
    }
}
package com.example.easyexcel;

import com.alibaba.excel.util.BooleanUtils;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.common.usermodel.HyperlinkType;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Hyperlink;

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

    /**
     * 在所有单元格数据操作完成之后调用
     * @param context
     */
    @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);
        }
    }

}

实现方法:

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

执行结果:

执行结果

1.3.18 插入批注(拦截器实现)

行拦截器:RowWriteHandler

package com.alibaba.excel.write.handler;

import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;

import org.apache.poi.ss.usermodel.Row;

/**
 * intercepts handle row creation
 *
 * @author Jiaju Zhuang
 */
public interface RowWriteHandler extends WriteHandler {

    //在创建行之前调用
    default void beforeRowCreate(RowWriteHandlerContext context) {
        beforeRowCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRowIndex(),
            context.getRelativeRowIndex(), context.getHead());
    }
    
    //在创建行之前调用
    default void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer rowIndex,
        Integer relativeRowIndex, Boolean isHead) {}

    //在行创建之后调用
    default void afterRowCreate(RowWriteHandlerContext context) {
        afterRowCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRow(),
            context.getRelativeRowIndex(), context.getHead());
    }

    //在行创建之后调用
    default void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
        Integer relativeRowIndex, Boolean isHead) {}

    //在行的所有操作完成之后调用。在填充数据的情况下,可能会被多次调用。
    default void afterRowDispose(RowWriteHandlerContext context) {
        afterRowDispose(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRow(),
            context.getRelativeRowIndex(), context.getHead());
    }

    //在行的所有操作完成之后调用。在填充数据的情况下,可能会被多次调用。
    default void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
        Integer relativeRowIndex, Boolean isHead) {}
}

示例:

package com.example.easyexcel;

import com.alibaba.excel.util.BooleanUtils;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Comment;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;

/**
 * 自定义拦截器.新增注释,第一行头加批注
 *
 * @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);
        }
    }

}

方法实现:

@Test
public void commentWrite() {
    String fileName = TestFileUtil.getPath() + "commentWrite.xlsx";
    // 这里要注意inMemory 要设置为true,才能支持批注。目前没有好的办法解决 不在内存处理批注。这个需要自己选择。
    EasyExcel.write(fileName, UserData.class)
        .inMemory(Boolean.TRUE)
        .registerWriteHandler(new CommentWriteHandler())
        .sheet("插入批注")
        .doWrite(userService.getUserDataList());
}

执行结果:

执行结果

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

示例方法:

/**
* 可变标题处理(包括标题国际化等)
* <p>
* 简单的说用List<List<String>>的标题 但是还支持注解
* <p>
* 1. 创建excel对应的实体对象 参照{@link UserData}
* <p>
* 2. 直接写即可
*/
@Test
public void variableTitleWrite() {
    // 写法1
    String fileName = TestFileUtil.getPath() + "variableTitleWrite.xlsx";
    EasyExcel.write(fileName, UserData.class)
        .head(variableTitleHead())
        .sheet("可变标题处理(包括标题国际化等)")
        .doWrite(userService.getUserDataList());
}

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

执行结果:

执行结果

1.3.20 不创建对象的写

示例方法:

 @Test
public void noModelWrite() {
    String fileName = TestFileUtil.getPath() + "noModelWrite.xlsx";
    EasyExcel.write(fileName)
        .head(head())
        .sheet("不创建对象的写")
        .doWrite(dataList());
}

private List<List<String>> head() {
    List<List<String>> list = new ArrayList<>();
    List<String> head0 = new ArrayList<>();
    head0.add("数字" + System.currentTimeMillis());
    List<String> head1 = new ArrayList<>();
    head1.add("字符串" + System.currentTimeMillis());
    List<String> head2 = new ArrayList<>();
    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;
}

执行结果:

执行结果

1.3.21 web中的写

示例代码:

/**
* 文件下载(失败了会返回一个有部分数据的Excel)
* <p>
* 1. 创建excel对应的实体对象 参照{@link UserData}
* <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(), UserData.class)
        .sheet("web中的写")
        .doWrite(userService.getUserDataList());
    //关闭流
    response.getOutputStream().close();
}

执行结果:

执行结果

1.3.22 web中的写并且失败的时候返回json

示例代码:

/**
* 文件下载并且失败的时候返回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(), UserData.class)
            .autoCloseStream(Boolean.FALSE)
            .sheet("web中的写并且失败的时候返回json")
            .doWrite(userService.getUserDataList());
    } 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));
    }
}

[!tip]

至此,写Excel相关代码示范已演示完毕,下一节将展示读Excel的编写。


2. 读Excel

2.1 注解

[!tip]

本小节的注解参考小节1.1即可,二者并无区别。

2.2 参数

2.2.1 概念介绍

  • ReadWorkbook 可以理解成一个excel
  • ReadSheet 理解成一个excel里面的一个表单

2.2.2 通用参数

ReadWorkbookReadSheet 都会有的参数,如果为空,默认使用上级。

名称 默认值 描述
converter 默认加载了很多转换器,这里可以加入不支持的字段
readListener 可以注册多个监听器,读取excel的时候会不断的回调监听器中的方法
headRowNumber 1 excel中头的行数,默认1行
head clazz二选一。读取文件头对应的列表,会根据列表匹配数据,建议使用class
clazz head二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据
autoTrim true 会对头、读取数据等进行自动trim
use1904windowing false excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始
useScientificFormat false 数字转文本的时候在较大的数值的是否是否采用科学计数法

2.2.3 ReadWorkbook

书写的位置如下:

 EasyExcel.read(fileName, DemoData.class, new DemoDataListener())
 // 在 read 方法之后, 在 sheet方法之前都是设置ReadWorkbook的参数
 .sheet()
 .doRead();

ReadWorkbook独有的参数:

名称 默认值 描述
excelType 当前excel的类型,支持XLS、XLSX、CSV
inputStream file二选一。读取文件的流,如果接收到的是流就只用流,不用流则建议使用file参数。因为使用了inputStream,easyexcel会帮忙创建临时文件,最终还是file
file inputStream二选一。读取文件的文件。
mandatoryUseInputStream false 强制使用 inputStream 来创建对象,性能会变差,但是不会创建临文件。
charset Charset#defaultCharset 只有csv文件有用,读取文件的时候使用的编码
autoCloseStream true 自动关闭读取的流。
readCache 默认小于5M用内存,超过5M会使用 EhCache,这里不建议使用这个参数
readCacheSelector SimpleReadCacheSelector 用于选择什么时候用内存去存储临时数据,什么时候用磁盘存储临时数据。
ignoreEmptyRow true 忽略空的行
password 读取文件的密码
xlsxSAXParserFactoryName 指定sax读取使用的class的名称,例如:com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl
useDefaultListener true @since 2.1.4 默认会加入ModelBuildEventListener 来帮忙转换成传入class的对象,设置成false后将不会协助转换对象,自定义的监听器会接收到Map<Integer,CellData>对象,如果还想继续接听到class对象,请调用readListener方法,加入自定义的beforeListenerModelBuildEventListener、 自定义的afterListener即可。
extraReadSet 额外需要读取内容的set,默认不读取这些数据
readDefaultReturn STRING @since 3.2.0 STRING:会返回一个Map的数组,返回值就是你在excel里面不点击单元格看到的内容 ACTUAL_DATA:会返回一个Map<Integer,Object>的数组,返回实际上存储的数据,会帮自动转换类型,Object类型为BigDecimalBooleanStringLocalDateTime、null,中的一个, READ_CELL_DATA: 会返回一个Map<Integer,ReadCellData<?>>的数组,其中?类型参照ACTUAL_DATA的

[!tip]

这里展示的参数是ReadWorkbook独有的参数,还有一部分参数参考2.2.2小节。

2.2.4 ReadSheet

书写位置如下:

 EasyExcel.read(fileName, DemoData.class, new DemoDataListener())
 .sheet()
  // 在 sheet 方法之后, 在 doRead方法之前都是设置ReadSheet的参数
 .doRead();

ReadSheet独有的参数:

名称 默认值 描述
sheetNo 0 需要读取Sheet的编码,建议使用这个来指定读取哪个Sheet
sheetName 根据名字去匹配Sheet

[!tip]

这里展示的参数是ReadSheet独有的参数,还有一部分参数参考2.2.2小节。

2.3 读Excel编写

2.3.1 最简单的读

Excel示例:参考之前1.3.1小节生成的文件。

持久层:

package com.example.easyexcel;

import java.util.List;

/**
 * 假设这个是你的DAO存储。当然还要这个类让spring管理,当然你不用需要存储,也不需要这个类。
 **/
public class DemoDAO {
    public void save(List<UserData> list) {
        // 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法batchInsert,所有数据一次性插入
    }
}

最简单的读的监听器

package com.example.easyexcel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

// 有个很重要的点 UserDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class UserDataListener implements ReadListener<UserData> {

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

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

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

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(UserData 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("存储数据库成功!");
    }
}

[!important]

有个很重要的点 UserDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去。

示例方法:

/**
 * 最简单的读
 * <p>
 * 1. 创建excel对应的实体对象 参照{@link UserData}
 * <p>
 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UserDataListener}
 * <p>
 * 3. 直接读即可
*/
@Test
public void simpleRead() {
    // 写法1:JDK8+ ,不用额外写一个DemoDataListener
    // since: 3.0.0-beta1
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    // 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行
    // 具体需要返回多少行可以在`PageReadListener`的构造函数设置
    EasyExcel.read(fileName, UserData.class, new PageReadListener<UserData>(dataList -> {
        for (UserData userData : dataList) {
            log.info("读取到一条数据{}", JSON.toJSONString(userData));
        }
    })).sheet().doRead();

    // 写法2:
    // 匿名内部类 不用额外写一个DemoDataListener
    fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
    EasyExcel.read(fileName, UserData.class, new ReadListener<UserData>() {
        /**
         * 单次缓存的数据量
         */
        public static final int BATCH_COUNT = 100;
        /**
         *临时存储
         */
        private List<UserData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

        @Override
        public void invoke(UserData data, AnalysisContext context) {
            cachedDataList.add(data);
            if (cachedDataList.size() >= BATCH_COUNT) {
                saveData();
                // 存储完成清理 list
                cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            saveData();
        }

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

    // 有个很重要的点 UserDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
    // 写法3:
    fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();

    // 写法4
    fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    // 一个文件一个reader
    try (ExcelReader excelReader = EasyExcel.read(fileName, UserData.class, new UserDataListener()).build()) {
        // 构建一个sheet 这里可以指定名字或者no
        ReadSheet readSheet = EasyExcel.readSheet(0).build();
        // 读取一个sheet
        excelReader.read(readSheet);
    }
}

2.3.2 指定列的下标或者列名

Excel示例:参考之前1.3.1小节生成的文件。

对象:

@Data
public class UserData {
    /**
     * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
     */
    @ExcelProperty(value = "用户ID")
    private Long id;

    /**
     * 强制读取第二列 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
     */
    @ExcelProperty(index = 1)
    private String username;

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = "创建时间")
    private Date createTime;
}

监听器:参考最简单的读监听器

示例方法:

/**
 * 指定列的下标或者列名
 *
 * <p>1. 创建excel对应的实体对象,并使用{@link com.alibaba.excel.annotation.ExcelProperty}注解. 参照{@link UserData}
 * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UserData}
 * <p>3. 直接读即可
*/
@Test
public void indexOrNameRead() {
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    // 这里默认读取第一个sheet
    EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
}

2.3.3 读多个sheet

Excel示例:参考之前1.3.5小节生成的文件。

监听器:参考最简单的读监听器

示例方法:

/**
 * 读多个或者全部sheet,这里注意一个sheet不能读取多次,多次读取需要重新读取文件
 * <p>
 * 1. 创建excel对应的实体对象 参照{@link UserData}
 * <p>
 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UserDataListener}
 * <p>
 * 3. 直接读即可
*/
@Test
public void repeatedRead() {
    String fileName = TestFileUtil.getPath() + "repeatedWrite-3.xlsx";
    // 读取全部sheet
    // 这里需要注意 UserDataListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个UserDataListener里面写
    EasyExcel.read(fileName, UserData.class, new UserDataListener()).doReadAll();
    System.out.println("===============读取全部sheet读取完成,下面开始读取部分sheet===============");

    // 读取部分sheet
    fileName = TestFileUtil.getPath() + "repeatedWrite-3.xlsx";

    // 写法1
    try (ExcelReader excelReader = EasyExcel.read(fileName).build()) {
        // 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能不同必须不同的Listener
        ReadSheet readSheet1 = EasyExcel.readSheet(0)
            .head(UserData.class)
            .registerReadListener(new UserDataListener())
            .build();

        ReadSheet readSheet2 = EasyExcel.readSheet(1)
            .head(UserData.class)
            .registerReadListener(new UserDataListener())
            .build();

        // 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
        excelReader.read(readSheet1, readSheet2);
    }
}

[!caution]

  1. 这里需要注意 UserDataListenerdoAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个UserDataListener里面写。
  2. 读取全部sheet最后使用doReadAll这个API。

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

Excel示例:参考之前1.3.1小节生成的文件。

自定义转换器:

public class CustomStringStringConverter implements Converter<String> {
    /**
     * 返回本类支持的Java类型键。
     * 该方法用于指示本类处理的Java类型。在本例中,返回的是String.class,表示这个类支持的Java类型是String。
     * 这种类型的支持通常用于类型转换、数据绑定或其他需要明确指定支持类型的情境。
     *
     * @return 支持的Java类型的Class对象。在这个方法中,返回的是String.class。
     */
    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    }

    /**
     * 返回本类支持的Excel单元格数据类型。
     *
     * @return 返回CellDataTypeEnum.STRING,表示本类处理字符串类型的Excel单元格数据。
     */
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    /**
     * 读excel文件转为Java对象的时候会自动调用
     *
     * @param context read converter context
     * @return
     * @throws Exception
     */
    @Override
    public String convertToJavaData(ReadConverterContext<?> context) throws Exception {
        return "自定义:" + context.getReadCellData().getStringValue();
    }

    /**
     * 写excel文件时自动调用
     *
     * @param context write context
     * @return
     * @throws Exception
     */
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) throws Exception {
        return new WriteCellData<>(context.getValue());
    }
}

[!caution]

这个转换器和之前的自定义转换器有细微差别,注意比对。

对象:

@Data
public class UserData {
    @ExcelProperty(value = "用户ID")
    private Long id;

    /**
     * 我自定义 转换器,不管数据库传过来什么 。我给他加上“自定义:”
     */
    @ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class)
    private String username;

    /**
     * 这里用string 去接日期才能格式化。我想接收年月日格式
     */
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = "创建时间")
    private String createTime;
}

监听器:参考最简单的读监听器

方法示例:

@Test
public void converterRead() {
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet
    EasyExcel.read(fileName, UserData.class, new UserDataListener())
        // 这里注意 我们也可以registerConverter来指定自定义转换器, 但是这个转换变成全局了, 所有java为string,excel为string的都会用这个转换器。
        // 如果就想单个字段使用请使用@ExcelProperty 指定converter
        // .registerConverter(new CustomStringStringConverter())
        // 读取sheet
        .sheet().doRead();
}

2.3.5 多行头

Excel示例:参考之前1.3.1小节生成的文件。

监听器:参考最简单的读监听器

示例方法:

@Test
public void complexHeaderRead() {
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet 
    EasyExcel.read(fileName, UserData.class, new UserDataListener())
        .sheet()
        // 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据UserData 来解析,他没有指定头,也就是默认1行
        .headRowNumber(1).doRead();
}

这里有headRowNumber这个API,这里要注意headRowNumber如果不指定, 会根据你传入的classExcelPropertyvalue里面的表头的数量来决定行数,如果不传入class则默认为1,当然你指定了headRowNumber不管是否传入class都是以你传入的为准。

2.3.5 同步的返回

Excel示例:参考之前1.3.1小节生成的文件。

示例方法:

@Test
public void synchronousRead() {
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";

    List<UserData> list = EasyExcel.read(fileName).head(UserData.class).sheet().doReadSync();
    for (UserData data : list) {
        log.info("读取到数据:{}", JSON.toJSONString(data));
    }

    // 这里 也可以不指定class,返回一个list,然后读取第一个sheet 同步读取会自动finish
    List<Map<Integer, String>> listMap = EasyExcel.read(fileName).sheet().doReadSync();
    for (Map<Integer, String> data : listMap) {
        // 返回每条数据的键值对 表示所在的列 和所在列的值
        log.info("读取到数据:{}", JSON.toJSONString(data));
    }
}

[!caution]

  1. 同步的返回,不推荐使用,如果数据量大会把数据放到内存里面。
  2. 同步的返回,里面没有使用到自己定义的监听器,是用的同步读的监听器。

2.3.6 读取表头数据

Excel示例:参考之前1.3.1小节生成的文件。

监听器:参考最简单的读监听器,里面多了一个方法,只要重写invokeHeadMap方法即可

/**
 * 这里会一行行的返回头
 *
 * @param headMap
 * @param context
*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
    log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
    // 如果想转成成 Map<Integer,String>
    // 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
    // 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
}

方法示例:

@Test
public void headerRead() {
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
}

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

对象:参考之前1.3.8小节的对象,但是数据类型要变一下。

package com.example.easyexcel;

import lombok.Data;

@Data
public class ReadCellDemoData {
    /**
     * 超链接
     *
     * @since 3.0.0-beta1
     */
    private String hyperlink;

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

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

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

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

Excel示例:参考之前1.3.8小节生成的文件。

监听器:参考最简单的读监听器,里面多了一个 extra 方法。

@Slf4j
public class ReadCellListener implements ReadListener<ReadCellDemoData> {

    @Override
    public void invoke(ReadCellDemoData data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
    }

    @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 {
                    log.error("Unknown hyperlink!");
                }
                break;
            case MERGE:
                log.info(
                        "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
                        extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                        extra.getLastColumnIndex());
                break;
            default:
        }
    }

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

}

示例方法:

@Test
public void extraRead() {
    String fileName = TestFileUtil.getPath() + "writeCellDataWrite.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet
    EasyExcel.read(fileName, ReadCellDemoData.class, new ReadCellListener())
        // 需要读取批注 默认不读取
        .extraRead(CellExtraTypeEnum.COMMENT)
        // 需要读取超链接 默认不读取
        .extraRead(CellExtraTypeEnum.HYPERLINK)
        // 需要读取合并单元格信息 默认不读取
        .extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
}

[!caution]

由于是流式读取,没法在读取到单元格数据的时候直接读取到额外信息,所以只能最后通知哪些单元格有哪些额外信息。

2.3.8 读取公式和单元格类型

Excel示例:参考之前1.3.8小节生成的文件。

监听器:参考最简单的读监听器

对象:参考之前1.3.8小节的对象,但是数据类型要变一下。

package com.example.easyexcel;

import com.alibaba.excel.metadata.data.CellData;
import lombok.Data;

@Data
public class ReadCellDemoData {
    /**
     * 超链接
     *
     * @since 3.0.0-beta1
     */
    private CellData<String> hyperlink;

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

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

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

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

示例方法:

@Test
public void cellDataRead() {
    String fileName = TestFileUtil.getPath() + "writeCellDataWrite.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet
    EasyExcel.read(fileName, ReadCellDemoData.class, new ReadCellListener()).sheet().doRead();
}

2.3.9 数据转换等异常处理

Excel示例:参考之前1.3.1小节生成的文件。

对象:

package com.example.easyexcel;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.Data;

import java.util.Date;

@Data
public class UserData {
    @ExcelProperty(value = "用户ID")
    private Long id;

    /**
     * 用日期去接字符串 肯定报错
     */
    @ExcelProperty(value = "用户昵称")
    private Date username;

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(value = "创建时间")
    private Date createTime;
}

监听器:参考最简单的读监听器,里面多了一个方法,只要重写onException方法即可

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

方法示例:

@Test
public void exceptionRead() {
    String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet
    EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
}

2.3.10 不创建对象的读

Excel示例:参考之前1.3.1小节生成的文件。

监听器:

package com.example.easyexcel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.Map;

@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() + "simpleWrite.xlsx";
    EasyExcel.read(fileName, new NoModelDataListener()).sheet().doRead();
}

2.3.11 web中的读

Excel示例:参考之前1.3.1小节生成的文件。

监听器:参考最简单的读监听器

示例方法:

@Resource
private DemoDAO demoDAO;

/**
     * 文件上传
     *
     * @param file
     * @return
     * @throws IOException
     */
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), UserData.class, new UserDataListener(demoDAO)).sheet().doRead();
    return "success";
}

文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录