EasyExcel识别表头

本文将介绍使用EasyExcel实现识别单行表头的一种实现方式。主要是通过实现AnalysisEventListener 抽象类,并覆写其 hasNext 和 invokeHead 方法。

Jar依赖

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>easyexcel</artifactId>
	<version>3.3.4</version>
</dependency>

<dependency>
	<groupId>org.reflections</groupId>
	<artifactId>reflections</artifactId>
	<version>0.10.2</version>
</dependency>

实现代码

一个表头基类和2个子类,一个用于表头字段的类。

注意:代码使用反射,使用了@ExcelProperty(value = “地址”, index = 0)注解,index用于表头字段排序,value用于表头显示的名称。

1.表头基类
package top.jyokiyi.my.test.excel;
import lombok.Data;

@Data
public class ExcelBaseModel {
}
2.表头实现类1

继承表头基类:

package top.jyokiyi.my.test.excel;

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

@Data
public class ExcelHeadTest1Model extends ExcelBaseModel {

    @ExcelProperty(value = "地址", index = 0)
    private String address;

    @ExcelProperty(value = "名称", index = 1)
    private String name;
}
3.表头实现类2

继承表头基类:

package top.jyokiyi.my.test.excel;

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

@Data
public class ExcelHeadTest2Model extends ExcelBaseModel {

    @ExcelProperty(value = "邮件",index = 0)
    private String email;

    @ExcelProperty(value = "内容", index = 1)
    private String content;
}
4. 表头字段类
package top.jyokiyi.my.test.excel;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ExcelHeadFieldModel {
    /**
     * 列号
     */
    private Integer column;
    /**
     * 表头描述
     */
    private String headDesc;
}
5. 使用Reflections实现查找所有子类

注意:这里直接指定了包名,且子类和基类在同一个包中。(如有需要可自行修改,以实现自己的需求)


/**
 * 获取指定类的所有子类
 * @param clazz
 * @param <T>
 * @return
 */
public static <T>Set<Class<? extends T>> getAllSubTypes(Class<T> clazz) {
	Set<Class<? extends T>> subTypeClassSet = new HashSet<>();

	Reflections reflections = new Reflections("top.jyokiyi.my.test.excel");
	Set<Class<? extends T>> subTypesOf = reflections.getSubTypesOf(clazz);
	if (CollectionUtils.isNotEmpty(subTypesOf)) {
		subTypeClassSet.addAll(subTypesOf);
	}

	return subTypeClassSet;
}
6.表头类型与表头字段集合映射
/**
 * 将表头与对于的类映射起来
 * @param sets
 * @param <T>
 * @return
 */
