前言
我们在使用EasyExcel做导入功能时,通过在实体或者VO加@ExcelProperty(“测试”)注解来实现列的一一对应,read()之后获取一个List<T>的结果集,得到java的数据类型然后在进行业务操作
但EasyExcel这种通过注解的方式实现导入就必须使用事先定义好数据类型来接收,它不支持对象中的子对象,也不能导入excel带有动态列的数据(即相同列名下的多个数据列)
换做java的数据结构即如下结构:
这里使用原生的EasyExcel并不能接收到多个列名为 测试1 的数据
这里提供一种解决方式,它能够实现动态列的解析,实例化对象中的子对象这种数据结构(仅支持二级的嵌套,即一对多的这种关系)
一、代码
/* 注解用来支持解析构建子对象 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelDynamic {
}
/* 解析导入数据,存储解析到的动态列数据(其中用到一些工具类大家可以自己实现) */
public class DynamicReadListener extends AnalysisEventListener<Map<Integer, ReadCellData<?>>> {
/**
* log
*/
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 表头数据(存储所有的表头数据)
*/
private final List<Map<Integer, String>> headList = new ArrayList<>();
/**
* 数据体
*/
private final List<Map<Integer, String>> dataList = new ArrayList<>();
/**
* 动态列 索引
*/
private final List<List<Integer>> dynamicIndex = new ArrayList<>();
/**
* 动态列数据
*/
private final List<Map<String, List<Object>>> dynamicColumns = new ArrayList<>();
/**
* 实例化 Listener
* @return Listener
*/
public static DynamicReadListener init(){
return new DynamicReadListener();
}
/**
* 这里会一行行的返回头
* @param headMap 表头
* @param context 上线文
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
log.info("解析到表头:{}", JsonUtil.toJson(headMap));
//保存表头
headList.add(headMap);
//保存动态列索引
//CollectUtil.repeatNode(headMap) 获取map中重复value的keys
dynamicIndex.add(CollectUtil.repeatNode(headMap));
}
@Override
public void invoke(Map<Integer, ReadCellData<?>> data, AnalysisContext context) {
/*
* 保存数据行
* 这里因为接收类型是 ReadCellData 就未处理
* ReadCellData 有getStringValue 可供获取cell值
*/
//dataList.add(data);
ReadSheetHolder holder = context.readSheetHolder();
try {
handleDynamicColumns(data, holder.getHeadRowNumber() - 1, holder.getRowIndex() - 1, headList.size());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 后置处理
* @param context 上线文
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {}
/**
* 根据当前数据行,获取动态列数据
*
* @param data 当前数据
* @param dataRowIndex 数据行索引
* @param headRowIndex 数据头索引
* @param columnSize 列长度,初始化map需要
*/
private void handleDynamicColumns(Map<Integer, ReadCellData<?>> data, Integer headRowIndex, Integer dataRowIndex, Integer columnSize) throws NoSuchFieldException, IllegalAccessException {
if (!dynamicIndex.isEmpty()){
Map<String, List<Object>> columns = new HashMap<>(columnSize);
Map<Integer, String> targetHead = headList.get(headRowIndex);
for (Integer targetIndex : dynamicIndex.get(headRowIndex)) {
String head = targetHead.get(targetIndex);
String value = data.get(targetIndex).getStringValue();
if (Objects.isNull(columns.get(head))){
//不要使用Arrays.asList 因为返回的list 没有重写add方法
columns.put(head, ListUtil.asList(value));
}else {
columns.get(head).add(value);
}
}
dynamicColumns.add(columns);
}
}
public List<Map<Integer, String>> getHeadList() {
return headList;
}
public List<Map<Integer, String>> getDataList() {
return dataList;
}
public List<Map<String, List<Object>>> getDynamicColumns() {
return dynamicColumns;
}
}
/* easyExcel工具类 */
public class Excels extends EasyExcel{
private Excels() {}
/**
* 导入
* @param fileStream 文件流
* @param clazz 导入类型class
* @param <T> 导入类型
* @return List<T>
*/
public static <T> List<T> imports(InputStream fileStream, Class<T> clazz){
return read(fileStream).head(clazz).sheet().doReadSync();
}
/**
* 导入带有动态列的数据
*
* @param fileStream 文件流
* @param clazz 导入类型class
* @param <T> 导入类型
* @return List<T>
*/
public static <T> List<T> importsDynamic(InputStream fileStream, Class<T> clazz) throws IllegalAccessException, InstantiationException {
//初始化 处理动态列 readListener
DynamicReadListener dynamicReadListener = DynamicReadListener.init();
//初始化 同步读取数据 readListener
SyncReadListener syncReadListener = new SyncReadListener();
/*
* useDefaultListener = false
* 默认的 readListener 即 ModelBuildEventListener 会第一个去处理数据,导致ReadHolder中的 currentRowAnalysisResult 已转为 Model 类型,
* dynamicReadListener 调用到 invoke 时会报错, 因此需要 dynamicReadListener 排在 readListenerList 的第一位,保证能够接收到 map 类型,处理动态列
* 但我们仍需要 ModelBuildEventListener 所以我们手动注册
*/
ExcelReaderSheetBuilder sheet = read(fileStream).useDefaultListener(false).head(clazz).sheet();
sheet.registerReadListener(dynamicReadListener);
//注册 map转 model readListener
sheet.registerReadListener(new ModelBuildEventListener());
sheet.registerReadListener(syncReadListener);
sheet.doRead();
return build((List<T>) syncReadListener.getList(), dynamicReadListener);
}
/**
* 处理源数据 为其实例化其中的动态列属性
*
* @param targets 处理目标List<T> 中的动态列 为其实例化动态列属性
* @param <T> 处理数据类型
* @return 源数据
* @throws IllegalAccessException IllegalAccessException
*/
private static <T> List<T> build(List<T> targets, DynamicReadListener listener) throws IllegalAccessException, InstantiationException {
if (CollectionUtils.isNotEmpty(targets)){
for (int i = 0; i < targets.size(); i++) {
T target = targets.get(i);
// 初始化带有 @ExcelDynamic 标志的属性
for (Field targetField : target.getClass().getDeclaredFields()) {
if (Objects.nonNull(targetField.getAnnotation(ExcelDynamic.class))){
targetField.setAccessible(true);
targetField.set(target, build(listener.getDynamicColumns().get(i), targetField.getType()));
}
}
}
}
return targets;
}
/**
* 根据class、当前行动态列数据 构建动态列属性
*
* @param dynamicRow 当前行动态列数据
* @param clazz 处理类型
* @param <T> 处理类型
* @return 多列转List
*/
private static <T> T build(Map<String, List<Object>> dynamicRow, Class<T> clazz) throws InstantiationException, IllegalAccessException {
if (Objects.isNull(clazz)){
throw new NullPointerException("class not support null value");
}
//处理类型属性list
Field[] fields = clazz.getDeclaredFields();
//待处理命中动态列数据
Map<Field, List<Object>> hitColumn = new HashMap<>(fields.length);
//开始筛选命中动态列
dynamicRow.forEach((head, columns)->{
//遍历 <T> 属性
for (Field field : fields) {
//获取属性注解
ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
if (Objects.nonNull(annotation)){
//根据注解值匹配
Object[] annotationKeys = annotation.value();
for (Object annotationKey : annotationKeys) {
if (!Objects.equals("", annotationKey) && Objects.equals(head, annotationKey)){
if (CollectionUtils.isNotEmpty(columns)){
hitColumn.put(field, columns);
break;
}
}
}
}
}
});
return handle(hitColumn, clazz);
}
/**
* 处理命中的动态列
* @param hitColumn 待处理数据
* @param clazz 类型
* @param <T> T
* @return T
*/
private static <T> T handle(Map<Field, List<Object>> hitColumn, Class<T> clazz) throws InstantiationException, IllegalAccessException {
//处理返回结果
if(CollectionUtils.isNotEmpty(hitColumn.keySet())){
T target = clazz.newInstance();
hitColumn.forEach((field, columns) -> {
try {
List<Object> targetFieldList = new ArrayList<>();
for (Object column : columns) {
try {
Object targetValue = Objects.requireNonNull(getGenericClass(field)).getConstructor(String.class).newInstance((String)column);
targetFieldList.add(targetValue);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
}
}
field.setAccessible(true);
field.set(target, targetFieldList);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
return target;
}
return null;
}
/**
* 通过 Field 获取其泛型
* @param field 类属性
* @return 泛型类
*/
private static Class<?> getGenericClass(Field field){
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericType;
// 获取成员变量的泛型类型信息
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
return (Class<?>) actualTypeArgument;
}
}
return null;
}
}
二、使用
文件数据
测试
/* 测试 */
@Test
public void test1() throws FileNotFoundException, IllegalAccessException, InstantiationException {
List<A> as = Excels.importsDynamic(new FileInputStream(new File("C:\\Users\\caobinghui\\Desktop\\test.xlsx")), A.class);
as.forEach(System.out::println);
}
结果
二、结束
代码拙劣,大家可以自己去试着优化
觉得有帮助的可以点个赞,谢谢!!