# easyexcel-ie
**Repository Path**: Hawkc/easyexcel-ie
## Basic Information
- **Project Name**: easyexcel-ie
- **Description**: 基于easyexcel定制化的数据导入/导出
- **Primary Language**: Java
- **License**: GPL-3.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2023-06-10
- **Last Updated**: 2023-06-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# easyexcel-ie
基于 easyexcel 的定制化excel导入/导出的实现。
# 项目依赖
```xml
cn.com.zhaoweiping
easyexcel-ie
1.0.0-SNAPSHOT
```
# 功能实现
#### Excel导出格式
支持Excel的导入和导出;支持多种样式的导出,包括冻结窗格、单元格下拉选择、单元格注释、一个Sheet中多表头、多Sheet的导出;同时也支持导出的这些内容的导入。
#### 转换函数
在`com.gitee.alpha.ie.handler`中主要定义了Excel导出所需用的类,在`com.gitee.alpha.ie.convert`中主要定义了导入/导出中所使用的转换函数,包括头转换函数和值转换函数。
支持实体类型的header/value转换函数(`EntityHeaderConverter/EntityDataConverter`)和多表头的转换函数(`ManyHeaderConverter/ManyTableDataConverter`)。
对于常量转换函数请继承`com.gitee.alpha.ie.convert.AbstractConstantConverter`,常量主要使用在导出的Excel中进行加拉选择使用,再导入的时候同样也能进行转换成相应的类型。
#### 转换函数注册
使用`com.gitee.alpha.ie.convert.ConverterRegister`进行全局注册;支持初始化报扫描方式进行注册。
#### 支持Web上传解析
支持将easyexcel-ie导入web项目中使用,上传Excel数据到后端进行解析后导入数据,同时能返回每条数据的读取状态或每个Sheet数据的读取状态给前端,能实时的获取到数据导入执行情况。
# 使用方法
## Excel导出
### 转换器注册
```java
@PostConstruct
public void converterRegister() {
ConverterRegister.list("cn.com.zhaoweiping", Converter.class);
}
```
使用`ConverterRegister`中的方法进行转换器的注册,可进行全局注册。
转换器可以根据类型来获取得到,使用`ConverterRegister.getConverter(Class> clazz)`
### 一个Web下载数据导出的例子
抽象Controller中的导出通用实现(简单数据类型)
```java
protected void doExport(
HttpServletRequest request, HttpServletResponse response, Class> entityClass)
throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=data.xlsx");
IService iService = getIService();
Assert.isFalse(
iService == null,
String.format(
"需要导出文件的Controller没有实现 %s 的 %s 方法", this.getClass().getName(), "getIService()"));
// 若选中数据,则导出选中的数据; 若没有选中数据,则导出所有的数据
// 构建请求参数
Map context = ?; // 可从request中获取
// 设置默认分页参数信息
if (context.containsKey(QueryOps.PAGE_INDEX) == false) {
context.put(QueryOps.PAGE_INDEX, 1);
}
if (context.containsKey(QueryOps.PAGE_SIZE) == false) {
context.put(QueryOps.PAGE_SIZE, 20);
}
// QueryBuilder来自Alpha-Framework
QueryBuilder queryBuilder = new QueryBuilder(parameter.getEntity(), context);
doExport(response, queryBuilder);
}
protected void doExport(HttpServletResponse response, QueryBuilder queryBuilder)
throws Exception {
}
```
复杂数据类型的导出需要单独定制(一个复杂数据类型导出的例子)
```java
@Override
protected void doExport(HttpServletResponse response, QueryBuilder queryBuilder)
throws Exception {
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
ExcelHelper excelHelper = new ExcelHelper();
// A和B的关系为 public class A { private List bs; }
// 设置表头
List aExcelFields = excelHelper.buildExcelFields(A.class);
List bExcelFields = excelHelper.buildExcelFields(B.class);
List> aHead = excelHelper.buildHead(A.class);
List> bHead = excelHelper.buildHead(B.class);
// 设置下拉选择内容
Map aDropDownValues = excelHelper.buildDropDownValues(aExcelFields);
Map bDropDownValues = excelHelper.buildDropDownValues(bExcelFields);
// 设置注释
List vos = excelHelper.buildCommentVos(aExcelFields, 0);
if (CollectionUtil.isNotEmpty(vos)) {
vos.addAll(excelHelper.buildCommentVos(bExcelFields, 2));
} else {
vos = excelHelper.buildCommentVos(bExcelFields, 2);
}
// 转换函数
List aConverters = excelHelper.findConverters(aExcelFields);
List bConverters = excelHelper.findConverters(bExcelFields);
// body
Page page = aService.findAllPage(queryBuilder);
int index = 0;
if (page != null && CollectionUtil.isNotEmpty(page.getContent())) {
index =
write(
excelWriter,
page.getContent(),
index,
vos,
aHead,
bHead,
aDropDownValues,
bDropDownValues,
aConverters,
bConverters);
}
while (page != null && CollectionUtil.isNotEmpty(page.getContent()) && !page.isLast()) {
queryBuilder.pageIncrease();
page = getIService().findAllPage(queryBuilder);
if (page != null && CollectionUtil.isNotEmpty(page.getContent())) {
index =
write(
excelWriter,
page.getContent(),
index,
vos,
aHead,
bHead,
aDropDownValues,
bDropDownValues,
aConverters,
bConverters);
}
}
excelWriter.finish();
}
private int write(
ExcelWriter excelWriter,
List as,
int index,
List vos,
List> aHead,
List> bHead,
Map aDropDownValues,
Map bDropDownValues,
List aConverters,
List bConverters) {
if (CollectionUtil.isNotEmpty(as)) {
as =
aService.findAll(
as.stream().map(a -> a.getAId()).collect(Collectors.toList()));
for (int i = 0; i < as.size(); i++) {
A a = as.get(i);
index += i;
// 准备Sheet
WriteSheet sheet =
EasyExcel.writerSheet(index, a.getAName())
.registerWriteHandler(new FreezePaneHandler(0, 3, 0, 3))
.registerWriteHandler(new SetColumnCommentHandler(vos))
.build();
// 写入A数据到ATable
ExcelWriterTableBuilder aTableBuild =
EasyExcel.writerTable(0)
.head(aHead)
// 设置自适应列
.registerWriteHandler(new SetColumnWidthHandler())
// 设置下拉选择
.registerWriteHandler(new SpinnerWriteHandler(aDropDownValues, 1, 1))
.needHead(true);
aTableBuild = buildExcelWriterTableBuilderConverter(aTableBuild, aConverters);
WriteTable aTable = aTableBuild.build();
excelWriter.write(Arrays.asList(a), sheet, aTable);
// 写入BS到BTable
ExcelWriterTableBuilder bTableBuild =
EasyExcel.writerTable(1)
.head(bHead)
.registerWriteHandler(new SetColumnWidthHandler())
.registerWriteHandler(new SpinnerWriteHandler(bDropDownValues, 3))
.needHead(true);
bTableBuild =
buildExcelWriterTableBuilderConverter(bTableBuild, bConverters);
WriteTable bTable = bTableBuild.build();
excelWriter.write(a.getBs(), sheet, bTable);
}
}
return index;
}
```
## Excel导入
使用导出的Excel文件修改之后或者按照导出的Excel文件做为模板进行导入即可。
### 一个Web上传数据导入的例子
文件上传实现
```java
@PostMapping(value = "/data/import")
public AppResult upload(HttpServletRequest request, @RequestParam("file") MultipartFile file) {
AppResult result = AppResult.ofSuccess();
try {
// 前端使用返回的这个UUID来查询获取到数据的解析状态
String uuid = this.doUpload(request, file);
result.setData(uuid);
} catch (Exception e) {
result = AppResult.ofFail(500, e.getMessage());
}
return result;
}
protected String doUpload(HttpServletRequest request, MultipartFile multipartFile)
throws Exception {
Assert.isFalse(multipartFile.isEmpty(), "文件上传失败");
String uuid = IdUtil.fastSimpleUUID();
String originName = multipartFile.getOriginalFilename();
// 临时文件目录/使用application.yml中进行配置传入方式
File dir = new File(uploadDir);
if (dir.exists() == false) {
dir.mkdirs();
}
// 防止文件名重复/在完成数据导入之后会删除掉临时Excel文件/需要注意的是可能文件名过长的问题
File file = new File(uploadDir + uuid + originName);
multipartFile.transferTo(file);
int sheetCounts = new ExcelHelper().getSheetCounts(file);
SimpleReadContext readContext = new SimpleReadContext(sheetCounts);
readContext.setFile(file);
ReadContextHolder.set(uuid, readContext);
new Thread(() -> this.doUploading(file, readContext)).start();
return uuid;
}
```
上传解析状态的查看,前端使用setTimeout/setInterval来定时请求获取数据在前端解析response展示即可
```java
@RestController
@Api(value = "数据导入控制台监视信息")
@RequestMapping("/console")
public class DataImportConsoleMonitorInfo {
@GetMapping("/info/{uuid}")
public AppResult> info(@PathVariable(value = "uuid") String uuid) {
AppResult> result = AppResult.ofSuccess();
ReadContext readContext = ReadContextHolder.get(uuid);
if (readContext == null) {
result.setAdditional("Excel解析、数据导入完成!");
} else {
List list = readContext.read();
result.setData(list);
Status readExcelStatus = readContext.readExcelStatus();
if (readExcelStatus == Status.FINISH) {
// Excel读取完成
result.setAdditional("Excel解析、数据导入完成!");
ReadContextHolder.remove(uuid);
}
}
return result;
}
}
```
业务数据导入的实现示例(简单数据类型/JavaEntity)
```java
public void doUploading(File file, SimpleReadContext readContext) {
try {
// 获取当前类的泛型
Class clazz = ?; // 需要实现获取抽象父类字类Controller中的数据类型泛型(即实体类型)
// 默认使用实体头转换
HeaderConverter headerConverter = new EntityHeaderConverter(clazz);
// 默认使用实体值转换
CellDataConverter cellDataConverter = new EntityDataConverter(clazz, readContext);
// 读取所有的Sheet
EasyExcel.read(
file,
new ExcelReadAnalysisListener(readContext, headerConverter, cellDataConverter, this))
.doReadAll();
} catch (Exception e) {
log.error("上传失败", e);
}
}
// 默认实现在抽象Controller中用于数据导入的通用处理方法、此处能处理的导入只有单类型数据(实体)模式,若需要导入多类型(多实体)等复杂类型的数据
// 需要重写importHandler方法
@Override
protected void importHandler(
Exception e, List list, ReadContext readContext, AnalysisContext context) {
IService service = this.getIService();
Assert.isFalse(
service == null,
String.format(
"继承%s实现数据的导入/导出,%s不能为空", this.getClass().getName(), IService.class.getName()));
SimpleReadContext simpleReadContext = (SimpleReadContext) readContext;
// 默认当没有异常时处理正常数据
if (e == null && CollectionUtil.isNotEmpty(list)) {
for (RowWrapper wrapper : list) {
try {
Object data = wrapper.getRowData();
service.save((IEntity) data);
simpleReadContext.setStatus(wrapper.getRowIndex(), true, "成功");
} catch (Exception ne) {
if (ne instanceof DuplicateKeyException) {
simpleReadContext.setStatus(wrapper.getRowIndex(), true, String.format("失败:存在重复记录"));
} else {
simpleReadContext.setStatus(
wrapper.getRowIndex(), true, String.format("失败:%s", ne.getMessage()));
}
log.error("业务数据处理发生异常, 错误信息:", ne);
}
}
}
if (readContext.readStatus() == Status.FINISH) {
// 此处调用父类的clear函数清空缓存数据
// 需要注意的是在字类Controller中重写importHandler方法是不能在调用importHandler了,需要使用super.clear();
super.importHandler(e, list, readContext, context);
log.debug(
String.format(
"读取处理文件:%s,记录数:%d,失败条数:%d,错误信息:%s",
simpleReadContext.getFileName(),
simpleReadContext.getTotal(),
simpleReadContext.getFailTotal(),
simpleReadContext.getPartFails(5)));
}
}
```
业务数据导入的实现示例(复杂数据类型/多JavaEntity/Map对象/Json对象)
```java
// 此处示例中使用了两个项目中的实体类型A和B
// Excel表格一个Sheet样式为:
// 0行 A表头
// 1行 A数据
// 2行 B表头
// 3行 B数据
// 4行 B数据
// n行 ...
// m行 B数据
@Override
public void doUploading(File file, SimpleReadContext readContext) {
try {
CellDataConverter cellDataConverter =
new ManyTableDataConverter(
Sets.newHashSet(0, 2), readContext, new ConstantCellValueConverter());
ManyHeaderHelper manyHeaderHelper = ManyHeaderHelper.getInstance();
HeaderDescribe aHeaderDescribe = manyHeaderHelper.buildHeaderDescribe(A.class);
HeaderDescribe bHeaderDescribe = manyHeaderHelper.buildHeaderDescribe(B.class);
LinkedHashMap describes = Maps.newLinkedHashMap();
describes.put(0, aHeaderDescribe);
describes.put(2, bHeaderDescribe);
HeaderConverter headerConverter = new ManyHeaderConverter(describes);
EasyExcel.read(
file,
new ExcelReadAnalysisListener(readContext, headerConverter, cellDataConverter, this))
.doReadAll();
} catch (Exception e) {
log.error("上传失败", e);
}
}
@Override
protected void importHandler(
Exception e, List list, ReadContext readContext, AnalysisContext context) {
SimpleReadContext simpleReadContext = (SimpleReadContext) readContext;
if (Status.FINISH == simpleReadContext.readStatus()) {
try {
// 获取一个Sheet中的数据
List as = get(A.class);
List rowWrappers = get(B.class);
// 数据转换
Map aMap = (Map) as.get(0).getRowData();
A a = BeanUtil.mapToBean(aMap, A.class, CopyOptions.create());
List bs = Lists.newArrayList();
for (RowWrapper wrapper : rowWrappers) {
Map bMap = (Map) wrapper.getRowData();
B b = BeanUtil.mapToBean(bMap, B.class, CopyOptions.create());
bs.add(b);
}
a.setBs(bs);
String aId = a.getAId();
A oA = aService.findById(aId);
if (oA == null) {
aService.save(a, false);
} else {
aService.update(a, false);
}
// 第一个参数为行号,这是看数据导入单位是按照行还是按照sheet为单位导入,若是按照sheet为单位则默认给-1
simpleReadContext.setStatus(-1, true, String.format("数据:%s 导入成功", a.getAName()));
} catch (Exception ne) {
if (ne instanceof DuplicateKeyException) {
simpleReadContext.setStatus(
-1,
false,
String.format("数据:%s 导入失败:存在重复记录", context.readSheetHolder().getSheetName()));
} else {
simpleReadContext.setStatus(
// context.readSheetHolder().getSheetNo(), // 获取行号
-1,
false,
String.format(
"数据:%s 导入失败,错误信息:%s", context.readSheetHolder().getSheetName(), ne.getMessage()));
}
log.error("业务数据处理发生异常, 错误信息:", ne);
} finally {
super.clear();
log.debug(
String.format(
"读取处理文件:%s,记录数:%d,失败条数:%d,错误信息:%s",
simpleReadContext.getFileName(),
simpleReadContext.getTotal(),
simpleReadContext.getFailTotal(),
simpleReadContext.getPartFails(5)));
}
}
}
```
# ExcelHelper
若使用Spring Data Jpa,则可用下面的方法重写ExcelHelper中的buildDropDownValues方法来构建Excel单元格下拉值列表
```java
// // ~ Spring Data Jpa
// import com.gitee.alpha.Constant;
// import cn.com.zhaoweiping.framework.repository.support.ColumnType;
// import cn.hutool.core.util.ArrayUtil;
// import cn.hutool.core.util.EnumUtil;
// import com.google.common.collect.Maps;
// import java.util.LinkedHashMap;
// import javax.persistence.EnumType;
// import javax.persistence.Enumerated;
// @Override
// protected Map buildDropDownValues(int colIndex, Field field) {
// Map dropDownValues = Maps.newHashMap();
// Class clazz = field.getType();
// if (clazz.isEnum()) {
// LinkedHashMap enums = EnumUtil.getEnumMap(clazz);
// List list = Lists.newArrayList();
// Enumerated enumerated = field.getAnnotation(Enumerated.class);
// if (CollectionUtil.isNotEmpty(enums)) {
// enums.forEach(
// (k, v) -> {
// String val = String.valueOf(k);
// if (v instanceof Constant && enumerated != null) {
// Constant cv = (Constant) v;
// if (enumerated.value() == EnumType.STRING) {
// val = cv.getLabel();
// } else if (enumerated.value() == EnumType.ORDINAL) {
// val = val;
// }
// } // else if () // 其他枚举类型
//
// list.add(val);
// });
//
// String[] values = ArrayUtil.toArray(list, String.class);
// if (ArrayUtil.isNotEmpty(values)) {
// dropDownValues.put(colIndex, values);
// }
// }
// } else if (ColumnType.BOOLEAN.getBasicType().equals(clazz.getName())
// || ColumnType.BOOLEAN.getJavaClass().getName().equals(clazz.getName())) {
// dropDownValues.put(colIndex, new String[] {"TRUE", "FALSE"});
// }
//
// return dropDownValues;
// }
```