官网:https://www.yuque.com/easyexcel/doc/easyexcel

导出

准备工作

引入依赖

<!--EasyExcel相关依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>

创建监听器

@Data
public class ExcelDataListener<T> extends AnalysisEventListener<T> {
private static Logger log4 = Logger.getLogger(AreaServiceImpl.class); /**
* 导入出错的信息(此处保存的为解析时错误的信息,某行某列+错误信息+;)
*/
private List<String> errorMsg = new ArrayList<>(); /**
* 必要列为空的信息(某行+'列1,列2...'+的值为空)
*/
private List<String> nullFields = new ArrayList<>(); /**
* 导入的表头(解析到的表头)
*/
private Map<Integer, String> headMap = new HashMap<>(); /**
* 缓存的数据(成功解析的数据)
*/
private List<T> dataList = new ArrayList<>(); /**
* 读取报错的数据(解析错误的数据,即无法映射到导出的实体类)
*/
private List<Map<String, String>> errorDataList = ListUtils.newArrayList(); /** 导出实体类的行索引字段名。
*用于设置解析到的数据所在的行数(在进行导入的时候用于定位service中save出错的数
*据,所在的行)
*/
private String rowIndexField; /**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
// private DemoDAO demoDAO; // public ExcelDataListener(DemoDAO demoDAO) {
// this.demoDAO = demoDAO;
// } /**
* 这个每一条数据解析都会来调用
*
* @param data one row value
* @param context
*/
@Override
public void invoke(T data, AnalysisContext context) {
log4.info("解析到一条数据:" + JSON.toJSONString(data));
// 由于会读取空行(excel中删除数据后的行)所以这里加判断
if (!"{}".equals(JSON.toJSONString(data))) {
// 由于第一行是表头,因此这里+1
int rowIndex = context.readRowHolder().getRowIndex() + 1;
Class<?> dataClazz = data.getClass();
try {
// 如果没有设置实体类行索引的字段名,则默认为rowIndex
String fieldName = StringUtils.isEmpty(rowIndexField) ? "rowIndex" : rowIndexField;
Field rowIndexField = dataClazz.getDeclaredField(fieldName);
rowIndexField.setAccessible(true);
rowIndexField.set(data,rowIndex);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
log4.info("对应的实体中没有行数");
}
// 校验必要列是否为空
List<String> checkField = ExcelUtil.checkField(data);
if (checkField.size() > 0) {
String errorMsg = "第" + rowIndex + "行," +
String.join("、",checkField) + "列的值为空";
nullFields.add(errorMsg);
} else {
dataList.add(data);
}
}
else
context.readRowHolder().setRowType(RowTypeEnum.EMPTY);
} /**
* 读取的表头
*
* @param headMap 表头
* @param context
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 此处由于会读取空单元格表头(删除内容后的单元格也会被读取)所以进行遍历判断
Set<Map.Entry<Integer, String>> entries = headMap.entrySet();
for (Map.Entry<Integer, String> head : entries) {
if (!StringUtils.isEmpty(head.getValue()))
this.headMap.put(head.getKey(),head.getValue());
}
} @Override
public boolean hasNext(AnalysisContext context) {
// 由于会读取空行数据,因此此处加判断遇到空行停止读取;(invoke方法中遇到空行会设置RowType)
if (RowTypeEnum.EMPTY.equals(context.readRowHolder().getRowType())) {
doAfterAllAnalysed(context);
return false;
}
return super.hasNext(context);
} /**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
log4.info("所有数据解析完成!");
} /**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
*
* @param exception
* @param context
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) {
// 如果是某一个单元格的转换异常 能获取到具体行号
// 如果要获取头的信息 配合invokeHeadMap使用
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException e = (ExcelDataConvertException)exception;
String errorIndexInfo = "第" + (e.getRowIndex() + 1) + "行"
+ "的”" +headMap.get(e.getColumnIndex()) +"“列,";
errorMsg.add(errorIndexInfo + exception.getMessage());
// 构造错误的数据
ReadRowHolder readRowHolder = context.readRowHolder();
HashMap<String, String> data = new HashMap<>();
Set<Map.Entry<Integer, Cell>> entries = readRowHolder.getCellMap().entrySet();
for (Map.Entry<Integer, Cell> entry : entries) {
ReadCellData value = (ReadCellData)(entry.getValue());
data.put(headMap.get(entry.getKey()),value.getStringValue());
}
errorDataList.add(data);
}
} }

工具类校验必要字段

/**
* 校验导入实体的必要字段
* @param entity 导入的实体
* @param <T>
* @return
*/
public static <T> List<String> checkField(T entity){
Class<?> clazz = entity.getClass();
Field[] fields = clazz.getDeclaredFields();
List<String> nullFields = new ArrayList<>();
for (Field field : fields) {
NeedNotNull notNull = field.getDeclaredAnnotation(NeedNotNull.class);
ExcelProperty property = field.getDeclaredAnnotation(ExcelProperty.class);
field.setAccessible(true);
// 该字段是否不能为空
if (!ObjectUtils.isEmpty(notNull)){
try {
Object value = field.get(entity);
// 如果必要字段的值为空则校验失败
if (ObjectUtils.isEmpty(value))
nullFields.add(StringUtils.join(property.value(),","));
} catch (IllegalAccessException e) {
e.printStackTrace();
log4.error(field.getName() + "字段值获取异常:"+e.getMessage());
}
}
}
return nullFields;
}

自定义注解

/**
* 自定义注解,用于判断是否需要合并以及合并的主键
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge { /**
* 是否需要合并单元格
*/
boolean needMerge() default false; /**
* 是否是主键,即该字段相同的行合并
*/
boolean isPk() default false;
} /**
* 自定义注解,用于判断字段是否不能为空
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface NeedNotNull { /**
* 是否不能为空
*/
boolean value() default true;
/**
* 字段名称
*/
String name() default "";
}

