马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
EasyExcel02 - 深入实践
本文整理自掘金大佬 - 竹子爱熊猫
https://juejin.cn/post/7405158771017220131
一:通用监听器
利用EasyExcel分析excel文件时,针对差别的业务场景,通常必要界说差别的监听器,如:
- 现在必要导入商品数据,就必要界说一个ProductListener;
- 现在必要导入员工数据,就必要界说一个StaffListener;
- ……
很显然,这一步会造成大量性子类似的class被界说出来,以是,能否封装一个通用监听器,以此来镌汰这步工作量与重复类呢
1:通用监听器
不管是什么业务场景下的Excel导入,都会颠末“分析文件、提取数据、举行业务处理处罚”这三步
除开第三步外,前两步逻辑是类似的,既然逻辑类似,那么天然可以抽象成通用监听器
- package com.example.bootrocketmq.study.wheel.easyexcel;
- import com.alibaba.excel.context.AnalysisContext;
- import com.alibaba.excel.event.AnalysisEventListener;
- import java.lang.reflect.Field;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- /**
- * @author cui haida
- * 2024/12/22
- */
- public class CommonListener<T> extends AnalysisEventListener<T> {
- //创建list集合封装最终的数据
- private final List<T> data;
- // 字段列表
- private final Field[] fields;
- // 类
- private final Class<T> clazz;
- // 是否对excel的有效性进行校验,如果设置为true, 将会检验excel模板的头和数据模型类字段是否匹配
- private boolean validateSwitch = true;
- public CommonListener(Class<T> clazz) {
- fields = clazz.getDeclaredFields();
- this.clazz = clazz;
- this.data = new ArrayList<T>();
- }
- /*
- * 每解析到一行数据都会触发
- * */
- @Override
- public void invoke(T row, AnalysisContext analysisContext) {
- data.add(row);
- }
- /*
- * 读取到excel头信息时触发,会将表头数据转为Map集合
- * */
- @Override
- public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
- // 校验读到的excel表头是否与数据模型类匹配
- if (validateSwitch) {
- // 如果需要进行正确性校验,将会进行工具方法进行excel模板校验 -> header和数据模型类字段关系校验
- ExcelUtil.validateExcelTemplate(headMap, clazz, fields);
- }
- }
- /*
- * 所有数据解析完之后触发
- * */
- @Override
- public void doAfterAllAnalysed(AnalysisContext analysisContext) {}
- /*
- * 关闭excel表头验证
- * */
- public void offValidate() {
- this.validateSwitch = false;
- }
- /*
- * 返回解析到的所有数据
- * */
- public List<T> getData() {
- return data;
- }
- }
复制代码 上述界说了一个通用监听器,实在跟EasyExcel里提供的SyncReadListener非常类似,只不外我们这里做了两点优化
- 利用泛型来取代Object,使其变得更加的机动,获取数据的时间无需举行逼迫转换
- 增长模板校验机制,查抄excel的头信息和数据模子字段的匹配关系
为什么要有第二个呢?
全部excel导入的业务场景,都是先下载模板,再根据模板指引填写数据,末了才上传填写好的excel文件,这是导入场景的业务流程
如果你不对传入的excel文件举行模板校验,这时就算任意传个excel文件,EasyExcel也照旧不会报错,而是分析出多行全部字段为空的数据,从而触发你背面的业务逻辑引发未知Bug
- package com.example.bootrocketmq.study.wheel.easyexcel;
- import com.alibaba.excel.annotation.ExcelIgnore;
- import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
- import com.alibaba.excel.annotation.ExcelProperty;
- import com.alibaba.excel.exception.ExcelAnalysisException;
- import lombok.extern.slf4j.Slf4j;
- import java.lang.reflect.Field;
- import java.util.Collection;
- import java.util.Map;
- /**
- * @author cui haida
- * 2024/12/22
- */
- @Slf4j
- public class ExcelUtil {
- /*
- * 校验excel文件的表头,与数据模型类的映射关系是否匹配
- * */
- public static void validateExcelTemplate(Map<Integer, String> headMap, Class<?> clazz, Field[] fields) {
- // 拿到头部名称
- Collection<String> headNames = headMap.values();
- // 类上是否存在忽略excel字段的注解
- boolean classIgnore = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
- int count = 0;
- for (Field field : fields) {
- // 如果字段上存在忽略注解,则跳过当前字段
- if (field.isAnnotationPresent(ExcelIgnore.class)) {
- continue;
- }
- ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
- if (null == excelProperty) {
- // 如果类上也存在忽略注解,则跳过所有未使用ExcelProperty注解的字段
- if (classIgnore) {
- continue;
- }
- // 如果检测到既未忽略、又未映射excel列的字段,则抛出异常提示模板不正确
- throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
- }
- // 校验数据模型类上绑定的名称,是否与excel列名匹配
- String[] value = excelProperty.value();
- String name = value[0];
- if (name != null && !name.isEmpty() && !headNames.contains(name)) {
- throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
- }
- // 更新有效字段的数量
- count++;
- }
- // 最后校验数据模型类的有效字段数量,与读到的excel列数量是否匹配
- if (headMap.size() != count) {
- throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
- }
- }
- }
复制代码 先来看三个入参,分别是当前读到的表头聚集,以及数据模子类的class对象和字段数组,这三个参数会用来校验模板的准确性。
- 注意点一:ExcelProperty注解提供了列名、权重、索引三种列匹配模式,可现在只是基于列名实现了校验逻辑,以是界说的数据模子类上,不支持利用index、order来指定字段与excel列的匹配关系(也可以自行改造上面的第四步校验逻辑)
- 注意点二:出于校验机制的存在,以是这个通用监听器无法正常读取多行头的excel文件,如果你想要正常分析多行头文件,则可以在创建监听器之后,手动调用offValidate()方法来关闭模板校验机制,这时就能制止校验机制干扰多行头文件的读取
- 注意点三:如果你利用的EasyExcel版本较低,分析excel数据时不会主动忽略已利用过的空行,即填写过内容又删除的数据行,在分析时仍旧会被辨认成一条数据,这种环境必要在监听器的invoke()方法中主动过滤
2:分批处理处罚监听器
前面封装的通用监听器,现实上只能满足数据量不大的业务场景,当数据量到达几万行、数十万、百万行时,如果再利用这个通用监听器就会存在OOM风险
由于分析到的全部数据,都会暂存到内部的data聚集里,直至整个文件全部数据行读取竣事
对于大文件而言,这种模式会给内存造成较大的负担,一旦同时导入的excel文件数量过多,内存资源耗尽就会引发内存溢出标题,怎么办?可以选择分批处理处罚读取到的数据
- package com.example.bootrocketmq.study.wheel.easyexcel;
- import com.alibaba.excel.context.AnalysisContext;
- import com.alibaba.excel.event.AnalysisEventListener;
- import java.lang.reflect.Field;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import java.util.function.Consumer;
- /**
- * @author cui haida
- * 2024/12/22
- */
- public class BatchHandleListener<T> extends AnalysisEventListener<T> {
- // 每次处理的行数(每次处理1000行 - 批次大小)
- private static int BATCH_NUMBER = 1000;
- // 临时存储读取到的excel数据
- private List<T> data;
- // 行数和批次编号
- private int rows, batchNo;
- // 是否要进行excel模板校验,如果需要校验,将validateSwitch设置为true
- private boolean validateSwitch = true;
- /*
- * 每批数据的业务逻辑处理器
- * 说明:如果业务方法会返回结果,可以将换成Function接口,同时类上新增一个类型参数
- **/
- private final Consumer<List<T>> businessHandler;
- /*
- * 用于校验excel模板正确性的字段
- **/
- private final Field[] fields;
- private final Class<T> clazz;
- /**
- * @author cui haida
- * 方法编写日期: 2024/12/22
- * desc -> 构造方法1 - 使用的批次大小为默认的1000条
- */
- public BatchHandleListener(Class<T> clazz, Consumer<List<T>> handle) {
- // 通过构造器为字段赋值,用于校验excel文件与模板十分匹配
- this(clazz, handle, BATCH_NUMBER);
- }
- /**
- * @author cui haida
- * 方法编写日期: 2024/12/22
- * desc -> 构造方法2 - 传入具体使用的批次大小
- */
- public BatchHandleListener(Class<T> clazz, Consumer<List<T>> handle, int batchNumber) {
- // 通过构造器为字段赋值,用于校验excel文件与模板十分匹配
- this.clazz = clazz;
- this.fields = clazz.getDeclaredFields();
- // 初始化临时存储数据的集合,及外部传入的业务方法
- this.businessHandler = handle;
- BATCH_NUMBER = batchNumber;
- this.data = new ArrayList<>(BATCH_NUMBER);
- }
- /*
- * 读取到excel头信息时触发,会将表头数据转为Map集合(用于校验导入的excel文件与模板是否匹配)
- * 注意点1:当前校验逻辑不适用于多行头模板(如果是多行头的文件,请关闭表头验证);
- * 注意点2:使用当前监听器的导入场景,模型类不允许出现既未忽略、又未使用ExcelProperty注解的字段;
- **/
- @Override
- public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
- if (validateSwitch) {
- ExcelUtil.validateExcelTemplate(headMap, clazz, fields);
- }
- }
- /*
- * 每成功解析一条excel数据行时触发
- **/
- @Override
- public void invoke(T row, AnalysisContext analysisContext) {
- data.add(row);
- // 判断当前已解析的数据是否达到本批上限,是则执行对应的业务处理
- if (data.size() >= BATCH_NUMBER) {
- // 更新读取到的总行数、批次数
- rows += data.size();
- batchNo++;
- // 触发业务逻辑处理
- this.businessHandler.accept(data);
- // 处理完本批数据后,使用新List替换旧List,旧List失去引用后会很快被GC
- data = new ArrayList<>(BATCH_NUMBER);
- }
- }
- /*
- * 所有数据解析完之后触发
- **/
- @Override
- public void doAfterAllAnalysed(AnalysisContext analysisContext) {
- // 因为最后一批可能达不到指定的上限,所以解析完之后要做一次收尾处理
- if (!data.isEmpty()) {
- this.businessHandler.accept(data);
- // 更新读取到的总行数、批次数,以及清理集合辅助GC
- rows += data.size();
- batchNo++;
- data.clear();
- }
- }
- /*
- * 关闭excel表头验证
- **/
- public void offValidate() {
- this.validateSwitch = false;
- }
- public int getRows() {
- return rows;
- }
- public int getBatchNo() {
- return batchNo;
- }
- }
复制代码 这个批量处理处罚监听器比上一个通用监听器多了些逻辑
- 起首多了一个批次的概念,当分析的数据量到达给定量级时,就会触发businessHandler业务处理处罚器,而businessHandler则是在监听器初始化时传入的Consumer对象。
- 当实行业务逻辑竣过后,会重新new一个新的聚集吸收数据,而旧聚集被断开引用关系后,会在短时间内被GC,这里不利用clear()方法的缘故起因,是由于clear()内部会去断开与每个元素的引用,这种方式会影响整个文件的读取性能。其次,当分析到的行数还未到达给定阈值时,会继续分析背面的数据,直到抵达阈值后重复前面的流程。
- 这个分批处理处罚监听器,还重写了doAfterAllAnalysed()方法,这个方法会在整个文件分析完成后触发,内里实现的逻辑是为了做好收尾工作,由于末了一批大概达不到指定的上限,以是分析完之后还要视环境做一次处理处罚。不外这里用了clear()方法,毕竟这是末了一次收尾工作,后续也不必要新聚集来吸收数据了,直接整理现有聚集最符合。
该监听器还提供了两个API:
- getRows():获取当前监听器分析的总行数;
- getBatchNo():获取当前正在处理处罚的批次数(批次号)。
这两个方法返回的值,会随着excel文件不绝分析而不绝变革,当文件彻底分析完成后,调用这两个方法可以获取到总批次数以及数据总行数。
关于这个监听器,会在百万级大文件分析的实战中才会用到,先暂时相识即可。
二:封装excel导出工具类
上面listener是导入的时间用到的,那么如果是导出的时间呢?
- package com.example.bootrocketmq.study.wheel.easyexcel;
- import com.alibaba.excel.EasyExcelFactory;
- import com.alibaba.excel.support.ExcelTypeEnum;
- import com.alibaba.excel.write.builder.ExcelWriterBuilder;
- import com.alibaba.excel.write.metadata.style.WriteCellStyle;
- import com.alibaba.excel.write.metadata.style.WriteFont;
- import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
- import org.apache.poi.ss.usermodel.HorizontalAlignment;
- import org.apache.poi.ss.usermodel.IndexedColors;
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
- import java.net.URLEncoder;
- import java.util.List;
- /**
- * @author cui haida
- * 2024/12/22
- * 导出工具类
- */
- public class ExcelExportUtil {
- /*
- * 三种excel文件类型分别对应的响应头格式
- **/
- private static final String XLS_CONTENT_TYPE = "application/vnd.ms-excel";
- private static final String XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
- private static final String CSV_CONTENT_TYPE = "text/csv";
- /*
- * 导出excel的通用方法
- * clazz -> 导出excel所需的数据模型类
- * execlData -> 需要导出的数据列表
- * filenName -> 当前导出的文件名称(不带文件后缀)
- * execlType -> 导出的文件类型(xlsx, xls, csv)
- * response -> 网络相应对象
- **/
- public static void exportExcel(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType, HttpServletResponse response) throws IOException {
- HorizontalCellStyleStrategy styleStrategy = setCellStyle();
- // 对文件名进行UTF-8编码、拼接文件后缀名
- fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20") + excelType.getValue();
- switch (excelType) {
- case XLS:
- response.setContentType(XLS_CONTENT_TYPE);
- break;
- case XLSX:
- response.setContentType(XLSX_CONTENT_TYPE);
- break;
- case CSV:
- response.setContentType(CSV_CONTENT_TYPE);
- break;
- default:
- }
- response.setCharacterEncoding("utf-8");
- response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
- ExcelWriterBuilder writeWork = EasyExcelFactory.write(response.getOutputStream(), clazz);
- writeWork.registerWriteHandler(styleStrategy).excelType(excelType).sheet().doWrite(excelData);
- }
- /*
- * 设置单元格风格
- **/
- public static HorizontalCellStyleStrategy setCellStyle(){
- // 设置表头的样式(背景颜色、字体、居中显示)
- WriteCellStyle headStyle = new WriteCellStyle();
- //设置表头的背景颜色
- headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
- WriteFont headFont = new WriteFont();
- headFont.setFontHeightInPoints((short)12);
- headFont.setBold(true);
- headStyle.setWriteFont(headFont);
- headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
- // 设置Excel内容策略(水平居中)
- WriteCellStyle cellStyle = new WriteCellStyle();
- cellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
- // 创建并返回样式策略对象
- return new HorizontalCellStyleStrategy(headStyle, cellStyle);
- }
- }
复制代码 接着基于response对象,设置了相应的数据范例、编码格式、文件名称等,末了就基于相应对象的输出流写出了天生的excel文件。
固然,通常大文件导出并不会实时返回流给调用方,而是返回OSS地点给前端去下载
- /**
- * @author cui haida
- * 方法编写日期: 2024/12/22
- * desc -> 上传文件到oss
- * @param clazz -> 导出excel所需的数据模型类
- * @param excelData -> 需要导出的数据列表
- * @param excelType -> excel的类型
- * @param fileName -> excel文件名称
- */
- public static String exportExcelToOSS(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType) throws IOException {
- HorizontalCellStyleStrategy styleStrategy = setCellStyle();
- fileName = fileName + excelType.getValue();
- File excelFile = File.createTempFile(fileName, excelType.getValue());
- EasyExcelFactory.write(excelFile, clazz).registerWriteHandler(styleStrategy).sheet().doWrite(excelData);
- String url = uploadFileToOss(excelFile);
- if (excelFile.exists()) {
- boolean flag = excelFile.delete();
- log.info("删除临时文件是否成功:{}", flag);
- }
- return url;
- }
- /*
- * 模拟将上传OSS文件的代码(实际使用请替换为真实上传)
- * @param file 文件对象
- **/
- public static String uploadFileToOss(File file) {
- String resultUrl = "";
- // 省略上传至OSS的代码……
- return resultUrl;
- }
复制代码 三:项目实践
1: excel导入并举行批量落库
为了更加的贴合业务环境,创建对应的数据库表举行测试
- CREATE TABLE `panda` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
- `name` varchar(80) DEFAULT NULL COMMENT '名称',
- `nickname` varchar(80) DEFAULT NULL COMMENT '外号',
- `unique_code` varchar(20) DEFAULT NULL COMMENT '唯一编码',
- `sex` tinyint(1) DEFAULT '2' COMMENT '性别,0:男,1:女,2:未知',
- `height` decimal(6,2) DEFAULT NULL COMMENT '身高',
- `birthday` date DEFAULT NULL COMMENT '出生日期',
- `pic` varchar(255) DEFAULT NULL COMMENT '头像地址',
- `level` varchar(50) DEFAULT '0' COMMENT '等级',
- `motto` varchar(255) DEFAULT NULL COMMENT '座右铭',
- `address` varchar(255) DEFAULT NULL COMMENT '所在地址',
- `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- `is_deleted` tinyint(1) DEFAULT '0' COMMENT '删除标识,0:正常,1:删除',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='熊猫表';
复制代码 这是一张拥有十三个字段的熊猫表,下面初始化一些数据:
- insert into panda(name, nickname, unique_code, sex, height, birthday, pic, level, motto, address, create_time) values
- ("竹子", "小竹", "P888888", 0, '179.99', '2017-07-07', NULL, "高级", "十年磨一剑,五年磨半边!", "太阳省银河市地球村888号", now()),
- ("花花", "阿花", "P666666", 1, '155.00', '2016-06-07', NULL, "顶级", "我爱睡觉爱我!", "太阳省银河市地球村666号", now()),
- ("甜甜", "肥肥", "P999999", 0, '163.43', '2020-02-02', NULL, "特级", "今天的事能拖就拖,明天的事明天再说!", "太阳省银河市地球村999号", now()),
- ("子竹", "小子", "P555555", 1, '188.88', '2021-08-08', NULL, "初级", "你小子!", "太阳省银河市地球村555号", now());
复制代码 前面封装了通用监听器,下面就来试试结果,先来写一个最根本的excel导入接口,对应的导入模板如下:

起首来界说一个与excel模板匹配的数据模子类:
- package org.example.myeasyexcel.model.excel;
- import com.alibaba.excel.annotation.ExcelProperty;
- import com.alibaba.excel.annotation.format.DateTimeFormat;
- import lombok.Data;
- import java.io.Serializable;
- import java.math.BigDecimal;
- import java.util.Date;
- /**
- * @author cui haida
- * 2024/12/23
- * 熊猫导出类
- */
- @Data
- public class PandaReadModel implements Serializable {
- private static final long serialVersionUID = 1L;
- @ExcelProperty("名称")
- private String name;
- @ExcelProperty("外号")
- private String nickname;
- @ExcelProperty("唯一编码")
- private String uniqueCode;
- @ExcelProperty("性别")
- private String sex;
- @ExcelProperty("身高")
- private BigDecimal height;
- @ExcelProperty("出生日期")
- @DateTimeFormat("yyyy-MM-dd")
- private Date birthday;
- @ExcelProperty("等级")
- private String level;
- @ExcelProperty("座右铭")
- private String motto;
- @ExcelProperty("所在地址")
- private String address;
- }
复制代码 这是一个尺度的Java类,没有任何特别的地方,下面再来编写service层的实现逻辑:
- @Override
- @Transactional(rollbackFor = Exception.class)
- public void importExcelV1(MultipartFile file) {
- // 创建通用监听器来解析excel文件
- CommonListener<PandaReadModel> listener = new CommonListener<>(PandaReadModel.class);
- try {
- // 从excel模板中读取数据
- EasyExcelFactory.read(file.getInputStream(), PandaReadModel.class, listener).sheet().doRead();
- } catch (IOException e) {
- log.error("导入熊猫数据出错:{}: {}", e, e.getMessage());
- throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "网络繁忙,请稍后重试!");
- }
- // 对读取到的数据进行批量保存
- List<PandaReadModel> excelData = listener.getData();
- if (excelData.isEmpty()) {
- throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "请检查您上传的excel文件是否为空!");
- }
- batchSaveExcelData(excelData);
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public void batchSaveExcelData(List<PandaReadModel> excelData) {
- List<Panda> pandas = excelData.stream().map(model -> {
- Panda panda = new Panda();
- BeanUtils.copyProperties(model, panda, "sex");
- panda.setSex(Sex.codeOfValue(model.getSex()));
- panda.setCreateTime(new Date());
- return panda;
- }).collect(Collectors.toList());
- saveBatch(pandas);
- }
复制代码 接着界说controller层
- @RestController
- @RequestMapping("/panda")
- public class PandaController {
- @Resource
- private PandaService pandaService;
- @PostMapping("/import/v1")
- public ServerResponse<Void> importExcelV1(MultipartFile file) {
- if (null == file) {
- throw new BusinessException(ResponseCode.FILE_IS_NULL);
- }
- pandaService.importExcelV1(file);
- return ServerResponse.success();
- }
- }
复制代码 调用外部接口就可以发现已经乐成的读取了数据并
2:根据检索条件导出excel
导出也非常的容易,先界说导出模子类:
- package org.example.myeasyexcel.model.vo;
- import com.alibaba.excel.annotation.ExcelIgnore;
- import com.alibaba.excel.annotation.ExcelProperty;
- import com.alibaba.excel.annotation.format.DateTimeFormat;
- import com.alibaba.excel.annotation.write.style.ColumnWidth;
- import lombok.Data;
- import org.example.myeasyexcel.converter.SexConverter;
- import java.io.Serializable;
- import java.math.BigDecimal;
- import java.util.Date;
- /**
- * @author cui haida
- * 2024/12/22
- */
- @Data
- @ColumnWidth(10)
- public class PandaExportVO implements Serializable {
- private static final long serialVersionUID = 1L;
- @ExcelIgnore
- private Long id;
- @ExcelProperty(value = "熊猫昵称", index = 0)
- private String nickname;
- @ExcelProperty(value = "性别", index = 1, converter = SexConverter.class)
- private Integer sex;
- @ExcelProperty(value = "唯一编码", index = 2)
- private String uniqueCode;
- @ExcelProperty(value = "身高", index = 3)
- private BigDecimal height;
- @ExcelProperty(value = "出生日期", index = 4)
- @DateTimeFormat("yyyy-MM-dd")
- @ColumnWidth(15)
- private Date birthday;
- @ExcelProperty(value = "创建时间", index = 5)
- @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
- @ColumnWidth(20)
- private Date createTime;
- }
复制代码 固然,如果导入、导出的字段类似,你可以利用同一个类,不外个人发起是最好分开
下面再来界说一个查询条件参数类:
- package org.example.myeasyexcel.model.dto;
- import lombok.Data;
- import java.io.Serializable;
- /**
- * @author cui haida
- * 2024/12/23
- */
- @Data
- public class PandaQueryDTO implements Serializable {
- private static final long serialVersionUID = 1L;
- /*
- * 搜索关键字(兼容名称、外号、编码三个条件)
- */
- private String keyword;
- /*
- * 生日开始时间(yyyy-MM-dd格式)
- */
- private String startTime;
- /*
- * 生日结束时间(yyyy-MM-dd格式)
- */
- private String endTime;
- }
复制代码 这个DTO类中,统共有三个字段,支持名称、外号、编码的暗昧搜刮,以及基于生日按范围查询熊猫数据,接着来看service层:
- /**
- * 根据条件导出熊猫信息到Excel
- *
- * @param queryDTO 包含查询条件的PandaQueryDTO对象,用于筛选要导出的熊猫信息
- * @param response HttpServletResponse对象,用于将Excel文件作为HTTP响应返回
- *
- * 本方法首先根据查询条件从数据库中筛选出相应的熊猫信息,然后利用ExcelUtil工具类
- * 将这些信息导出为Excel文件,并通过HTTP响应返回给客户端如果导出过程中发生IO异常,
- * 则记录错误日志
并抛出业务异常,提示导出失败 - */
- @Override
- public void exportExcelByCondition(PandaQueryDTO queryDTO, HttpServletResponse response) {
- // 通过查询条件选择出对应的熊猫
- List<PandaExportVO> pandas = baseMapper.selectPandas(queryDTO);
- // 生成唯一文件名,防止重复
- String fileName = "熊猫基本信息集合-" + System.currentTimeMillis();
- try {
- // 通过工具方法进行导入
- ExcelUtil.exportExcel(PandaExportVO.class, pandas, fileName, ExcelTypeEnum.XLSX, response);
- } catch (IOException e) {
- // 记录异常日志
 - log.error("熊猫数据导出失败,{}:{}", e, e.getMessage());
- // 抛出业务异常,提示用户导出失败
- throw new BusinessException("熊猫基本信息导出失败,请稍后再试!");
- }
- }
复制代码 对应的工具方法在上面已经分析,这里在形貌一遍
- package org.example.myeasyexcel.util;
- import com.alibaba.excel.EasyExcelFactory;
- import com.alibaba.excel.ExcelWriter;
- import com.alibaba.excel.annotation.ExcelIgnore;
- import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
- import com.alibaba.excel.annotation.ExcelProperty;
- import com.alibaba.excel.exception.ExcelAnalysisException;
- import com.alibaba.excel.support.ExcelTypeEnum;
- import com.alibaba.excel.write.builder.ExcelWriterBuilder;
- import com.alibaba.excel.write.metadata.style.WriteCellStyle;
- import com.alibaba.excel.write.metadata.style.WriteFont;
- import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.poi.ss.usermodel.HorizontalAlignment;
- import org.apache.poi.ss.usermodel.IndexedColors;
- import org.example.myeasyexcel.enums.ExcelTemplate;
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.IOException;
- import java.io.UnsupportedEncodingException;
- import java.lang.reflect.Field;
- import java.net.URLEncoder;
- import java.util.*;
- /**
- * @author cui haida
- * 2024/12/23
- */
- @Slf4j
- public class ExcelUtil {
- /*
- * 三种excel文件类型分别对应的响应头格式
- * */
- private static final String XLS_CONTENT_TYPE = "application/vnd.ms-excel";
- private static final String XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
- private static final String CSV_CONTENT_TYPE = "text/csv";
- /*
- * 导出excel的通用方法
- * clazz:传入excel导出的VO类
- * data:传入需要导出的数据列表
- * fileName:当前导出的excel文件名称(不带文件后缀)
- * excelType:导出的文件类型
- * response:网络响应对象
- * */
- public static void exportExcel(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType, HttpServletResponse response) throws IOException {
- HorizontalCellStyleStrategy styleStrategy = setCellStyle();
- setResponse(fileName, excelType, response);
- ExcelWriterBuilder writeWork = EasyExcelFactory.write(response.getOutputStream(), clazz);
- writeWork.registerWriteHandler(styleStrategy).excelType(excelType).sheet().doWrite(excelData);
- }
- /*
- * 初始化模板填充导出场景的写对象
- * */
- public static ExcelWriter initExportFillWriter(String fileName, ExcelTypeEnum excelType, ExcelTemplate template, HttpServletResponse response) throws IOException {
- setResponse(fileName, excelType, response);
- return EasyExcelFactory.write(response.getOutputStream())
- .excelType(excelType)
- .withTemplate(template.getPath()).build();
- }
- /*
- * 设置通用的响应头信息
- * */
- public static void setResponse(String fileName, ExcelTypeEnum excelType, HttpServletResponse response) {
- // 对文件名进行UTF-8编码、拼接文件后缀名
- try {
- fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20") + excelType.getValue();
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e);
- }
- switch (excelType) {
- case XLS:
- response.setContentType(XLS_CONTENT_TYPE);
- break;
- case XLSX:
- response.setContentType(XLSX_CONTENT_TYPE);
- break;
- case CSV:
- response.setContentType(CSV_CONTENT_TYPE);
- break;
- }
- response.setCharacterEncoding("utf-8");
- response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
- }
- /*
- * 设置单元格风格
- * */
- public static HorizontalCellStyleStrategy setCellStyle(){
- // 设置表头的样式(背景颜色、字体、居中显示)
- WriteCellStyle headStyle = new WriteCellStyle();
- //设置表头的背景颜色
- headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
- WriteFont headFont = new WriteFont();
- headFont.setFontHeightInPoints((short)12);
- headFont.setBold(true);
- headStyle.setWriteFont(headFont);
- headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
- // 设置Excel内容策略(水平居中)
- WriteCellStyle cellStyle = new WriteCellStyle();
- cellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
- return new HorizontalCellStyleStrategy(headStyle, cellStyle);
- }
- /*
- * 上传错误的Excel文件到OSS
- * */
- public static String exportExcelToOSS(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType) throws IOException {
- HorizontalCellStyleStrategy styleStrategy = ExcelUtil.setCellStyle();
- fileName = fileName + excelType.getValue();
- File excelFile = File.createTempFile(fileName, excelType.getValue());
- EasyExcelFactory.write(excelFile, clazz).registerWriteHandler(styleStrategy).sheet().doWrite(excelData);
- String url = uploadFileToOss(excelFile);
- if (excelFile.exists()) {
- boolean flag = excelFile.delete();
- log.info("删除临时文件是否成功:{}", flag);
- }
- return url;
- }
- /*
- * 模拟将上传OSS文件的代码(实际使用请替换为真实上传)
- * */
- public static String uploadFileToOss(File file) {
- String resultUrl = "https://juejin.cn/user/862486453028888/posts";
- // 省略上传至OSS的代码……
- return resultUrl;
- }
- /*
- * 校验excel文件的表头,与数据模型类的映射关系是否匹配
- * */
- public static void validateExcelTemplate(Map<Integer, String> headMap, Class<?> clazz, Field[] fields) {
- Collection<String> headNames = headMap.values();
- // 类上是否存在忽略excel字段的注解
- boolean classIgnore = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
- int count = 0;
- // 如果EasyExcel框架版本较低,请把这行代码放开,并将有效的字段列表返回,用于空值校验
- // List<Field> validFields = new ArrayList<>(fields.length);
- for (Field field : fields) {
- // 忽略序列化ID字段
- if ("serialVersionUID".equals(field.getName())) {
- continue;
- }
- // 如果字段上存在忽略注解,则跳过当前字段
- if (field.isAnnotationPresent(ExcelIgnore.class)) {
- continue;
- }
- ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
- if (null == excelProperty) {
- // 如果类上也存在忽略注解,则跳过所有未使用ExcelProperty注解的字段
- if (classIgnore) {
- continue;
- }
- // 如果检测到既未忽略、又未映射excel列的字段,则抛出异常提示模板不正确
- throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
- }
- // 校验数据模型类上绑定的名称,是否与excel列名匹配
- String[] value = excelProperty.value();
- String name = value[0];
- if (name != null && 0 != name.length() && !headNames.contains(name)) {
- throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
- }
- // 更新有效字段的数量
- count++;
- // validFields.add(field);
- }
- // 最后校验数据模型类的有效字段数量,与读到的excel列数量是否匹配
- if (headMap.size() != count) {
- throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
- }
- }
- /**
- * 判断整行单元格数据是否均为空(依赖于validateExcelTemplate()方法返回的有效字段列表)
- * 说明:该方法适用于低版本的EasyExcel读取数据时校验,因为低版本的不会自动忽略空行
- */
- public static <T> boolean rowIsNull(T data, List<Field> validFields) {
- if (data instanceof String) {
- return "".equals(data);
- }
- try {
- List<Boolean> fieldNulls = new ArrayList<>(validFields.size());
- for (Field field : validFields) {
- field.setAccessible(true);
- Object value = field.get(data);
- if (Objects.isNull(value)) {
- fieldNulls.add(Boolean.TRUE);
- } else {
- fieldNulls.add(Boolean.FALSE);
- }
- }
- return fieldNulls.stream().allMatch(Boolean.TRUE::equals);
- } catch (Exception e) {
- log.error("读取数据行[{}]解析失败: {}", data, e.getMessage());
- }
- return true;
- }
- }
复制代码 末了编写controller层,就是简朴的对service层举行调用
- @PostMapping("/export/v1")
- public void exportExcelV1(@RequestBody PandaQueryDTO queryDTO, HttpServletResponse response) {
- pandaService.exportExcelByCondition(queryDTO, response);
- }
复制代码
3:校验写入的excel的数据

在有些场景中,我们必要对导入的excel数据举行校验洗濯后才气利用
如果上传的excel文件存在校验堕落的信息,则需像上图一样指明错误缘故起因,末了回传给调用方下载检察,这该怎么实现呢?如下:
- @Override
- @Transactional(rollbackFor = Exception.class)
- public BaseImportExcelVO<String> importExcelV2(MultipartFile file) {
- CommonListener<PandaReadModel> listener = new CommonListener<>(PandaReadModel.class);
- try {
- EasyExcelFactory.read(file.getInputStream(), PandaReadModel.class, listener).sheet().doRead();
- } catch (IOException e) {
- log.error("导入熊猫数据出错:{}: {}", e, e.getMessage());
- throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "网络繁忙,请稍后重试!");
- }
- List<PandaReadModel> excelData = listener.getData();
- if (excelData.isEmpty()) {
- throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "请检查您上传的excel文件是否为空!");
- }
- // 校验excel数据,如果校验未通过直接阻断执行,将错误信息返回给调用方
- BaseImportExcelVO<String> result = validateExcelData(excelData);
- if (result.getErrorFlag()) {
- return result;
- }
- // 对校验通过的数据进行批量落库
- batchSaveExcelData(excelData);
- return result;
- }
- /*
- * 校验导入的excel数据
- * */
- private BaseImportExcelVO<String> validateExcelData(List<PandaReadModel> excelData) {
- BaseImportExcelVO<String> result = new BaseImportExcelVO<>();
- boolean errorFlag = false;
- List<PandaReadErrorModel> validatePandas = new ArrayList<>();
- String birthdayErrorMsg = "生日不能为空;", uniCodeErrorMsg = "唯一编码重复;";
- // 根据唯一编码查询库内的熊猫数
- List<String> uniCodes = excelData.stream().map(PandaReadModel::getUniqueCode).collect(Collectors.toList());
- List<CountVO<String, Integer>> counts = baseMapper.selectCountByUniCodes(uniCodes);
- Map<String, Integer> countMap = counts.stream().collect(Collectors.toMap(CountVO::getKey, CountVO::getValue));
- // 循环对excel所有数据行进行校验
- for (PandaReadModel excelRow : excelData) {
- String errorMsg = "";
- PandaReadErrorModel errorModel = new PandaReadErrorModel();
- BeanUtils.copyProperties(excelRow, errorModel);
- // 如果库里对应的唯一编码能查到熊猫,说明UniqueCode重复
- if (countMap.containsKey(excelRow.getUniqueCode())) {
- errorMsg += uniCodeErrorMsg;
- errorFlag = true;
- }
- // 如果导入的生日字段为空,说明对应的excel行没填写出生日期
- if (null == excelRow.getBirthday()) {
- errorMsg += birthdayErrorMsg;
- errorFlag = true;
- }
- errorModel.setErrorMsg(errorMsg);
- validatePandas.add(errorModel);
- }
- // 如果存在校验未通过的记录,则导出校验出错的数据为excel文件
- if (errorFlag) {
- String url, fileName = "熊猫信息导入-校验出错文件-" + System.currentTimeMillis();
- try {
- url = ExcelUtil.exportExcelToOSS(PandaReadErrorModel.class, validatePandas, fileName, ExcelTypeEnum.XLSX);
- // ExcelUtil.exportExcel(PandaReadErrorModel.class, validatePandas, fileName, ExcelTypeEnum.XLSX, response);
- } catch (IOException e) {
- log.error("生成熊猫导入校验出错文件失败:{}: {}", e, e.getMessage());
- throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "网络繁忙,请稍后重试!");
- }
- result.setResult(url);
- result.setCheckMsg("文件校验未通过!");
- }
- result.setErrorFlag(errorFlag);
- return result;
- }
复制代码 4:部分字段差别的导出场景
来看这个场景,正如上图所示,现在有两个导出需求,可仅仅只有第一列字段差别罢了,这时我们必要界说两个接口吗?答案是不必要,面临这种大多数字段类似、部分字段差别的场景,可以基于多态的特性来实现,起首界说一个父类来声明公共字段:
- @Data
- public class PandaStatisticsExportVO implements Serializable {
- private static final long serialVersionUID = 1L;
- @ExcelProperty(index = 1, value = "熊数")
- private Integer counting;
- @ExcelProperty(index = 2, value = "身高>=170cm熊数")
- private Integer heightGte170cm;
- }
复制代码 接着分别为两个差别的场景界说子类,即:
- /**
- * 熊猫统计导出VO类(根据年龄分组)
- */
- @Data
- @EqualsAndHashCode(callSuper = true)
- public class SexStatisticsExportVO extends PandaStatisticsExportVO implements Serializable {
- private static final long serialVersionUID = 1L;
- @ExcelProperty(index = 0, value = "性别分组", converter = SexConverter.class)
- private Integer sex;
- }
- /**
- * 熊猫统计导出VO类(根据等级分组)
- */
- @Data
- @EqualsAndHashCode(callSuper = true)
- public class LevelStatisticsExportVO extends PandaStatisticsExportVO implements Serializable {
- private static final long serialVersionUID = 1L;
- @ExcelProperty(index = 0, value = "等级分组")
- private String level;
- }
复制代码 这是面向对象语言的尺度写法,值得一提的是:这里公道的利用了ExcelProperty注解的index属性,由于两个表格中,存在差别的字段就是第一列,以是两个子类的差别字段的列索引写成0即可。
下面来看详细的service实现:
- @Override
- public void exportStatisticsData(PandaStatisticsDTO statisticsDTO, HttpServletResponse response) {
- List<PandaStatisticsBO> pandaStatisticsBOs = baseMapper.selectPandaStatistics(statisticsDTO);
- List<PandaStatisticsExportVO> exportData;
- Class<?> clazz;
- // 如果是按性别分组统计,则使用性别的数据模型类
- if (0 == statisticsDTO.getStatisticsType()) {
- clazz = SexStatisticsExportVO.class;
- exportData = pandaStatisticsBOs.stream().map(bo -> {
- SexStatisticsExportVO sexVO = new SexStatisticsExportVO();
- BeanUtils.copyProperties(bo, sexVO);
- return sexVO;
- }).collect(Collectors.toList());
- }
- // 如果是按等级分组统计,则使用等级的数据模型类
- else if (1 == statisticsDTO.getStatisticsType()) {
- clazz = LevelStatisticsExportVO.class;
- exportData = pandaStatisticsBOs.stream().map(bo -> {
- LevelStatisticsExportVO levelVO = new LevelStatisticsExportVO();
- BeanUtils.copyProperties(bo, levelVO);
- return levelVO;
- }).collect(Collectors.toList());
- } else {
- throw new BusinessException("暂不支持这种统计方式哦~");
- }
- // 导出对应的excel数据
- String fileName = "熊猫统计数据-" + System.currentTimeMillis();
- try {
- ExcelUtil.exportExcel(clazz, exportData, fileName, ExcelTypeEnum.XLSX, response);
- } catch (IOException e) {
- log.error("熊猫统计数据导出失败,{}:{}", e, e.getMessage());
- throw new BusinessException("统计数据导出失败,请稍后再试!");
- }
- }
复制代码 代码仍旧非常简朴,起首会根据参数里传入的导出范例,来选择要利用的数据模子类:
- 0:代表按性别统计数据,利用SexStatisticsExportVO模子类;
- 1:代表按品级统计数据,利用LevelStatisticsExportVO模子类。
5:多行头的excel导出
有些业务场景下,我们必要导出像上图如许的归并表头文件,即一个大列下面有多个子列,整个文件由多个大列+N多个子列构成,这种需求该怎么导出呢?
实在非常简朴,只靠ExcelProperty注解就能实现,如下:
- @Data
- public class MultiLineHeadExportVO implements Serializable {
- private static final long serialVersionUID = 1L;
- @ExcelProperty(value = {"基本信息", "昵称"})
- private String nickname;
- @ExcelProperty(value = {"基本信息", "编码"})
- private String uniqueCode;
- @ExcelProperty(value = {"基本信息","性别"}, converter = SexConverter.class)
- private Integer sex;
- @ExcelProperty(value = {"基本信息","身高"})
- private BigDecimal height;
- @ExcelProperty(value = {"基本信息","出生日期"})
- @DateTimeFormat("yyyy-MM-dd")
- @ColumnWidth(15)
- private Date birthday;
- @ExcelProperty(value = {"其他信息","等级"})
- private String level;
- @ExcelProperty(value = {"其他信息","座右铭"})
- @ColumnWidth(30)
- private String motto;
- @ExcelProperty(value = {"其他信息", "创建时间"})
- @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
- @ColumnWidth(20)
- private Date createTime;
- }
复制代码 正如代码所示,如果你想要将某几列归并成一个大列,只需在注解的value属性中,以数组情势通报类似的大列名即可。
而EasyExcel发现列名是数组情势时,就会实行主动归并类似列表的字段,而导出代码无需做任何改造:
- @Override
- public void exportMultiLineHeadExcel(HttpServletResponse response) {
- List<MultiLineHeadExportVO> pandas = baseMapper.selectAllPandas();
- String fileName = "多行表头熊猫数据-" + System.currentTimeMillis();
- try {
- ExcelUtil.exportExcel(MultiLineHeadExportVO.class, pandas, fileName, ExcelTypeEnum.XLSX, response);
- } catch (IOException e) {
- log.error("多行表头熊猫数据,{}:{}", e, e.getMessage());
- throw new BusinessException("数据导出失败,请稍后再试!");
- }
- }
复制代码 |