一、依赖和Excel相关操作的类
使用的依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.4</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
使用的Excel模型类
@Data
@ColumnWidth(value = 20)
@HeadRowHeight(value = 30)
@ContentRowHeight(value = 25)
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER, verticalAlignment = VerticalAlignmentEnum.CENTER)
@HeadStyle(fillForegroundColor = 22)
@AllArgsConstructor
@NoArgsConstructor
public class Model {
@ExcelProperty("开始时间")
@DateTimeFormat("yyyy年MM月dd日 hh时mm分ss秒")
private LocalDateTime a1;
@ExcelProperty("结束日期")
private LocalDate a2;
@ExcelProperty("编号")
private String a3;
@ExcelProperty("数量")
private Integer a4;
}
封装好的导入和导出的方法,可以修改里面的file,集成springMvc实现web端的相关导入导出功能,里面相关的类会放在对应的测试那里
@Slf4j
@RequiredArgsConstructor
public class ExcelService {
private final SortRowWriteHandler sortRowWriteHandler;
private final LocalDateConverter localDateConverter;
public <T, R> List<R> fileConvertBean(File file, Class<T> template, Function<T, R> convert) {
TemplateImportEventListener<T, R> excelDataListener = new TemplateImportEventListener<>(convert);
parseExcel(file, template, 0, excelDataListener);
return excelDataListener.getResult();
}
public <T, R> List<R> fileConvertBean(File file, Class<T> template, CustomImportReq req, Function<T, R> convert) {
if (req == null || CollectionUtil.isEmpty(req.getFieldColumns())) {
return fileConvertBean(file, template, convert);
}
CustomImportEventListener<T, R> eventListener = new CustomImportEventListener<>(req.getFieldColumns(), template, convert);
parseExcel(file, null, req.getSheetNo(), eventListener);
return eventListener.getResult();
}
public void exportExcel(String exportName, Class<?> templateClass, List<?> data) {
exportExcel(exportName, templateClass, exportName, data, null);
}
public void exportExcel(ExportTemplate exportTemplate) {
exportExcel(exportTemplate.exportName(), exportTemplate.templateClass(), exportTemplate.sheetName(), Collections.EMPTY_LIST, null);
}
public void exportExcel(String exportName, Class<?> templateClass, String sheetName, List<?> data, List<String> includeColumnFiledNames) {
try {
ExcelWriterBuilder writerBuilder = EasyExcel.write(new FileOutputStream("D:\\test\\" + exportName + ".xlsx"), templateClass).registerConverter(localDateConverter);
if (CollUtil.isNotEmpty(includeColumnFiledNames)) {
writerBuilder.includeColumnFieldNames(includeColumnFiledNames).registerWriteHandler(sortRowWriteHandler);
}
writerBuilder.sheet(sheetName).doWrite(data);
} catch (IOException e) {
log.error("exportExcel ioException", e);
throw new ExcelException("解析excel文件出错");
}
}
private void parseExcel(File file, Class<?> template, Integer sheetNo, AnalysisEventListener<?> excelDataListener) {
try {
InputStream inputStream = new FileInputStream(file);
EasyExcel.read(inputStream, template, excelDataListener).sheet(sheetNo).registerConverter(localDateConverter).doRead();
} catch (IOException e) {
log.error("parseExcel error", e);
throw new ExcelException("解析excel文件出错");
} catch (ExcelAnalysisException excelAnalysisException) {
throw (ExcelException) excelAnalysisException.getCause();
}
}
}
LocalDate的转换器:
public class LocalDateConverter implements Converter<LocalDate> {
private static final DateTimeFormatter DEFAULT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public Class<LocalDate> supportJavaTypeKey() {
return LocalDate.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public LocalDate convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return LocalDate.parse(cellData.getStringValue(), getDateTimeFormat(contentProperty));
}
@Override
public WriteCellData<?> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return new WriteCellData<>(value.format(getDateTimeFormat(contentProperty)));
}
private DateTimeFormatter getDateTimeFormat(ExcelContentProperty contentProperty) {
DateTimeFormatProperty dateTimeFormatProperty = contentProperty.getDateTimeFormatProperty();
String format = dateTimeFormatProperty == null ? null : dateTimeFormatProperty.getFormat();
return StrUtil.isBlank(format) ? DEFAULT_FORMAT : DateTimeFormatter.ofPattern(format);
}
}
自定义异常,用来处理一些解析出的错误,来统一抛出
public class ExcelException extends RuntimeException {
public ExcelException(String messageTemplate, Object... params) {
super(StrUtil.format(messageTemplate, params));
}
}
public class ExcelExceptionAssert {
public static void notNUll(Object target, String messageTemplate, Object... params) {
isFalse(target == null, messageTemplate, params);
}
public static void isFalse(boolean expression, String messageTemplate, Object... params) {
if (expression) {
throw new ExcelException(messageTemplate, params);
}
}
}
二、Excel模板下载
针对需要通过excel进行批量导入数据,给用户提供一个导入模板,让用户按照模板来填写数据后进行导入。 测试代码:
private ExcelService excelService;
private ExcelImportLogService excelImportLogService;
@BeforeEach
void setUp() {
List<String> loggers = Arrays.asList(Logger.ROOT_LOGGER_NAME);
for (String log : loggers) {
Logger logger = (Logger) LoggerFactory.getLogger(log);
logger.setLevel(Level.INFO);
}
excelService = new ExcelService(new SortRowWriteHandler(), new LocalDateConverter());
excelImportLogService = new ExcelImportLogService(excelService);
}
@Test
void templateDownload() {
excelService.exportExcel(new ImportExcelTemplate());
}
导出效果: 实现代码:
public interface ExportTemplate {
String exportName();
default String sheetName() {
return exportName();
}
Class<?> templateClass();
}
public class ImportExcelTemplate implements ExportTemplate {
@Override
public String exportName() {
return "测试导入模板下载";
}
@Override
public Class<?> templateClass() {
return Model.class;
}
}
三、导出数据到excel
一种是满足按照固定的列导出excel,一种可以通过控制列和列顺序来导出excel(结合前端传入排序好的列和指定的列即可自定义导出)
@Test
void dataExport() {
List<Model> models = Arrays.asList(new Model(LocalDateTime.now(), LocalDate.now(), "test", 1));
excelService.exportExcel("测试导出excel", Model.class, models);
excelService.exportExcel("测试排序导出", Model.class, "测试排序导出", models,
Arrays.asList("a2", "a1"));
}
实现效果: 排序导出的实现:通过注入easyexcel提供的handle来改变列的映射实现排序,包含列是easyexcel已经提供的功能
public class SortRowWriteHandler implements RowWriteHandler {
@Override
public void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer rowIndex, Integer relativeRowIndex, Boolean isHead) {
if (isHead) {
ExcelWriteHeadProperty excelWriteHeadProperty = writeSheetHolder.getExcelWriteHeadProperty();
Map<Integer, Head> headMap = excelWriteHeadProperty.getHeadMap();
Collection<String> includeColumnFieldNames = writeSheetHolder.getIncludeColumnFieldNames();
Map<String, Head> fieldNameHead = headMap.values().stream().collect(Collectors.toMap(Head::getFieldName, head -> head));
int index = 0;
for (String includeColumnFieldName : includeColumnFieldNames) {
Head head = fieldNameHead.get(includeColumnFieldName);
if (head == null) {
continue;
}
headMap.put(index++, head);
}
}
}
}
四、模板导入和自定义导入
@Test
void dataImport() {
List<Model> models = excelService.fileConvertBean(new File("D:\\test\\测试导出excel.xlsx"), Model.class, model -> model);
System.out.println(models);
CustomImportReq customImportReq = new CustomImportReq();
customImportReq.setSheetNo(0);
List<FieldColumnReq> fieldColumnReqs = new ArrayList<>();
FieldColumnReq req1 = new FieldColumnReq("a1", 0, "标题1");
FieldColumnReq req2 = new FieldColumnReq("a2", -1, "结束日期");
fieldColumnReqs.add(req1);
fieldColumnReqs.add(req2);
customImportReq.setFieldColumns(fieldColumnReqs);
List<Model> models1 = excelService.fileConvertBean(new File("D:\\test\\自定义导入.xlsx"), Model.class, customImportReq, model -> model);
System.out.println(models1);
excelImportLogService.fileConvertBean(new File("D:\\test\\测试导出excel.xlsx"), Model.class, model -> {
ExcelExceptionAssert.notNUll(model.getA4(), "数量不能为空");
return model;
});
}
实现效果: 模板导入的excel(需要严格匹配模板类) 自定义导入的excel(对名字没有要求): 执行结果:
4.1模板导入实现
这些功能的实现都为通过不同的监听器,来执行不同的数据解析操作
@Slf4j
public class TemplateImportEventListener<T, R> extends AnalysisEventListener<T> {
private final List<R> result = new ArrayList<>();
private final Function<T, R> convert;
private boolean hasData;
@Override
public void invoke(T data, AnalysisContext context) {
int maxSize = 10000;
if (result.size() >= maxSize) {
throw new ExcelException("超过最大导入数量10000");
}
R result = convert.apply(data);
if (result != null) {
this.result.add(result);
}
hasData = true;
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (CollectionUtil.isEmpty(result) && !hasData) {
throw new ExcelException("导入数据不能为空");
}
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
Map<Integer, Head> readHeadMap = context.readSheetHolder().excelReadHeadProperty().getHeadMap();
if (readHeadMap.size() != headMap.size()) {
throw new ExcelException("导入文件与模板不一致");
}
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
if (exception instanceof ExcelException) {
throw exception;
}
log.error("解析数据异常", exception);
throw new ExcelException("解析excel异常");
}
public List<R> getResult() {
return result;
}
public TemplateImportEventListener(Function<T, R> convert) {
this.convert = convert;
}
}
4.2自定义导入实现
思路:前端通过解析excel的头,做成下来选择,传给后端对应的索引,然后后端根据对应的索引去映射表格中的数据,自动查找则是根据提供的模板类上定义的名字来循环去判断,做一个模糊映射的操作.
public class CustomImportReq {
protected Integer sheetNo;
protected List<FieldColumnReq> fieldColumns;
}
public class FieldColumnReq {
private String fieldName;
private Integer column;
private String columnHead;
}
实现映射的监听器:
@Slf4j
public class CustomImportEventListener<T, R> extends AnalysisEventListener<Map<Integer, String>> {
private final List<R> result;
private final Class<T> templateClass;
private final Function<T, R> convert;
private final Map<String, FieldColumnReq> fieldExcelColumnMap;
private final Map<String, String> excelColumnFieldMap;
private boolean hasData;
private Map<String, String> fieldChineseMap;
private final int AUTOMATIC_SEARCH_COLUMN = -1;
@Override
public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) {
int maxSize = 10000;
if (result.size() >= maxSize) {
throw new ExcelException("超过最大导入数量10000");
}
Map<String, String> strDataMap = new HashMap<>();
data.forEach((key, value) -> strDataMap.put(key.toString(), value));
T bean = BeanUtil.toBean(strDataMap, templateClass, CopyOptions.create().setFieldMapping(excelColumnFieldMap));
R result = convert.apply(bean);
if (result != null) {
this.result.add(result);
}
hasData = true;
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if (CollectionUtil.isEmpty(result) && !hasData) {
throw new ExcelException("导入数据不能为空");
}
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
Set<Map.Entry<Integer, String>> headSet = headMap.entrySet();
for (Map.Entry<String, FieldColumnReq> entry : fieldExcelColumnMap.entrySet()) {
String field = entry.getKey();
FieldColumnReq fieldExcelColumn = entry.getValue();
Integer column = fieldExcelColumn.getColumn();
if (column == AUTOMATIC_SEARCH_COLUMN) {
String chineseFiled = fieldChineseMap.get(field);
for (Map.Entry<Integer, String> headEntry : headSet) {
String title = headEntry.getValue();
if (title.contains(chineseFiled)) {
column = headEntry.getKey();
break;
}
}
if (column == AUTOMATIC_SEARCH_COLUMN) {
throw new ExcelException("{}无法自动查找", chineseFiled);
}
}
String oldField = excelColumnFieldMap.putIfAbsent(column.toString(), field);
if (oldField != null) {
FieldColumnReq oldFieldColumn = fieldExcelColumnMap.get(oldField);
throw new ExcelException("第{}列重复对应{}和{}", column + 1, oldFieldColumn.getColumnHead(),
fieldExcelColumn.getColumnHead());
}
}
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
if (exception instanceof ExcelException) {
throw exception;
}
log.error("解析异常", exception);
throw new ExcelException("数据解析异常,请检查");
}
public List<R> getResult() {
return result;
}
public CustomImportEventListener(List<FieldColumnReq> fieldExcelColumns, Class<T> templateClass, Function<T, R> convert) {
this.fieldExcelColumnMap = new HashMap<>();
for (FieldColumnReq fieldExcelColumn : fieldExcelColumns) {
Integer column = fieldExcelColumn.getColumn();
this.fieldExcelColumnMap.put(fieldExcelColumn.getFieldName(), fieldExcelColumn);
if (this.fieldChineseMap == null && column == AUTOMATIC_SEARCH_COLUMN) {
initFieldChineseMap(templateClass);
}
}
this.templateClass = templateClass;
this.result = new ArrayList<>();
this.excelColumnFieldMap = new HashMap<>();
this.convert = convert;
}
private void initFieldChineseMap(Class<?> excelTemplateClass) {
this.fieldChineseMap = new HashMap<>();
for (Field field : excelTemplateClass.getDeclaredFields()) {
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty != null) {
String[] value = excelProperty.value();
if (StrUtil.isNotBlank(value[0])) {
String formatStr = value[0];
int asteriskIndex = formatStr.indexOf("*");
int leftBracketIndex = formatStr.indexOf("(");
int substringStartIndex = 0;
int substringEndIndex = formatStr.length();
if (asteriskIndex != -1) {
substringStartIndex = asteriskIndex + 1;
}
if (leftBracketIndex != -1) {
substringEndIndex = leftBracketIndex;
}
fieldChineseMap.put(field.getName(), formatStr.substring(substringStartIndex, substringEndIndex));
}
}
}
}
}
4.3扩展导入(这里演示实现记录日志)
业务中可能会有需要对数据进行校验,记录导入日志等一些操作,可以通过扩展ExcelService的方法来进行实现,ExcelService主要只负责将excel数据解析成java对应的模型
@RequiredArgsConstructor
public class ExcelImportLogService {
private final ExcelService excelService;
public <T, R> void fileConvertBean(File file, Class<T> template, Function<T, R> convert, CustomImportReq customImportReq) {
AtomicInteger successCount = new AtomicInteger();
AtomicInteger errorCount = new AtomicInteger();
Function<T, R> logRecordFunction = templateData -> {
R result = null;
try {
result = convert.apply(templateData);
successCount.incrementAndGet();
} catch (ExcelException excelException) {
System.out.println(excelException.getMessage());
errorCount.incrementAndGet();
}
return result;
};
System.out.println("=============================打印验证=================================");
List<R> result = excelService.fileConvertBean(file, template, customImportReq, logRecordFunction);
int error = errorCount.get();
int success = successCount.get();
int total = error + success;
System.out.println(StrUtil.format("error:{} success:{} total:{}", error, success, total));
System.out.println(result);
}
public <T, R> void fileConvertBean(File file, Class<T> template, Function<T, R> convert) {
fileConvertBean(file, template, convert, null);
}
}
以上功能只是切换成了本地file的形式,业务中要使用的时候可以换成mvc的MultipartFile和对应的servlet输出流
|