创建导出的实体类

@Data
public class AreaImportDto { /** 区域名称 */
@ExcelProperty("区域名称")
@CustomMerge(needMerge = true, isPk = true)
@ColumnWidth(20)
@NeedNotNull(name = "区域名称")
private String areaName; /** 区域的X坐标 */
@ExcelProperty("父区域")
@CustomMerge(needMerge = true)
@ColumnWidth(10)
private String parentName; /** 人员名称 */
@ExcelProperty("人员名称")
@CustomMerge(needMerge = true)
@ColumnWidth(20)
private String personName; /** 导入数据所在的行数 */
private Integer rowIndex;
}

ps:

@ExcelProperty: 导出的列名(如果是多行表头,则值为数组,详见官网)

@CustomMerge(needMerge = true, isPk = true):要合并的列

@ColumnWidth(20):列宽

@NeedNotNull(name = "区域名称"):是否为必要列

构造数据

  • 调用service查询数据库数据
  • 定义转换方法,将实体类转换为导出实体(此处可以省略,如果不另外定义导出实体的话耦合性较高)

创建导出工具类

private static final String FILE_SEPARATOR = "/";

/**
* 数据导出到excel
*
* @param params 请求参数(自定义的请求参数实体,包含query(Map)、sort、page属性)
* @param serverPath 服务器文件导出路径
* @param defaultFileName 默认文件名
* @param targetData 导出数据
* @param clazz 导出数据的类对象
* @param isMergeStrategy 是否要合并行(一对多导出时用到)
* @return
* @throws IOException
*/
public static <T> Map<String, String> exportExcel(SearchParamDto params, String defaultFileName,
String serverPath, String urlPath, List<T> targetData,
Class<T> clazz, boolean isMergeStrategy) throws IOException {
HashMap<String, String> result = new HashMap<String, String>() {{
put("msg", "数据导出成功");
}};
// 请求中传递的需要导出的列
JSONArray fields = JSONArray.parseArray(params.getQuery().get("fields").toString());
// 设置文件名
String fileName1 = ObjectUtils.isEmpty(params.getQuery().get("fileName")) ?
defaultFileName :
params.getQuery().get("fileName").toString();
// String fileName = URLEncoder.encode(fileName1, "UTF-8").replaceAll("\\+", "%20") + ".xlsx";
String fileName = fileName1 + ".xlsx"; try {
// 获取当前时间
String updTm = DateUtil.getAllTime();
String filePath = serverPath + FILE_SEPARATOR + updTm + FILE_SEPARATOR + fileName;
String destDirName = serverPath + FILE_SEPARATOR + updTm;
createDir(destDirName);
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
log4.info(fileName1 + "数据导出成功!");
ExcelWriterBuilder head = EasyExcel.write(fileOutputStream).autoCloseStream(true)
.includeColumnFiledNames(fields.toJavaList(String.class))
.head(clazz);
if (isMergeStrategy) {
head.registerWriteHandler(new CustomMergeStrategy(clazz));
}
head.excelType(ExcelTypeEnum.XLSX)
.sheet("sheet1")
.doWrite(targetData);
String url = urlPath + FILE_SEPARATOR + updTm + FILE_SEPARATOR + fileName;
// 返回请求文件的url以及服务器报错路径
result.put("url", url);
result.put("savePath", serverPath + FILE_SEPARATOR + updTm);
} catch (Exception e) {
log4.error("数据导出失败: {}", e);
result.put("msg", "数据导出失败");
return result;
} return result;
}

