1、说明
EasyExcel是阿里出的一款基于Java的、快速、简洁、解决大文件内存溢出的处理Excel的开源工具,本文主要是使用这个工具对Excel导入导出进行讲解。
- 官网: https://easyexcel.opensource.alibaba.com/
- github: https://github.com/alibaba/easyexcel
2、EasyExcel的使用
2.1、创建工具类
创建工具类EasyExcelUtil,将导出和导入的相关方法写入该类中,以下详细讲述使用EasyExcel导出、导入数据。
2.1.1、EasyExcel导出
2.1.1.1、导出一个sheet-无样式
/**
* 导出excel表格
*
* @param response
* @param dataList 数据列表
* @param clazz 数据对象
* @param fileName 文件名称
* @param sheetName sheet名称
* @throws Exception
*/
public static <T> void exportExcel(HttpServletResponse response, List<T> dataList, Class<T> clazz, String fileName, String sheetName) throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
response.setHeader("Content-disposition", "attachment;filename=" + fileName + FileTypeEnum.TEMPLATE_SUFFIX.getDesc());
EasyExcelFactory.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(dataList);
}
2.1.1.2、导出一个sheet-有样式
/**
* 导出excel表格-支设置单元格样式
*
* @param response
* @param dataList 数据列表
* @param clazz 数据对象
* @param fileName 文件名称
* @param sheetName sheet名称
* @param writeHandler 处理器
* @throws Exception
*/
public static <T> void exportWriteHandlerExcel(HttpServletResponse response, List<T> dataList, Class<T> clazz, String fileName, String sheetName, WriteHandler writeHandler) throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
response.setHeader("Content-disposition", "attachment;filename=" + fileName + FileTypeEnum.TEMPLATE_SUFFIX.getDesc());
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), clazz).build();
// 这里注意如果同一个sheet只要创建一次
WriteSheet writeSheet = EasyExcel.writerSheet(sheetName)
.registerWriteHandler(writeHandler).build();
excelWriter.write(dataList, writeSheet);
excelWriter.finish();
}
2.1.1.3、导出多个sheet
/**
* 导出多个sheet
*
* @param response
* @param dataList 多个sheet数据列表
* @param clazzMap 对应每个sheet列表里面的数据对应的sheet名称
* @param fileName 文件名
* @param <T>
* @throws Exception
*/
public static <T> void exportExcels(HttpServletResponse response, List<List<?>> dataList, Map<Integer, String> clazzMap, String fileName) throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
response.setHeader("Content-disposition", "attachment;filename=" + fileName + FileTypeEnum.TEMPLATE_SUFFIX.getDesc());
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
int sheetNum = dataList.size();
for (int i = 0; i < sheetNum; i++) {
List<?> objects = dataList.get(i);
Class<?> aClass = objects.get(0).getClass();
WriteSheet writeSheet0 = EasyExcel.writerSheet(i, clazzMap.get(i)).head(aClass).build();
excelWriter.write(objects, writeSheet0);
}
excelWriter.finish();
}
2.1.1.4、导出手动填充数据的模板
此功能应用于导出同一个业务功能数据,但是每次导出的数据的表头是动态变化的,比如不同租户查看的同一个页面上展示的字段不一样,举个例子,租户A看到展示的字段是a、b、c;租户B看到展示字段是a、b。那么此时导出模板的表头字段就不一样。
<1>导出模板,传入一个处理器
/**
* 导出excel表格-列不确定情况;只导出模板【一个处理器】
*
* @param response
* @param headList 表头列表
* @param dataList 数据列表
* @param fileName 文件名称
* @param sheetName sheet名称
* @param writeHandler 处理器
* @throws Exception
*/
public static <T> void exportExcel(HttpServletResponse response, List<List<String>> headList, List<List<T>> dataList,
String fileName, String sheetName, WriteHandler writeHandler) throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
response.setHeader("Content-disposition", "attachment;filename=" + fileName + FileTypeEnum.TEMPLATE_SUFFIX.getDesc());
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
// 这里注意如果同一个sheet只要创建一次
ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet(sheetName).head(headList);
if (null != writeHandler) {
sheetBuilder.registerWriteHandler(writeHandler);
}
WriteSheet writeSheet = sheetBuilder.build();
excelWriter.write(dataList, writeSheet);
excelWriter.finish();
}
<2>导出模板,传入多个处理器
/**
* 导出excel表格-列不确定情况;只导出模板【多个处理器】
*
* @param response
* @param headList 表头列表
* @param dataList 数据列表
* @param fileName 文件名称
* @param sheetName sheet名称
* @param writeHandlers 处理器集合
* @throws Exception
*/
public static <T> void exportExcel(HttpServletResponse response, List<List<String>> headList, List<List<T>> dataList, String fileName, String sheetName, List<WriteHandler> writeHandlers) throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
response.setHeader("Content-disposition", "attachment;filename=" + fileName + FileTypeEnum.TEMPLATE_SUFFIX.getDesc());
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
// 这里注意如果同一个sheet只要创建一次
ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet(sheetName).head(headList);
if (CollUtil.isNotEmpty(writeHandlers)) {
for (WriteHandler writeHandler : writeHandlers) {
sheetBuilder.registerWriteHandler(writeHandler);
}
}
WriteSheet writeSheet = sheetBuilder.build();
excelWriter.write(dataList, writeSheet);
excelWriter.finish();
}
2.1.1.5、根据模板导出数据
填充数据的模板示例如下,注意:模板的格式需要加上字段对应的字段属性名称。样式举例:
说明:. 的{}就是根据自定义实体存值,{}里面是你自己写的字段
<1>填充列表数据
/**
* 根据模板将集合对象填充表格
*
* @param inputStream 模板文件输入流
* @param response 模板文件输出流
* @param list 填充对象集合
* @param fileName 文件名称
* @param sheetName 需要写入的sheet(不传:填充到第一个sheet)
* @throws Exception
*/
public static <T> void exportTemplateExcelList(InputStream inputStream, HttpServletResponse response, List<T> list, String fileName, String sheetName) throws Exception {
// 全部填充:全部加载到内存中一次填充
if (StringUtils.isBlank(sheetName)) {
EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).sheet().doFill(list);
} else {
EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).sheet(sheetName).doFill(list);
}
}
<2>填充列表+自定义字段属性值数据
此场景适用于模板中的一些字段不是实体中字段,而是一些特殊处理的字段,比如说展示当前文档的一个指示说明,只是特别标注在文档中某一个位置就好了,这个不是集合中的数据,所以自定义字段需要进行特殊处理,他的处理是通过下面方法的入参object。
一个sheet
/**
* 根据模板将集合对象填充表格-单个sheet
*
* @param inputStream 模板文件输入流
* @param list 填充对象集合-元素对应模板中的.{}
* @param object 填充对象-元素对应模板中的{}
* @param fileName 文件名称
* @throws Exception
*/
public static <T> void exportTemplateSheet(InputStream inputStream, HttpServletResponse response, List<T> list, Object object, String fileName) throws Exception {
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
ExcelWriter excelWriter = EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).build();
WriteSheet writeSheet0 = EasyExcelFactory.writerSheet(0).build();
// 全部填充:全部加载到内存中一次填充
excelWriter.fill(object, fillConfig, writeSheet0);
excelWriter.fill(list, fillConfig, writeSheet0);
// 可分多次填充,使用文件缓存(省内存)
// excelWriter.fill(list1, writeSheet);
// excelWriter.fill(list2, writeSheet);
excelWriter.finish();
}
两个sheet
/**
* 根据模板将集合对象填充表格-多个sheet
*
* @param inputStream 模板文件输入
* @param response 模板文件输出流
* @param list1 填充对象集合-元素对应模板中的.{}
* @param list2
* @param object1 填充对象-元素对应模板中的{}
* @param object2
* @param fileName 文件名称
* @throws Exception
*/
public static <T> void exportTemplateSheets(InputStream inputStream, HttpServletResponse response, List<T> list1, List<T> list2,
Object object1, Object object2, String fileName) throws Exception {
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
ExcelWriter excelWriter = EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).build();
WriteSheet writeSheet0 = EasyExcelFactory.writerSheet(0).build();
WriteSheet writeSheet1 = EasyExcelFactory.writerSheet(1).build();
excelWriter.fill(object1, fillConfig, writeSheet0);
excelWriter.fill(list1, fillConfig, writeSheet0);
excelWriter.fill(object2, fillConfig, writeSheet1);
excelWriter.fill(list2, fillConfig, writeSheet1);
excelWriter.finish();
}
2.1.1.6、补充导出公共方法getOutputStream
/**
* 构建输出流
*
* @param fileName 文件名称
* @param response 模板文件输出流
* @param response
* @return
* @throws Exception
*/
private static OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + FileTypeEnum.TEMPLATE_SUFFIX.getDesc());
return response.getOutputStream();
}
2.1.2、EasyExcel导入
2.1.2.1、导入一个sheet
/**
* 导入-读取一页sheet
*
* @param inputStream 文件流
* @param clazz 数据对象
* @param sheetName 要读取的sheet (不传,默认读取第一个sheet)
* @throws Exception
*/
public static <T> List<T> importExcel(InputStream inputStream, Class<T> clazz, String sheetName, ReadExcelListener readExcelListener) throws Exception {
ExcelReaderBuilder builder = EasyExcelFactory.read(inputStream, clazz, readExcelListener);
if (StringUtils.isBlank(sheetName)) {
builder.sheet().doRead();
} else {
builder.sheet(sheetName).doRead();
}
return readExcelListener.getList();
}
2.1.2.2、导入多个sheet
/**
* 导入-读取多个sheet
*
* @param inputStream 文件流
* @param sheetNum 需要读取的sheet个数(默认0开始,如果传入2,则读取0、1)
* @param sheetObjList 每个sheet里面需要封装的对象(如果index为2,则需要传入对应的2个对象)
* @param <T>
* @return
*/
public static <T> List<List<T>> importExcels(InputStream inputStream, int sheetNum, List<T> sheetObjList, ReadExcelListener<T> readExcelListener) throws Exception {
List<List<T>> resultList = new LinkedList<>();
for (int index = 0; index < sheetNum; index++) {
Class<T> tClass = (Class<T>) sheetObjList.get(index).getClass();
EasyExcelFactory.read(inputStream, tClass, readExcelListener).sheet(index).doRead();
List<T> list = readExcelListener.getList();
resultList.add(list);
}
return resultList;
}
2.2、创建导出样式相关公共类
2.2.1、设置单元格宽度
CellWidthStyleHandler
package com.wm.easyexcel.util;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.CellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName: CellWidthStyleHandler
* @Description: 设置表头的调整宽策略
* @Author: WM
* @Date: 2022/12/23 14:37
*/
public class CellWidthStyleHandler extends AbstractColumnWidthStyleStrategy {
// 可以根据这里的最大宽度,按自己需要进行调整,搭配单元格样式实现类中的,自动换行,效果更好
private static final int MAX_COLUMN_WIDTH = 50;
private Map<Integer, Map<Integer, Integer>> CACHE = new HashMap(8);
/**
* 是否设置固定宽度
*/
private boolean fixed;
/**
* 固定宽度
*/
private int fixedWidth;
public CellWidthStyleHandler() {
}
public CellWidthStyleHandler(boolean fixed, int fixedWidth) {
this.fixed = fixed;
this.fixedWidth = (fixedWidth == 0 ? 15 : fixedWidth);
}
@Override
protected void setColumnWidth(CellWriteHandlerContext context) {
boolean needSetWidth = context.getHead() || !CollUtil.isEmpty(context.getCellDataList());
if (!needSetWidth) {
return;
}
WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder();
// 设置固定宽度
if (fixed) {
Cell cell = context.getCell();
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), fixedWidth * 256);
return;
}
// 设置自动调整宽度
Map<Integer, Integer> maxColumnWidthMap = CACHE.get(writeSheetHolder.getSheetNo());
if (maxColumnWidthMap == null) {
maxColumnWidthMap = new HashMap(16);
CACHE.put(writeSheetHolder.getSheetNo(), maxColumnWidthMap);
}
Cell cell = context.getCell();
int columnWidth = this.dataLength(context.getCellDataList(), cell, context.getHead());
if (columnWidth >= 0) {
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = MAX_COLUMN_WIDTH;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
private int dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
CellData cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
2.2.2、设置默认单元格字体样式
DefaultExcelHeadHandler
package com.wm.easyexcel.util;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import org.apache.poi.ss.usermodel.IndexedColors;
/**
* @ClassName: DefaultExcelHeadHandler
* @Description: 默认自定义表头样式拦截器
* @Author: WM
* @Date: 2022/12/22 17:35
*/
public class DefaultExcelHeadHandler extends HorizontalCellStyleStrategy {
//设置头样式
@Override
protected void setHeadCellStyle(CellWriteHandlerContext context) {
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 11);
headWriteFont.setColor(IndexedColors.BLACK.getIndex());
headWriteCellStyle.setWriteFont(headWriteFont);
if (stopProcessing(context)) {
return;
}
WriteCellData<?> cellData = context.getFirstCellData();
WriteCellStyle.merge(headWriteCellStyle, cellData.getOrCreateStyle());
}
}
ExcelHeadStyles
package com.wm.easyexcel.util;
import lombok.Data;
/**
* @ClassName: ExcelHeadStyles
* @Description: excel表头颜色定义
* @Author: WM
* @Date: 2022/12/22 17:24
*/
@Data
public class ExcelHeadStyles {
/**
* 表头横坐标 - 行
*/
private Integer rowIndex;
/**
* 表头纵坐标 - 列
*/
private Integer columnIndex;
/**
* 内置颜色
*/
private Short indexColor;
/**
* 字体颜色
*/
private Short fontColor;
public ExcelHeadStyles(Integer columnIndex, Short fontColor) {
this.columnIndex = columnIndex;
this.fontColor = fontColor;
}
public ExcelHeadStyles(Integer rowIndex, Integer columnIndex, Short fontColor) {
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
this.fontColor = fontColor;
}
}
2.3、创建导入公共监听器
ReadExcelListener
package com.wm.easyexcel.util;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @ClassName: ReadExcelListener
* @Description: 读取excel文件数据监听器
* @Author: WM
* @Date: 2022/12/21 17:00
*/
@Slf4j
public abstract class ReadExcelListener<T> extends AnalysisEventListener<T> {
private static final int BATCH_COUNT = 10000;
/**
* 数据集
*/
private final List<T> list = new ArrayList<>();
/**
* 表头数据
*/
private Map<Integer, String> headMap = null;
private Map<String, Object> params;
// 获取数据集
public List<T> getList() {
return this.list;
}
// 获取表头数据
public Map<Integer, String> getHeadMap() {
return this.headMap;
}
/**
* 每条数据都会进入
*
* @param object
* @param analysisContext
*/
@Override
public void invoke(T object, AnalysisContext analysisContext) {
this.list.add(object);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
/**
* 数据解析完调用
*
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 读取到的表头信息
*
* @param headMap
* @param context
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
this.headMap = headMap;
}
/**
* 异常时调用
*
* @param exception:
* @param context:
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 数据解析异常
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
throw new RuntimeException("第" + excelDataConvertException.getRowIndex() + "行" + excelDataConvertException.getColumnIndex() + "列" + "数据解析异常");
}
// 其他异常...
}
/**
* 数据存储到数据库
*/
public abstract boolean saveData();
}
这里作为公共导入监听器,保存数据的方法saveData(),下放到各层具体的业务去继承实现。
2.4、创建实体类
创建学生和老师两个实体类
Student
package com.wm.easyexcel.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName: Student
* @Description: 学生
* @Author: WM
* @Date: 2021-07-22 21:30
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
@ExcelProperty("序号")
private Integer num;
@ExcelProperty("学号")
private String sno;
@ExcelProperty("学生名字")
private String name;
@ExcelProperty("性别")
private String sex;
}
Teacher
package com.wm.easyexcel.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* @ClassName: Teacher
* @Description: 老师
* @Author: WM
* @Date: 2021-07-22 21:30
**/
@Data
public class Teacher {
@ExcelProperty("编号")
private String sno;
@ExcelProperty("老师名字")
private String name;
@ExcelProperty("性别")
private String sex;
}
2.5、创建业务接口
IStudentService
package com.wm.easyexcel.service;
import com.wm.easyexcel.entity.Student;
import java.util.List;
import java.util.Map;
/**
* @ClassName: IStudentService
* @Description: 学生相关接口
* @Author: WM
* @Date: 2023/1/9 16:52
*/
public interface IStudentService {
/**
* 查询学生集合信息
*
* @return
*/
List<Student> listStudent(Long limit);
/**
* 批量保存学生数据
*
* @param students
* @return
*/
boolean batchSaveStudent(List<Student> students);
/**
* 批量保存学生数据-自行处理行列数据,不使用实体
*
* @param dataList
* @param headMap
* @return
*/
boolean batchSaveStudent(List<Map<Integer, String>> dataList, Map<Integer, String> headMap);
}
对应的实现类StudentServiceImpl
package com.wm.easyexcel.service.impl;
import cn.hutool.core.lang.UUID;
import com.wm.easyexcel.entity.Student;
import com.wm.easyexcel.service.IStudentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @ClassName: StudentServiceImpl
* @Description: 学生相关接口实现
* @Author: WM
* @Date: 2023/1/9 16:53
*/
@Slf4j
@Service
public class StudentServiceImpl implements IStudentService {
@Override
public List<Student> listStudent(Long limit) {
List<Student> students = new ArrayList<>();
for (int i = 0; i < limit; i++) {
Student student = new Student();
student.setSex("男");
student.setNum(i);
student.setName("小A" + i);
student.setSno(UUID.fastUUID().toString());
students.add(student);
}
return students;
}
@Override
public boolean batchSaveStudent(List<Student> students) {
// 调用dao层接口保存数据...
log.info("批量保存学生数据,共:{}条数据", students.size());
return true;
}
@Override
public boolean batchSaveStudent(List<Map<Integer, String>> dataList, Map<Integer, String> headMap) {
// 这里可以针对读取到的表头数据和对应的行数据,这种应用场景适用于导入的excel中数据需要我们自己解析做其他业务处理,或者表头列是动态的,我们
// 没有办法使用一个固定的实体做映射
log.info("表头列数据:{}", headMap.values());
log.info("批量保存学生数据,共:{}条数据", dataList.size());
return true;
}
}
ITeacherService
package com.wm.easyexcel.service;
import com.wm.easyexcel.entity.Teacher;
import java.util.List;
import java.util.Map;
/**
* @ClassName: ITeacherService
* @Description: 老师相关接口
* @Author: WM
* @Date: 2023/1/9 16:52
*/
public interface ITeacherService {
/**
* 查询老师集合信息
*
* @return
*/
List<Teacher> listTeacher(Long limit);
/**
* 批量保存老师数据
*
* @param Teachers
* @return
*/
boolean batchSaveTeacher(List<Teacher> Teachers);
}
对应的实现类TeacherServiceImpl
package com.wm.easyexcel.service.impl;
import cn.hutool.core.lang.UUID;
import com.wm.easyexcel.entity.Teacher;
import com.wm.easyexcel.service.ITeacherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: TeacherServiceImpl
* @Description: 老师相关接口实现
* @Author: WM
* @Date: 2023/1/9 16:53
*/
@Slf4j
@Service
public class TeacherServiceImpl implements ITeacherService {
@Override
public List<Teacher> listTeacher(Long limit) {
List<Teacher> teachers = new ArrayList<>();
for (int i = 0; i < limit; i++) {
Teacher teacher = new Teacher();
teacher.setSex("女");
teacher.setName("老师A" + i);
teacher.setSno(UUID.fastUUID().toString());
teachers.add(teacher);
}
return teachers;
}
@Override
public boolean batchSaveTeacher(List<Teacher> teachers) {
// 调用dao层接口保存数据...
log.info("批量保存老师数据,共:{}条数据", teachers.size());
return true;
}
}
2.6、创建Controller
2.6.1、导出ExportController
创建导出的Controller为工具类中各个导出方法的测试做准备。
2.6.1.1、代码展示
package com.wm.easyexcel.controller;
import cn.hutool.core.collection.CollUtil;
import com.wm.easyexcel.entity.Student;
import com.wm.easyexcel.handler.UserExcelHeadHandler;
import com.wm.easyexcel.service.IStudentService;
import com.wm.easyexcel.service.ITeacherService;
import com.wm.easyexcel.strategy.RowMergeStrategy;
import com.wm.easyexcel.util.CellWidthStyleHandler;
import com.wm.easyexcel.util.EasyExcelUtil;
import com.wm.easyexcel.util.ExcelHeadStyles;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.*;
import java.util.stream.Collectors;
/**
* @ClassName: ExportController
* @Description: 导出前端控制器
* @Author: WM
* @Date: 2023/1/9 16:44
*/
@Slf4j
@RestController
public class ExportController {
@Resource
private IStudentService studentService;
@Resource
private ITeacherService teacherService;
private static final String[] USER_HEAD_FIELDS = new String[]{"序号", "学号", "姓名", "性别"};
/**
* 导出学生数据-一个sheet
*
* @param response
*/
@GetMapping("/student/export/sheet")
public void exportSheetStudent(HttpServletResponse response) {
List<Student> students = studentService.listStudent(10L);
log.info("单sheet学生数据开始导出...");
long startTimeL = System.currentTimeMillis();
try {
EasyExcelUtil.exportExcel(response, students, Student.class, "导出学生数据-一个sheet", "第一页");
} catch (Exception e) {
log.error("单sheet学生数据导出异常!", e);
}
log.info("单sheet学生数据导出完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/**
* 导出学生数据-多个sheet
*
* @param response
*/
@GetMapping("/student/export/sheets")
public void exportSheetsStudent(HttpServletResponse response) {
log.info("多sheet学生数据开始导出...");
long startTimeL = System.currentTimeMillis();
try {
List<List<?>> sheetList = new ArrayList<>();
List<Student> students1 = studentService.listStudent(10L);
List<Student> students2 = studentService.listStudent(20L);
sheetList.add(students1);
sheetList.add(students2);
Map<Integer, String> clazzMap = new HashMap<>();
clazzMap.put(0, "sheet1");
clazzMap.put(1, "sheet2");
EasyExcelUtil.exportExcels(response, sheetList, clazzMap, "导出学生数据-多个sheet");
} catch (Exception e) {
log.error("多sheet学生数据导出失败!", e);
}
log.info("多sheet学生数据导出完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/**
* 导出学生数据-重复行合并格式
*
* @param response
*/
@GetMapping("/student/export/mergeRow")
public void exportMergeRowStudent(HttpServletResponse response) {
log.info("合并格式学生数据开始导出...");
long startTimeL = System.currentTimeMillis();
try {
List<Student> students = studentService.listStudent(10L);
List<String> mergeDataList = students.stream().map(Student::getSex).collect(Collectors.toList());
EasyExcelUtil.exportWriteHandlerExcel(response, students, Student.class, "学生重复数据表格合并", "第一页", new RowMergeStrategy(mergeDataList, 3));
} catch (Exception e) {
log.error("合并格式学生数据导出失败!", e);
}
log.info("合并格式学生数据导出完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/**
* 下载导入学生数据的模板【一个自定义Handler】
*
* @param response
*/
@GetMapping("/student/downTemplate001")
public void downTemplateStudent001(HttpServletResponse response) {
log.info("设置单元格宽度-学生数据模板开始下载...");
long startTimeL = System.currentTimeMillis();
try {
List<List<String>> headList = Arrays.stream(USER_HEAD_FIELDS).map(field -> CollUtil.newArrayList(field)).
collect(Collectors.toList());
CellWidthStyleHandler columnWidthHandler = new CellWidthStyleHandler(true, 15);
EasyExcelUtil.exportExcel(response, headList, Collections.EMPTY_LIST, "学生数据导入模板-设置单元格宽度", "student", columnWidthHandler);
} catch (Exception e) {
log.error("设置单元格宽度-学生数据模板下载失败!", e);
}
log.info("设置单元格宽度-学生数据模板下载完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/**
* 下载导入学生数据的模板【多个自定义Handler】
*
* @param response
*/
@GetMapping("/student/downTemplate002")
public void downTemplateStudent002(HttpServletResponse response) {
log.info("设置单元格宽度+字体-学生数据模板开始下载...");
long startTimeL = System.currentTimeMillis();
try {
List<List<String>> headList = Arrays.stream(USER_HEAD_FIELDS).map(field -> CollUtil.newArrayList(field)).
collect(Collectors.toList());
// 表头第一行的指定前3列字标红
ExcelHeadStyles excelHeadStyle = new ExcelHeadStyles(0, 3, IndexedColors.RED1.getIndex());
UserExcelHeadHandler headHandler = new UserExcelHeadHandler(excelHeadStyle);
CellWidthStyleHandler columnWidthHandler = new CellWidthStyleHandler(true, 15);
EasyExcelUtil.exportExcel(response, headList, Collections.EMPTY_LIST, "学生数据导入模板-设置单元格宽度+字体", "student", CollUtil.newArrayList(headHandler, columnWidthHandler));
} catch (Exception e) {
log.error("设置单元格宽度+字体-学生数据模板下载失败!", e);
}
log.info("设置单元格宽度+字体-学生数据模板下载完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/**************以下填充模板数据的接口测试说明:可改为get请求,InputStream可使用本地文件输入流,然后变可通过浏览器下载。******************************/
/**
* 根据模板将集合对象填充表格-list
*
* @param template
* @param response
*/
@PostMapping("/student/template/fill-list")
public void exportFillTemplateList(@RequestParam("template") MultipartFile template, HttpServletResponse response) {
log.info("根据模板填充学生数据,list,文件开始下载...");
long startTimeL = System.currentTimeMillis();
try {
List students = studentService.listStudent(10L);
EasyExcelUtil.exportTemplateExcelList(template.getInputStream(), response, students, "填充学生模板数据-list", "students");
} catch (Exception e) {
log.error("根据模板填充学生数据,list,文件下载失败!", e);
}
log.info("根据模板填充学生数据,list,文件下载完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/**
* 根据模板将集合对象填充表格-单个sheet
*
* @param template
* @param response
*/
@PostMapping("/student/template/fill-sheet")
public void exportFillTemplateSheet(@RequestParam("template") MultipartFile template, HttpServletResponse response) {
log.info("根据模板填充学生数据,单个sheet,文件开始下载...");
long startTimeL = System.currentTimeMillis();
try {
List<Student> students = studentService.listStudent(10L);
Map<String, Object> map = new HashMap<>(3);
map.put("startMonth", "2022-8-10");
map.put("endMonth", "2022-8-12");
EasyExcelUtil.exportTemplateSheet(template.getInputStream(), response, students, map, "填充学生模板数据-sheet");
} catch (Exception e) {
log.error("根据模板填充学生数据,单个sheet,文件下载失败!", e);
}
log.info("根据模板填充学生数据,单个sheet,文件下载完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/**
* 根据模板将集合对象填充表格-多个sheet
*
* @param template
* @param response
*/
@PostMapping("/student/template/fill-sheets")
public void exportFillTemplateSheets(@RequestParam("template") MultipartFile template, HttpServletResponse response) {
log.info("根据模板填充数据,多个sheet,文件开始下载...");
long startTimeL = System.currentTimeMillis();
try {
List students = studentService.listStudent(10L);
Map<String, Object> map = new HashMap<>(2);
map.put("startMonth", "2022-8-10");
map.put("endMonth", "2022-8-12");
List teachers = teacherService.listTeacher(10L);
Map<String, Object> map2 = new HashMap<>(1);
map2.put("illustrate", "老师人员列表");
EasyExcelUtil.exportTemplateSheets(template.getInputStream(), response, students, teachers, map, map2, "填充学生模板数据-sheets");
} catch (Exception e) {
log.error("根据模板填充数据,多个sheet,文件下载失败!", e);
}
log.info("根据模板填充数据,多个sheet,文件下载完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
/********转为get请求测试示例********/
/**
* get请求测试
*
* @param response
*/
@GetMapping("/student/template/test")
public void test(HttpServletResponse response) throws FileNotFoundException {
File file = new File("C:\\Users\\Administrator\\Desktop\\test.xlsx");
FileInputStream inputStream = new FileInputStream(file);
log.info("根据模板填充学生数据,单个sheet,文件开始下载...");
long startTimeL = System.currentTimeMillis();
try {
List<Student> students = studentService.listStudent(10L);
Map<String, Object> map = new HashMap<>(3);
map.put("startMonth", "2022-8-10");
map.put("endMonth", "2022-8-12");
EasyExcelUtil.exportTemplateSheet(inputStream, response, students, map, "填充学生模板数据-sheet");
} catch (Exception e) {
log.error("根据模板填充学生数据,单个sheet,文件下载失败!", e);
}
log.info("根据模板填充学生数据,单个sheet,文件下载完成:{}秒", (System.currentTimeMillis() - startTimeL) / 1000);
}
}
2.6.1.2、方法说明
(1)导出学生数据-一个sheet
此方法是简单的做导出数据,导出的excel不做任何格式的修改。
导出结果如下:
(2)导出学生数据-多个sheet
此方法导出两个sheet的数据,导出的excel不做任何格式的修改。
导出结果如下:
(3)导出学生数据-重复行合并格式
此方法导出数据中,指定列的相邻行单元格有重复的内容会被合并成一个单元格。这里需要创建一个设定单元格合并策略类RowMergeStrategy:
package com.wm.easyexcel.strategy;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: RowMergeStrategy
* @Description: 自定义行合并策略
* @Author: WM
* @Date: 2023/1/9 20:01
*/
public class RowMergeStrategy extends AbstractMergeStrategy {
/**
* 分组,每几行合并一次
*/
private List<Integer> exportFieldGroupCountList;
/**
* 目标合并列index
*/
private Integer targetColumnIndex;
// 需要开始合并单元格的首行index
private Integer rowIndex;
// exportDataList为待合并目标列的值
public RowMergeStrategy(List<String> exportDataList, Integer targetColumnIndex) {
this.exportFieldGroupCountList = getGroupCountList(exportDataList);
this.targetColumnIndex = targetColumnIndex;
}
@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer integer) {
if (null == rowIndex) {
rowIndex = cell.getRowIndex();
}
// 仅从首行以及目标列的单元格开始合并,忽略其他
if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == targetColumnIndex) {
mergeGroupColumn(sheet);
}
}
private void mergeGroupColumn(Sheet sheet) {
int rowCount = rowIndex;
for (Integer count : exportFieldGroupCountList) {
if (count == 1) {
rowCount += count;
continue;
}
// 合并单元格
CellRangeAddress cellRangeAddress = new CellRangeAddress(rowCount, rowCount + count - 1, targetColumnIndex, targetColumnIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
rowCount += count;
}
}
// 该方法将目标列根据值是否相同连续可合并,存储可合并的行数
private List<Integer> getGroupCountList(List<String> exportDataList) {
if (CollUtil.isEmpty(exportDataList)) {
return new ArrayList<>();
}
List<Integer> groupCountList = new ArrayList<>();
int count = 1;
for (int i = 1; i < exportDataList.size(); i++) {
if (exportDataList.get(i).equals(exportDataList.get(i - 1))) {
count++;
} else {
groupCountList.add(count);
count = 1;
}
}
groupCountList.add(count);
return groupCountList;
}
}
导出结果如下:
(4)下载导入学生数据的模板【一个自定义Handler】
- 这个方法的应用场景是,用户下载自己需要填充数据的模板,这里excel的表头是自定义设定的,可固定也可不固定,我程序里设定的是固定写死的,举个不固定表头的例子:不同租户下用户导出的模板显示的表头数据不同,这个一般是在数据库查出来的设置的。
- 此外这个方法还设置了单元格的固定宽度样式,所以使用了一个Handler处理器,即CellWidthStyleHandler。
导出结果如下:
(5)下载导入学生数据的模板【多个自定义Handler】
此方法是在(4)方法的基础上,通过ExcelHeadStyles、UserExcelHeadHandler增加了对表头字体的设置,这里处理的是表头第一行的指定前3列字标红。因为是这是指定业务个人化的一种样式,所以不放入公共样式包里。
UserExcelHeadHandler代码如下:
package com.wm.easyexcel.handler;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.wm.easyexcel.util.ExcelHeadStyles;
import org.apache.poi.ss.usermodel.IndexedColors;
/**
* @ClassName: UserExcelHeadHandler
* @Description: 用户相关excel处理器
* @Author: WM
* @Date: 2023/1/9 18:24
*/
public class UserExcelHeadHandler extends HorizontalCellStyleStrategy {
private ExcelHeadStyles excelHeadStyle;
public UserExcelHeadHandler(ExcelHeadStyles excelHeadStyle) {
this.excelHeadStyle = excelHeadStyle;
}
//设置头样式
@Override
protected void setHeadCellStyle(CellWriteHandlerContext context) {
int columnNo = excelHeadStyle.getColumnIndex();
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 11);
if (context.getColumnIndex() <= columnNo) {
headWriteFont.setColor(excelHeadStyle.getFontColor());
} else {
headWriteFont.setColor(IndexedColors.BLACK.getIndex());
}
headWriteCellStyle.setWriteFont(headWriteFont);
if (stopProcessing(context)) {
return;
}
WriteCellData<?> cellData = context.getFirstCellData();
WriteCellStyle.merge(headWriteCellStyle, cellData.getOrCreateStyle());
}
}
导出结果如下:
(6)根据模板将集合对象填充表格-list
此方法是根据上传的模板文件,将查询的数据填充到excel指定的名称的sheet,然后导出。
上传文件的模板(后面根据上传模板导出填充数据都是用的这个模板):
模板影样式如下:
导出结果如下:
(7)根据模板将集合对象填充表格-单个sheet
此方法涉及到自定义字段数据的导出,这里是“开始日期”、“结束日期”。
模板样式如下:
导出结果如下:
(8)根据模板将集合对象填充表格-多个sheet
此方法导出两个不同集合的数据,指定到对应的sheet。
学生的样式未变,老师模板格式如下:
导出结果如下:
2.6.2、导入ImportController
创建导入的Controller为工具类中各个导入方法的测试做准备。
2.6.2.1、代码展示
package com.wm.easyexcel.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import com.wm.easyexcel.entity.Student;
import com.wm.easyexcel.entity.Teacher;
import com.wm.easyexcel.listern.ImportUserListener;
import com.wm.easyexcel.util.EasyExcelUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: ImportController
* @Description: 导入前端控制器
* @Author: WM
* @Date: 2023/1/9 16:44
*/
@Slf4j
@RestController
public class ImportController {
@Resource
private ImportUserListener importUserListener;
/**
* 导入一个sheet学生数据
*
* @param file
*/
@PostMapping("/student/import/sheet")
public boolean importSheetStudent(@RequestParam("file") MultipartFile file) {
EasyExcelUtil.checkFile(file);
log.info("一个sheet学生数据开始导入...");
long startTime = System.currentTimeMillis();
List<Student> dataList = null;
try {
dataList = EasyExcelUtil.importExcel(file.getInputStream(), null, null, importUserListener);
// 数据录入
Assert.isFalse(CollUtil.isEmpty(dataList), "未解析到导入的数据!");
} catch (Exception e) {
log.error("一个sheet学生数据导入失败!", e);
}
log.info("一个sheet学生数据导入成功,共:{}条,消耗总时间:{}秒", dataList.size(), ((System.currentTimeMillis() - startTime) / 1000));
return true;
}
/**
* 导入多个sheet学生数据
*
* @param file
*/
@PostMapping("/student/import/sheets")
public boolean importSheets(@RequestParam("file") MultipartFile file) {
EasyExcelUtil.checkFile(file);
log.info("多个sheet数据开始导入...");
long startTime = System.currentTimeMillis();
List<Student> dataList = null;
try {
List sheetObjList = new ArrayList(2);
sheetObjList.add(Student.class);
sheetObjList.add(Teacher.class);
dataList = EasyExcelUtil.importExcels(file.getInputStream(), 2, sheetObjList, importUserListener);
// 数据录入
Assert.isFalse(CollUtil.isEmpty(dataList), "未解析到导入的数据!");
} catch (Exception e) {
log.error("多个sheet数据导入失败!", e);
}
log.info("多个sheet数据导入成功,共:{}条,消耗总时间:{}秒", dataList.size(), ((System.currentTimeMillis() - startTime) / 1000));
return true;
}
}
2.6.2.2、方法说明
(1)导入一个sheet学生数据
此方法是将上传的学生数据excel文件解析,然后解析后的数据保存到数据库中。此处需要使用到的ImportUserListener继承了ReadExcelListener,其主要实现逻辑是将学生或老师的数据进行保存处理。
ImportUserListener代码如下:
package com.wm.easyexcel.listern;
import cn.hutool.core.collection.CollUtil;
import com.wm.easyexcel.entity.Student;
import com.wm.easyexcel.entity.Teacher;
import com.wm.easyexcel.service.IStudentService;
import com.wm.easyexcel.service.ITeacherService;
import com.wm.easyexcel.util.ReadExcelListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* @ClassName: ImportStudentListener
* @Description: 导入学生excel数据监听器
* @Author: WM
* @Date: 2022/12/22 10:44
*/
@Component
public class ImportUserListener extends ReadExcelListener {
@Resource
private IStudentService studentService;
@Resource
private ITeacherService teacherService;
/**
* 实现数据保存逻辑
*
* @return
*/
@Override
public boolean saveData() {
List dataList = getList();
if (CollUtil.isEmpty(dataList)) {
return false;
}
Object obj = dataList.get(0);
if (obj instanceof Student) {
return studentService.batchSaveStudent(dataList);
} else if (obj instanceof Teacher) {
return teacherService.batchSaveTeacher(dataList);
}
return true;
}
}
(2)导入多个sheet学生数据
此方法是将excel解析出来的学生和老师数据集合保存于数据库中,这里其实就是在(1)方法的基础上增加了循环处理的操作。
2.6.2.3、导入监听器-动态表头字段
有些场景每次导入数据的表头字段是动态变化的,所以我们不能使用一个固定的实体去和excel表头字段做映射,那么这时候就需要我们自己去处理表头和表数据。这里使用到了ImportDataListener监听器,代码如下:
package com.wm.easyexcel.listern;
import cn.hutool.core.collection.CollUtil;
import com.wm.easyexcel.service.IStudentService;
import com.wm.easyexcel.util.ReadExcelListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* @ClassName: ImportDataListener
* @Description: 导入数据监听器
* @Author: WM
* @Date: 2023/1/9 19:27
*/
@Component
public class ImportDataListener extends ReadExcelListener {
@Resource
private IStudentService studentService;
/**
* 实现数据保存逻辑-直接操作表头和行数据
*
* @return
*/
@Override
public boolean saveData() {
Map<Integer, String> headMap = getHeadMap();
List dataList = getList();
if (CollUtil.isEmpty(headMap) || CollUtil.isEmpty(dataList)) {
return false;
}
return studentService.batchSaveStudent(dataList, headMap);
}
}
3、测试
3.1、导出测试
3.1.1、总测试结果
最终导出的文件如下所示:
3.2、导入测试
3.2.1、导入一个sheet学生数据
3.2.2、导入多个sheet学生数据
4、总结
本文主要是对EasyExcel导入导出使用的讲解,包括一些导出常用的单元格宽度以及内部字体样式设置,常规的导出以及不固定表头数据的导出,根据上传模板导出数据;常规的根据上传excel文件导入数据,以及根据非固定字段的excel文件上传导入数据。
本文章的demo地址:https://github.com/Deamer1102/excel-demo/tree/master/easyexcel