public static <T>Map<Class<? extends T>, List<ExcelHeadFieldModel>> getHeadMapForExcelProperty(Set<Class<? extends T>> sets) {
	Map<Class<? extends T>, List<ExcelHeadFieldModel>> maps = new HashMap<>();

	for (Class<? extends T> clz : sets) {
		List<ExcelHeadFieldModel> fieldModelList = new ArrayList<>();
		try {
			Field[] fields = clz.getDeclaredFields();
			for (Field field : fields) {
				if (field.isAnnotationPresent(ExcelProperty.class)) {
					ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
					fieldModelList.add(ExcelHeadFieldModel.builder()
							.column(annotation.index()).headDesc(annotation.value()[0]).build());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (CollectionUtils.isNotEmpty(fieldModelList)) {
			maps.put(clz, fieldModelList.stream()
                       .sorted(Comparator.comparing(ExcelHeadFieldModel::getColumn))
                       .collect(Collectors.toList()));
		}
	}
	return maps;
}
7.实现监听类

继承easy excel 的 AnalysisEventListener 抽象类,并覆写其 hasNext 和 invokeHead 方法。

package top.jyokiyi.my.test.excel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.util.StringUtils;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class ExcelHeaderIdentifyListener extends AnalysisEventListener<Map<Integer, String>> {

    private static ThreadLocal<Class<? extends ExcelBaseModel>> classResult = new ThreadLocal<>();

    public ThreadLocal<Class<? extends ExcelBaseModel>> getClassResult() {
        return classResult;
    }

    public void clear() {
        classResult.remove();
    }

    /**
     * 返回false,读取一行后,不再继续读取
     *
     * @param context
     * @return
     */
    @Override
    public boolean hasNext(AnalysisContext context) {
        return false;
    }

    /**
     * 根据表头解析获取对应的表头子类
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        List<ExcelHeadFieldModel> headFieldModels = headMap.values().stream().map(column -> ExcelHeadFieldModel.builder()
                .column(column.getColumnIndex()).headDesc(column.getStringValue()).build())
                .sorted(Comparator.comparing(ExcelHeadFieldModel::getColumn))
                .collect(Collectors.toList());
        // 将表头字段拼接起来
        String joinStr = headFieldModels.stream()
                .map(ExcelHeadFieldModel::getHeadDesc)
                .collect(Collectors.joining());

        // 获取表头基类的所有子类信息
        Set<Class<? extends ExcelBaseModel>> allSubTypes = ClassUtils.getAllSubTypes(ExcelBaseModel.class);
        // 将class和表头映射起来
        Map<Class<? extends ExcelBaseModel>, List<ExcelHeadFieldModel>> listMap = ClassUtils.getHeadMapForExcelProperty(allSubTypes);
        // 查找对应的子类
        for (Map.Entry<Class<? extends ExcelBaseModel>, List<ExcelHeadFieldModel>> entry : listMap.entrySet()) {
            if (entry.getValue().size() == headFieldModels.size() && StringUtils.equals(joinStr, entry.getValue()
                    .stream().map(ExcelHeadFieldModel::getHeadDesc).collect(Collectors.joining()))) {
                classResult.set(entry.getKey());
                break;
            }
        }
    }
    

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {

    }


    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {

    }
}
8.测试
package top.jyokiyi.my.test.excel;

import com.alibaba.excel.EasyExcel;

import java.io.File;
import java.io.FileInputStream;

public class ReadTest {


    public static void main(String[] args) throws Exception {
        ExcelHeaderIdentifyListener listener = new ExcelHeaderIdentifyListener();
        String dir = System.getProperty("user.dir");
        String filePath = dir + File.separator + "src" + File.separator + "test_excel.xlsx";

        EasyExcel.read(new FileInputStream(filePath), listener)
                 .sheet().doRead();

        Class<? extends ExcelBaseModel> aClass = listener.getClassResult();
        listener.clear();
        System.out.println(aClass.getName());
    }
}

测试结果:

top.jyokiyi.my.test.excel.ExcelHeadTest1Model

测试文件的表头如下:

地址名称
12

一点点之LocalDate

问题或建议,请公众号留言;
最新更新日期:2022-01-09

ISO 8601日期和时间格式

ISO 8601是国际通用的明确日期和时间格式的规范。该ISO标准有助于消除影响全球运营的各种日期约定、文化和时区可能导致的疑虑。它提供了一种显示日期和时间的方式,这种方式对人和机器来说都是明确定义和理解的。

LocalDate

LocalDateISO-8601日历系统中没有时区的日期。LocalDate是一个不可变的日期时间对象(线程安全的对象),它表示一个日期,通常展示视为【年-月-日】。
也可以访问其他的日期字段包含以下:

  • day-of-year

  • day-of-week

  • week-of-year

此类仅是对日期的描述,可用于生日,假期等。由于无关偏移量和时区,它就不能代表时间线上的一个瞬间。

比较相等是使用equals方法。
默认的格式是uuuu-MM-dd

LocalDate方法列表

方法名称 描述
now,of,from,parse,ofYearDay,ofEpochDay 这些静态方法根据指定条件生成一个LocalDate对象
plusDays,plusMonths,plusWeeks,plusYears 在当前日期上增加相应的天数,周数,月数,年数
minusDays,minusWeeks,minusMonths,minusYears 在当前日期上减少相应的天数,周数,月数,年数
plus,minus 在当前日期上 增加/减少 一个 DurationPeriod,或者ChronoUnit单位指定的值
withDayOfMonth,withDayOfYear,withMonth,withYear 以当前日期,修改为指定的日期,返回新的LocalDate对象
getDayOfMonth,getDayOfWeek,getDayOfYear 获取当前日期在月份中的第几天,周中的第几天,年中的第几天
getMonth,getMonthValue 获取当前日期所在的月份,前一个是枚举Month,后面的值是1-12
getYear 获取当前日期的年份,范围:-999999999+999999999
isAfter,isBefore 当前日期是否在指定日期之后或之前
isLeapYear 当前日期所在年份是否是闰年
lengthOfMonth,lengthOfYear 获取当前日期所在月份的天数,所在年份的天数
range 获取当前日期,指定ChronoField后的范围
until 获取2个日期之前的Period对象,或者ChronoUnit对应的数字
adjustInto,with 根据指定的条件返回调整后,新的日期.2个方法是等价的
atStartOfDay,atStartOfDay,atTime 获取带有时间的LocalDateTime

方法返回的LocalDateLocalDateTime都是新的对象,

与Calendar的不同

  1. 星期周的不同:Calendar中周日是1,周六是7。LocalDate默认的周一是1,周日是7。
  2. 月份的不同:Calendar是从0-11表示的。LocalDate中是从1-12表示的。

应用示例

LocalDate date = LocalDate.now();
System.out.println("date="+date);
System.out.println("是否为闰年:"+date.isLeapYear());
System.out.println("年中第几天:"+date.getDayOfYear());
System.out.println("月中第几天:"+date.getDayOfMonth());
System.out.println("周几:"+date.getDayOfWeek());
System.out.println("月份:"+date.getMonth());
System.out.println("月份值:"+date.getMonthValue());
System.out.println("加上3天:"+date.plusDays(3));
System.out.println("加上3天:"+date.plus(3, ChronoUnit.DAYS));
System.out.println("日期修改为1月30号:"+date.withDayOfMonth(30));
System.out.println("日期修改为3月份:"+date.withMonth(3));
System.out.println("日期修改为本年份的第50天:"+date.withDayOfYear(50));
System.out.println("当前月份的天数:"+date.lengthOfMonth());
System.out.println("当前年份的天数:"+date.lengthOfYear());
System.out.println("一周的范围值:"+date.range(ChronoField.DAY_OF_WEEK));
System.out.println("当前月天数的范围值:"+date.range(ChronoField.DAY_OF_MONTH));
System.out.println("当前年天数的范围值:"+date.range(ChronoField.DAY_OF_YEAR));
System.out.println("2个日期相差天数1:"+date.until(LocalDate.parse("2022-01-15"),ChronoUnit.DAYS));
System.out.println("2个日期相差天数2:"+ChronoUnit.DAYS.between(date,LocalDate.parse("2022-01-15")));
System.out.println("2个日期相差Period1:"+date.until(LocalDate.parse("2022-01-15")));
System.out.println("2个日期相差Period2:"+ Period.between(date,LocalDate.parse("2022-01-15")));

System.out.println("日期调整1:"+ date.with(temporal -> temporal.minus(Period.ofDays(2))));
System.out.println("日期调整2:"+ date.with(temporal -> temporal.minus(2, ChronoUnit.DAYS)));

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("当前日期时间:"+localDateTime);
System.out.println("日期调整3:"+ localDateTime.with(temporal -> temporal.minus(Duration.ofHours(2))));
System.out.println("日期调整4:"+ localDateTime.with(temporal -> temporal.minus(2, ChronoUnit.HOURS)));

运行结果:

date=2022-01-09
是否为闰年:false
年中第几天:9
月中第几天:9
周几:SUNDAY
月份:JANUARY
月份值:1
加上3天:2022-01-12
加上3天:2022-01-12
日期修改为1月30号:2022-01-30
日期修改为3月份:2022-03-09
日期修改为本年份的第50天:2022-02-19
当前月份的天数:31
当前年份的天数:365
一周的范围值:1 - 7
当前月天数的范围值:1 - 31
当前年天数的范围值:1 - 365
2个日期相差天数1:6
2个日期相差天数2:6
2个日期相差Period1:P6D
2个日期相差Period2:P6D
日期调整1:2022-01-07
日期调整2:2022-01-07
当前日期时间:2022-01-09T13:13:36.303
日期调整3:2022-01-09T11:13:36.303
日期调整2:2022-01-09T11:13:36.303

注意:Duration是基于时间的时间量,包含hours-mintues-seconds,也可以使用ChronoUnit.DAYS 单位等效于24小时 。

Period是基于日期的时间量,包含years-months-days.  ChronoUnit.YEARS / ChronoUnit.MONTHS  /  ChronoUnit.DAYS


结束。

来自博主的微信公众号。