EasyExcel实现文件导出的更多相关文章

  1. SpringBoot基于EasyExcel解析Excel实现文件导出导入、读取写入

    1. 简介   Java解析.生成Excel比较有名的框架有Apache poi.jxl.但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题 ...

  2. 使用Layui、Axios、Springboot(Java) 实现EasyExcel的导入导出(浏览器下载)

    实现EasyExcel的导入导出(浏览器下载) 实现三个按钮的功能,但是却花费了一天的时间包括总结. 使用到的技术:springboot layui axios EasyExcel mybatis-p ...

  3. 使用VUE+SpringBoot+EasyExcel 整合导入导出数据

    使用VUE+SpringBoot+EasyExcel 整合导入导出数据 创建一个普通的maven项目即可 项目目录结构 1 前端 存放在resources/static 下 index.html &l ...

  4. C#+OpenGL+FreeType显示3D文字(1) - 从TTF文件导出字形贴图

    C#+OpenGL+FreeType显示3D文字(1) - 从TTF文件导出字形贴图 +BIT祝威+悄悄在此留下版了个权的信息说: 最近需要用OpenGL绘制文字,这是个很费时费力的事.一般的思路就是 ...

  5. loadrunner实现excel文件导出操作

    项目中需要对“商品信息”进行查询及导出,但是loadrunner并不能录制到“保存”这一操作. 项目介绍:flex+Http协议: 不能录制的原因: 在我们点击了“导出”按钮后,服务端已经生成一份我们 ...

  6. C#进行Visio二次开发之文件导出及另存Web页面

    在我前面很多关于Visio的开发过程中,介绍了各种Visio的C#开发应用场景,包括对Visio的文档.模具文档.形状.属性数据.各种事件等相关的基础处理,以及Visio本身的整体项目应用,虽然时间过 ...

  7. Ado.Net小练习01(数据库文件导出,导入)

    数据库文件导出主要程序: <span style="font-family: Arial, Helvetica, sans-serif;"><span style ...

  8. JAVA实用案例之文件导出(JasperReport踩坑实录)

    写在最前面 想想来新公司也快五个月了,恍惚一瞬间. 翻了翻博客,因为太忙,也有将近五个多月没认真总结过了. 正好趁着今天老婆出门团建的机会,记录下最近这段时间遇到的大坑-JasperReport. 六 ...

  9. 文件导出也可以这么写【js+blob】

    文件导出在软件开发中是个比较常用的功能,基本原理也很简单: 浏览器向后台发送一个Get请求 后台处理程序接收到请求后,经过处理,返回二进制文件流 浏览器接收到二进制文件流后提示下载文件 调用的js方法 ...

随机推荐

  1. day02 Java_变量

    参考: 变量的练习: 声明一个变量,一次声明多个变量. 声明变量直接初始化,先声明变量,而后再给变量初始化. 声明整型变量g,声明另一个整型变量h并赋值为h+10,输出变量h的值. 声明整型变量i,在 ...

  2. Airbnb的动态kubernetes集群扩缩容

    Airbnb的动态kubernetes集群扩缩容 本文介绍了Airbnb的集群扩缩容的演化历史,以及当前是如何通过Cluster Autoscaler 实现自定义扩展器的.最重要的经验就是Airbnb ...

  3. 基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理

    在早期的随笔就介绍过,把常规页面的内容拆分为几个不同的组件,如普通的页面,包括列表查询.详细资料查看.新增资料.编辑资料.导入资料等页面场景,这些内容相对比较独立,而有一定的代码量,本篇随笔介绍基于V ...

  4. Sentry的安装、配置、使用

    前言 上一篇文章介绍了ExceptionLess这个日志收集系统:ExceptionLess的安装.配置.使用 由于ExceptionLess官方提供的客户端只有.Net/.NetCore平台和js的 ...

  5. 二重循环中的break与continue

    二重循环中的break和continue用于控制本层循环,注意其不会影响外层循环的执行,这边和C中的break和continue的作用范围是一样的,也符合逻辑 example: 点击查看代码 for ...

  6. 使用try_catch_finally处理流中的异常和JDK7流中的异常处理

    在jdk1.7之前使用try_catch_finally处理流中的异常 格式: try{ 可能会出现异常的代码 }catch(异常类变量 变量名){ 异常的处理逻辑 }finally{ 一定会执行的代 ...

  7. Nginx Rewrite资源重定向

    # Rewrite功能配置 # Rewrite功能主要是实现了url重写 # 如:你输入www.jd123.com,你可以通过Rewrite让它重定向到www.jd.com # Rewrite的实现依 ...

  8. v-model原理问题

    v-model的原理 很多同学在理解Vue的时候都把Vue的数据响应原理理解为双向绑定,但实际上这是不准确的,我们之前提到的数据响应,都是通过数据的改变去驱动DOM重新的变化,而双向绑定已有数据驱动D ...

  9. DP 优化方法合集

    0. 前言 写完这篇文章后发现自己对于 DP 的优化一窍不通,所以补了补 DP 的一些优化,写篇 blog 总结一下. 1. 单调队列/单调栈优化 1.2 算法介绍 这应该算是最基础的 DP 优化方法 ...

  10. springboot整合xxl-job分布式定时任务【图文完整版】

    一.前言 定时任务有很多种,有一些大的框架也有一些简单的实现. 比如常见的: JDK的Timer和TimerTask Quartz异步任务调度框架 分布式定时任务XXL-JOB Spring Task ...