官方文档:写Excel | Easy Excel (alibaba.com)
当使用 EasyExcel 进行 Excel 文件导出时,我最近在工作中遇到了一个需求。因此,我决定写这篇文章来分享我的经验和解决方案。如果你对这个话题感兴趣,那么我希望这篇文章对你有所帮助。
本文的目标是介绍 EasyExcel 的基本概念、使用方法以及解决特定问题的技巧。通过使用 EasyExcel,我们可以提高文件导出的效率,简化代码,并实现更灵活的数据导出。
在阅读完本文后,你将能够了解 EasyExcel 的核心功能和常用操作,掌握如何根据实际需求进行配置和定制。此外,我还将分享一些实用的技巧和最佳实践,帮助你更好地利用 EasyExcel 完成文件导出任务。
最后,我要再次感谢你的关注和支持。如果你觉得这篇文章对你有所帮助,请不要吝啬点赞、收藏和关注我的其他文章或资源。这将是对我最大的鼓励和支持!
文章内容会持续更新!!!
选择使用 EasyExcel 而不是 POI 的原因主要有以下几点:
总的来说,EasyExcel 在处理大数据量的 Excel 文件导出方面,相比 POI 具有明显的优势,这也是为什么越来越多人选择使用 EasyExcel 的原因。
简单来说就是,因为 EasyExcel 性能更好。
EasyExcel 是一个基于 Java 的开源库,用于简化和优化 Excel 文件的读写操作。它提供了一种简单而高效的方式来处理大量数据的导入和导出,特别适用于大数据量的处理。
EasyExcel 具有以下特点:
{
"code": "1",
"message": "导出文件失败:java.lang.NoSuchMethodError: org.apache.poi.ss.usermodel.Cell.setCellValue(Ljava/time/LocalDateTime;)V"
}
这是因为要导出的列里面有 Date
类型,EasyExcel 识别不了
public class DateConverter implements Converter<Date> {
private static final String PATTERN_YYYY_MM_DD = "yyyy-MM-dd";
@Override
public Class<Date> supportJavaTypeKey() {
return Date.class;
}
/**
* easyExcel导出数据类型转换
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public Date convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
String value = cellData.getStringValue();
SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_YYYY_MM_DD);
return sdf.parse(value);
}
/**
* easyExcel导入Date数据类型转换
* @param context
* @return
* @throws Exception
*/
@Override
public WriteCellData<String> convertToExcelData(WriteConverterContext<Date> context) throws Exception {
Date date = context.getValue();
if (date == null) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_YYYY_MM_DD);
return new WriteCellData<>(sdf.format(date));
}
}
然后,修改导出视图类中的 Date 类型字段,加入 converter = DateConverterUtil.class
转换属性。
@ExcelProperty(value = "最新维保时间", index = 3, converter = DateConverter.class)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date time;
使用 EasyExcel 的 @DateTimeFormat
注解:
// 字段类型为String,否则注解可能会无效
@DateTimeFormat(value = "yyyy-MM-dd HH:mm:ss")
private String alarmTime;
对象转换使用案例:
1、在需要使用时,转换时间值
// 1、获取导出列表
List<Alarm> list = alarmService.list();
List<AlarmExportVO> list2 = list.stream().map(item -> {
AlarmExportVO exportVO = new AlarmExportVO();
exportVO.setEnterpriseTown(item.getEnterpriseTown());
exportVO.setMarketSupervision(item.getMarketSupervision());
exportVO.setDeviceName(item.getDeviceName());
exportVO.setAlarmType(item.getAlarmType());
exportVO.setAlarmLevel(item.getAlarmLevel());
// item.getAlarmTime() 返回的是一个 Date 对象
Date alarmTime = item.getAlarmTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formattedTime = sdf.format(alarmTime);
exportVO.setAlarmTime(formattedTime);
2、在实体类中进行修改,重写 get 方法
public String getAlarmTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(alarmTime);
}
反例:
直接用 @Resource
注入 bean 使用,会报错,不被 Spring IoC 管理。
List<DictDTO> dictList = documentMapper.getDict(CERTIFICATE);
报错:
注入的 bean
,查数据库的时候,报 空指针异常
,导致转换数据的时候第一条字典映射转换就失败
原因猜测:
new
出来的,而不是由 Spring 容器管理的 Bean
。Bean
,你需要手动获取这些 Bean,而不是通过 Spring 的依赖注入。Spring 管理的 bean 通常是由 Spring 容器负责创建、配置和管理的。当你使用
new
运算符直接实例化一个对象时,这个对象不会由 Spring 容器来管理,因此 Spring 不会介入该对象的生命周期和依赖注入。
解决方法
通过 Spring 容器提供的方法来获取已经由 Spring 管理的 bean。
DocumentMapper bean = SpringUtil.getBean(DocumentMapper.class);
List<DictDTO> dictList = bean.getDict(CERTIFICATE);
参考:
EasyExcel 使用Converter 转换注入时报nullPoint异常_converter null入参_地平线上的新曙光的博客-CSDN博客
完整代码:
public class DocumentDictConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return cellData.getStringValue();
}
/**
* 这里是写的时候会调用
*
* @return
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
// 获取字典列表
DocumentMapper bean = SpringUtil.getBean(DocumentMapper.class);
List<DictDTO> dictList = bean.getDict(CERTIFICATE);
HashMap<String, String> dictMap = new HashMap<>(16);
for (DictDTO dictDTO : dictList) {
dictMap.put(dictDTO.getDictValue(), dictDTO.getDictName());
}
// 根据字典映射进行值转换
String convertedValue = dictMap.get(context.getValue());
if (convertedValue != null) {
return new WriteCellData<>(convertedValue);
} else {
return new WriteCellData<>(context.getValue());
}
}
}
每次做字段值映射转换的时候都需要查数据库,太影响性能了。
@Cacheable
)参考笔者这篇文章:Cacheable注解小记 | DreamRain
使用说明:
@Cacheable("dictionaryCache")
注解来定义缓存区域为 “dictionaryCache”,并且可以根据 dictionaryType
参数来查询字典数据。@PostConstruct
注解来标记一个初始化方法,该方法在 Spring 容器加载完所有 bean 后执行。@Service
public class DictionaryDataService {
private Map<String, String> certificateDict = new HashMap<>();
@Autowired
private DocumentExpiredMapper documentExpiredMapper;
// 这个方法会在 bean 初始化时被自动调用
@PostConstruct
public void init() {
// 在 bean 初始化时执行一些初始化操作
List<DictDTO> dictList = documentExpiredMapper.getDict(CERTIFICATE);
for (DictDTO dictDTO : dictList) {
certificateDict.put(dictDTO.getDictValue(), dictDTO.getDictName());
}
}
public String getCertificateDictValue(String key) {
return certificateDict.get(key);
}
}
2、修改转换器类,使用内存中的字典数据进行转换:
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
DictionaryDataService bean = SpringUtil.getBean(DictionaryDataService.class);
String convertedValue = bean.getCertificateDictValue(context.getValue());
if (convertedValue != null) {
return new WriteCellData<>(convertedValue);
} else {
return new WriteCellData<>(context.getValue());
}
}
通过这种方式,字典数据在应用启动时加载到内存中,以后的值转换操作都会使用内存中的数据,避免了重复查询数据库的性能开销。这是一种常见的性能优化方法。
@Service
public class DictionaryService {
// 声明静态变量存储字典数据
public static HashMap<String, HashMap<String, String>> hashMap = new HashMap<>();
@Resource
private DocumentExpiredMapper documentExpiredMapper;
/**
* 将字典数据存入静态变量
* @param dictionaryType
* @return
*/
public HashMap<String, String> getDict(String dictionaryType) {
if (hashMap.containsKey(dictionaryType)){
return hashMap.get(dictionaryType);
}
List<DictDTO> dictList = documentExpiredMapper.getDict(dictionaryType);
HashMap<String, String> dictMap = new HashMap<>(dictList.size());
for (DictDTO dictDTO : dictList) {
dictMap.put(dictDTO.getDictValue(), dictDTO.getDictName());
}
hashMap.put(dictionaryType, dictMap);
return dictMap;
}
}
第一种方法(DictionaryService
使用缓存):
优点:
@Cacheable
注解,Spring 会自动处理缓存相关逻辑,包括缓存的清除、存储、失效等,减轻了你的工作负担。DictionaryService
服务,而不需要关心缓存细节。缺点:
第二种方法(DictionaryDataService
使用内存缓存):
优点:
缺点:
场景分析:
@PostConstruct
是 Java EE(Enterprise Edition)的注解之一,它标识在类实例化后,但在类投入使用之前要执行的方法。通常在使用 Spring 框架或其他依赖注入框架时,@PostConstruct
注解用于在 bean 的初始化过程中执行一些额外的初始化操作。以下是关于 @PostConstruct
注解的一些重要信息:
@PostConstruct
用于定义在 bean 的生命周期中何时应该执行的初始化方法。它提供了一个方便的方式来执行一些准备工作,如数据加载、资源初始化等。@PostConstruct
注解的方法会在 Spring 容器创建 bean 实例后,依赖注入之前执行。这意味着它是在 bean 的构造函数之后,依赖注入之前执行的,用于初始化 bean 的各种属性。@PostConstruct
注解的方法没有参数。方法名可以随意命名,但通常为 init
、initialize
、postConstruct
等。@PostConstruct
注解通常与依赖注入和容器管理框架(如 Spring、Java EE 容器等)一起使用。容器会在执行构造函数和依赖注入后,自动调用被 @PostConstruct
注解的初始化方法。@PostConstruct
注解的方法抛出异常,容器会将异常捕获并处理,通常会导致 bean 创建失败。这可以用于在初始化阶段检测配置错误或其他问题。@PostConstruct
注解的方法只会被调用一次,即使 bean 在容器中被多次注入也是如此。@PostConstruct
常用于执行一些需要在 bean 初始化时进行的操作,例如数据库连接的建立、资源初始化、数据加载等。以下是 “加载到内存” 和 “放入缓存” 的相关概念。
加载到内存:
放入缓存:
内存加载的情况:
内存加载可能是一次性的,例如在应用程序启动时加载配置文件。(一次性初始化)
内存加载也可能是动态的,例如从数据库中加载实时数据或通过用户请求加载。
- 如果数据的变化频率较低,可以使用定时刷新。
- 如果数据变化频繁,懒加载或异步加载可能更合适。
- 缓存加载器适用于需要复杂逻辑来获取数据的情况。
缓存的情况:
综上所述:
参考官方文档案例:web中的写并且失败的时候返回json
@Override
public void exportDocument(DocumentDTO documentDTO, HttpServletResponse response) throws IOException {
// 1、获取证件临期告警列表
List<DocumentVO> documentList = getDocumentList(documentDTO);
// 2、 Excel文件标题
String title = "Excel文件标题";
// 3、告警类型值替换
List<DocumentExportVO> exportVOList = BeanUtil.copy(documentList, DocumentExportVO.class);
// 4、EasyExcel导出
try {
// 设置内容类型
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// 设置字符编码
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easy excel没有关系
String fileName = URLEncoder.encode(title, "UTF-8").replaceAll("\\+", "%20");
// 设置响应头
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 这里需要设置不关闭流
EasyExcel.write(response.getOutputStream(), DocumentExportVO.class)
.autoCloseStream(Boolean.FALSE)
.sheet("模板")
.doWrite(exportVOList);
} catch (Exception e) {
// 重置response
response.reset();
String errorMessage = "导出文件失败:" + e.getMessage();
returnResult(response, errorMessage);
// e.printStackTrace();
}
}
代码规范问题:
// 将错误信息全部打印在控制台
e.printStackTrace();
转换器类
的问题(不会打印到日志文件中,但会一直刷控制台)代码仓库:excel-demo
要压缩图片,您可以使用 Java 中的图像处理库,例如 ImageIO 或 Thumbnails 库。
下面是使用 Thumbnails 库压缩图片的示例讲述。
首先,确保您已将 Thumbnails 库添加到您的项目依赖项中。在 Maven 项目中,您可以在 pom.xml
文件中添加以下依赖项:
<dependency>
<groupId>net.coobirdgroupId>
<artifactId>thumbnailatorartifactId>
<version>0.4.14version>
dependency>
使用 File.createTempFile()
方法创建一个临时文件,然后通过 toFile()
方法将压缩后的图片保存到临时文件中。最后,将临时文件的路径设置到 ExcelExportVO
对象的 file
属性中。
临时文件的生命周期由操作系统管理,通常在程序退出后会自动删除。(可以设置手动删除)
使用 Thumbnails.of()
方法加载原始图片,然后通过 scale()
方法设置压缩比例。
try {
// 1.1 压缩图片并保存到临时文件
File compressedFile = File.createTempFile("compressed_image", ".jpg");
// 1.2 压缩图片
Thumbnails.of(new URL(item.getFile()))
.scale(0.5) // 设置压缩比例
.toFile(compressedFile);
// 1.3 将临时文件路径设置到ExcelExportVO对象中
exportVO.setFile(compressedFile.toURI().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException("压缩图片失败!!!", e);
}
选择压缩比例的大小取决于您的需求和偏好,以及图像的具体情况。
总结
这种压缩的效果会更好,但是需要手动删除资源。
@Test
void export() {
// 1、获取导出列表
List<TblExcel> list = excelService.list();
List<ExcelExportVO> list2 = list.stream().map(item -> {
ExcelExportVO exportVO = new ExcelExportVO();
exportVO.setName(item.getName());
try {
// 1.1 压缩图片并保存到临时文件
File compressedFile = File.createTempFile("compressed_image", ".jpg");
// 1.2 压缩图片
Thumbnails.of(new URL(item.getFile()))
.scale(0.5) // 设置压缩比例
.toFile(compressedFile);
// 1.3 将临时文件路径设置到ExcelExportVO对象中
exportVO.setFile(compressedFile.toURI().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException("压缩图片失败!!!", e);
}
return exportVO;
}).collect(Collectors.toList());
// 2、导出
EasyExcel.write("D:\\TblExcel.xls")
.sheet("模板")
.head(ExcelExportVO.class)
.doWrite(list2);
// 3、使用完毕后手动删除临时文件
for (ExcelExportVO exportVO : list2) {
try {
File compressedFile = new File(exportVO.getFile().toURI());
if (!compressedFile.delete()) {
// 删除操作失败,记录日志或进行其他错误处理
log.error("删除临时文件失败: " + compressedFile.getAbsolutePath());
}
} catch (Exception e) {
// 处理删除临时文件的异常
e.printStackTrace();
}
}
}
try-with-resources
来压缩图像并保存到临时文件后,该临时文件会在 try-with-resources
块结束时自动关闭。因此,我们无需另外手动操作删除临时文件。代码如下:
/**
* Thumbnails 压缩图片导出 -- 使用 try-with-resources 自动关闭资源版
* 压缩效果较差
*/
@Test
void export11() {
// 1、获取导出列表
List<TblExcel> list = excelService.list();
List<ExcelExportVO> list2 = list.stream().map(item -> {
ExcelExportVO exportVO = new ExcelExportVO();
exportVO.setName(item.getName());
try {
// 压缩图像并保存到临时文件
File compressedFile;
try (OutputStream outputStream = new FileOutputStream(compressedFile = File.createTempFile("compressed_image", ".jpg"))) {
Thumbnails.of(new URL(item.getFile()))
.scale(0.5) // 设置压缩比例
.toOutputStream(outputStream);
}
// 将临时文件路径设置到ExcelExportVO对象中
exportVO.setFile(compressedFile.toURI().toURL());
} catch (IOException e) {
throw new RuntimeException("压缩图片失败!!!", e);
}
return exportVO;
}).collect(Collectors.toList());
// 2、导出
EasyExcel.write("D:\\TblExcel.xls")
.sheet("模板")
.head(ExcelExportVO.class)
.doWrite(list2);
}
在第一个方法中,我使用了 Thumbnails.of(new URL(item.getFile())).scale(0.5).toFile(compressedFile);
这一行代码来压缩图片。这个方法将压缩后的图片直接保存到了文件系统中,然后在 ExcelExportVO 对象中设置的是临时文件的路径。
而在第二个方法中,我使用了 Thumbnails.of(new URL(item.getFile())).scale(0.5).toOutputStream(outputStream);
这一行代码来压缩图片。这个方法将压缩后的图片数据写入到了一个输出流(OutputStream)中,而没有将其直接保存到文件系统。这意味着,虽然压缩后的图片数据被存储在了内存中的字节数组中,但并没有实际地创建一个新的临时文件。因此,在第二个方法中,ExcelExportVO 对象中的文件路径实际上指向的是一个尚未存在的临时文件。
当 EasyExcel 将这些对象写入到 Excel 文件时,它会尝试打开每个 ExcelExportVO 对象中的文件路径。在第一个方法中,因为临时文件已经存在,所以 EasyExcel 可以成功地打开并读取这些文件。但在第二个方法中,由于临时文件并未实际创建,所以 EasyExcel 无法打开这些文件。
因此,尽管两个方法都实现了压缩图片的功能,但由于第二个方法没有将压缩后的图片数据实际保存到文件系统中,所以在导出的 Excel 文件中可能不会包含这些图片数据,从而导致导出的文件更小。
测试数据:100 条记录,每条记录包含一张【图片URL】。
代码如下:
// 根据用户传入字段 假设我们只要导出 file
Set<String> includeColumnFiledNames = new HashSet<String>();
includeColumnFiledNames.add("file");
// 2、导出
EasyExcel.write("D:\\TblExcel.xls")
.sheet("模板")
.head(ExcelExportVO.class)
.includeColumnFieldNames(includeColumnFiledNames)
// .includeColumnIndexes(Collections.singleton(1))
.doWrite(list2);
具体说明:
includeColumnFieldNames 是根据字段名指定导出列(建议导出视图 VO 不要指定 index 属性,否则会有导出会有空列)
includeColumnIndexes 是根据索引指定导出列,即使实体类中没有指定 index
属性一样可以使用
List<Integer> columnList = Arrays.asList(0, 1, 2, 3, 4);
使用官方自带的处理器:
// 自动列宽
.registerWriteHandler((new LongestMatchColumnWidthStyleStrategy()))
具体说明:
自定义自适应列宽处理策略:
public class CustomColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
public CustomColumnWidthStyleStrategy() {
}
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
Sheet sheet = writeSheetHolder.getSheet();
sheet.setColumnWidth(3, 8000);
}
}
public class CustomMergeStrategy implements CellWriteHandler {
/**
* 合并列索引。
*/
private final List<Integer> mergeColumnIndexes;
/**
* 构造函数。
*
* @param mergeColumnIndexes 合并列索引集合。
*/
public CustomMergeStrategy(List<Integer> mergeColumnIndexes) {
this.mergeColumnIndexes = mergeColumnIndexes;
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 校验:如果当前是表头,则不处理。
if (isHead) {
return;
}
// 校验:如果当前是第一行,则不处理。
if (relativeRowIndex == 0) {
return;
}
// 校验:如果当前列索引不在合并列索引列表中,则不处理。
Integer columnIndex = cellDataList.get(0).getColumnIndex();
if (!this.mergeColumnIndexes.contains(columnIndex)) {
return;
}
// 获取:当前表格、当前行下标、上一行下标、上一行对象、上一列对象。
Sheet sheet = cell.getSheet();
int rowIndexCurrent = cell.getRowIndex();
int rowIndexPrev = rowIndexCurrent - 1;
Row rowPrev = sheet.getRow(rowIndexPrev);
Cell cellPrev = rowPrev.getCell(cell.getColumnIndex());
// 获取:当前单元格值、上一单元格值。
Object cellValueCurrent = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
Object cellValuePrev = cellPrev.getCellTypeEnum() == CellType.STRING ? cellPrev.getStringCellValue() : cellPrev.getNumericCellValue();
// 校验:如果当前单元格值与上一单元格值不相等,则不处理。
if (!cellValueCurrent.equals(cellValuePrev)) {
return;
}
List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
boolean merged = false;
for (int i = 0; i < mergedRegions.size(); i++) {
CellRangeAddress cellRangeAddress = mergedRegions.get(i);
if (cellRangeAddress.isInRange(rowIndexPrev, cell.getColumnIndex())) {
// 移除合并单元格。
sheet.removeMergedRegion(i);
// 设置合并单元格的结束行。
cellRangeAddress.setLastRow(rowIndexCurrent);
// 重新添加合并单元格。
sheet.addMergedRegion(cellRangeAddress);
merged = true;
break;
}
}
if (!merged) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(rowIndexPrev, rowIndexCurrent, cell.getColumnIndex(), cell.getColumnIndex());
sheet.addMergedRegion(cellRangeAddress);
}
}
}
// ......
// 导出
EasyExcel.write(response.getOutputStream())
.head(headList)
// 自动列宽
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// .registerWriteHandler(new CustomMergeStrategy(Arrays.asList(1, 2)))
.registerWriteHandler(new CustomMergeStrategy(Collections.singletonList(1)))
.autoCloseStream(Boolean.FALSE)
.sheet("模板")
.doWrite(resultList);
// ......
// 获取表格头列表
private List<List<String>> getHeadList(SettleDTO settleDTO) {
List<List<String>> headList = new ArrayList<>();
Date startDate = DateUtil.parse(settleDTO.getStartDay(), "yyyy-MM-dd");
Date endDate = DateUtil.parse(settleDTO.getEndDay(), "yyyy-MM-dd");
SimpleDateFormat sdf = new SimpleDateFormat("MM月dd日");
String startDay = sdf.format(startDate);
String endDay = sdf.format(endDate);
String day = startDay + "-" + endDay;
ArrayList<String> headColumn1 = new ArrayList<>();
headColumn1.add("第一列");
headList.add(headColumn1);
ArrayList<String> headColumn2 = new ArrayList<>();
headColumn2.add("第二列");
headList.add(headColumn2);
ArrayList<String> headColumn3 = new ArrayList<>();
headColumn3.add(day);
headColumn3.add("第三列");
headList.add(headColumn3);
return headList;
}
拓展性说明:
以下是 afterCellDispose
方法的各参数的含义:
WriteSheetHolder writeSheetHolder
:当前正在写入的 Sheet 的持有者。它包含有关当前 Sheet 的信息,例如 Sheet 的索引、当前行的索引以及其他相关详细信息。
WriteTableHolder writeTableHolder
:当前正在写入的表格的持有者。它包含有关当前表格的信息,例如表格的名称、起始行的索引以及其他相关详细信息。
List
:这是一个 WriteCellData
对象的列表,表示要写入当前单元格的数据。如果单元格跨越多个列或行,该列表可能包含多个 WriteCellData
对象。
Cell cell
:表示当前正在处理的单元格。它提供有关单元格位置、样式和其他属性的信息。
Head head
:表示当前单元格的头部(标题)。它包含有关头部的信息,如字段名、类类型以及其他相关详细信息。
如果传入的不是一个
class
文件(实体类)时,head 可能会为 null,例如上面示例(.head(headList)
),传入的是个列表集合,此时 head 为 null
Integer relativeRowIndex
:表示当前行在当前 Sheet 中的相对索引。一开始就是表头之下的第一行。
Boolean isHead
:这是一个布尔标志,指示当前单元格是否为表头单元格。如果 isHead
为 true
,则表示当前单元格是表头单元格;否则,它是数据单元格。
总的来说,这些参数提供了有关当前 Excel 写入过程状态的上下文信息。它们允许您基于正在处理的 Sheet、表格、单元格和头部,以及当前行在 Sheet 中的位置执行自定义逻辑。
压缩包导出有以下两种情况:
一、可以使用 hutool 的 ZipUtil 工具类
/**
* 压缩包导出 -- hutool
*/
@Test
void export6() throws IOException {
List<TblExcel> list = new ArrayList<>();
list.add(new TblExcel("张三", "abc"));
List<ByteArrayInputStream> ins = new ArrayList<>();
// 导出第一个Excel
ByteArrayOutputStream out1 = new ByteArrayOutputStream();
EasyExcel.write(out1, TblExcel.class).sheet("第一个").doWrite(list);
ins.add(new ByteArrayInputStream(out1.toByteArray()));
// 导出第二个Excel
ByteArrayOutputStream out2 = new ByteArrayOutputStream();
EasyExcel.write(out2, TblExcel.class).sheet("第二个").doWrite(list);
ins.add(new ByteArrayInputStream(out2.toByteArray()));
// 将多个 InputStream 压缩到一个 zip 文件
File zipFile = new File("C:\\Users\\乔\\Desktop\\noModelWrite.zip");
String[] fileNames = {"1.xlsx", "2.xlsx"};
InputStream[] inputStreams = ins.toArray(new InputStream[0]);
cn.hutool.core.util.ZipUtil.zip(zipFile, fileNames, inputStreams);
}
二、手写一个 ZipUtil 工具类
public class ZipUtil {
/**
* 默认编码,使用平台相关编码
*/
private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
/**
* 将文件流压缩到目标流中
*
* @param out 目标流,压缩完成自动关闭
* @param fileNames 流数据在压缩文件中的路径或文件名
* @param ins 要压缩的源,添加完成后自动关闭流
*/
public static void zip(OutputStream out, List<String> fileNames, List<InputStream> ins) throws IOException {
zip(out, fileNames.toArray(new String[0]), ins.toArray(new InputStream[0]));
}
/**
* 将文件流压缩到目标流中
*
* @param out 目标流,压缩完成自动关闭
* @param fileNames 流数据在压缩文件中的路径或文件名
* @param ins 要压缩的源,添加完成后自动关闭流
*/
public static void zip(File out, List<String> fileNames, List<InputStream> ins) throws IOException {
FileOutputStream outputStream = new FileOutputStream(out);
zip(outputStream, fileNames.toArray(new String[0]), ins.toArray(new InputStream[0]));
outputStream.flush();
}
/**
* 将文件流压缩到目标流中
*
* @param out 目标流,压缩完成自动关闭
* @param fileNames 流数据在压缩文件中的路径或文件名
* @param ins 要压缩的源,添加完成后自动关闭流
*/
public static void zip(OutputStream out, String[] fileNames, InputStream[] ins) throws IOException {
ZipOutputStream zipOutputStream = null;
try {
zipOutputStream = getZipOutputStream(out, DEFAULT_CHARSET);
zip(zipOutputStream, fileNames, ins);
} catch (IOException e) {
throw new IOException("压缩包导出失败!", e);
} finally {
IOUtils.closeQuietly(zipOutputStream);
}
}
/**
* 将文件流压缩到目标流中
*
* @param zipOutputStream 目标流,压缩完成不关闭
* @param fileNames 流数据在压缩文件中的路径或文件名
* @param ins 要压缩的源,添加完成后自动关闭流
* @throws IOException IO异常
*/
public static void zip(ZipOutputStream zipOutputStream, String[] fileNames, InputStream[] ins) throws IOException {
if (ArrayUtils.isEmpty(fileNames) || ArrayUtils.isEmpty(ins)) {
throw new IllegalArgumentException("文件名不能为空!");
}
if (fileNames.length != ins.length) {
throw new IllegalArgumentException("文件名长度与输入流长度不一致!");
}
for (int i = 0; i < fileNames.length; i++) {
add(ins[i], fileNames[i], zipOutputStream);
}
}
/**
* 添加文件流到压缩包,添加后关闭流
*
* @param in 需要压缩的输入流,使用完后自动关闭
* @param fileName 压缩的路径
* @param out 压缩文件存储对象
* @throws IOException IO异常
*/
private static void add(InputStream in, String fileName, ZipOutputStream out) throws IOException {
if (null == in) {
return;
}
try {
out.putNextEntry(new ZipEntry(fileName));
IOUtils.copy(in, out);
} catch (IOException e) {
throw new IOException(e);
} finally {
IOUtils.closeQuietly(in);
closeEntry(out);
}
}
/**
* 获得 {@link ZipOutputStream}
*
* @param out 压缩文件流
* @param charset 编码
* @return {@link ZipOutputStream}
*/
private static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) {
if (out instanceof ZipOutputStream) {
return (ZipOutputStream) out;
}
return new ZipOutputStream(out, DEFAULT_CHARSET);
}
/**
* 关闭当前Entry,继续下一个Entry
*
* @param out ZipOutputStream
*/
private static void closeEntry(ZipOutputStream out) {
try {
out.closeEntry();
} catch (IOException e) {
// ignore
}
}
}
测试代码如下:
@Test
void export5() throws IOException {
List<TblExcel> list = new ArrayList<>();
list.add(new TblExcel("张三", "abc"));
List<InputStream> ins = new ArrayList<>();
OutputStream out1 = new ByteArrayOutputStream();
OutputStream out2 = new ByteArrayOutputStream();
// 2、导出
EasyExcel.write(out1)
.sheet("第一个")
.head(ExcelExportVO.class)
.doWrite(list2);
ins.add(outputStream2InputStream(out1)); // 写法可参考上一个 hutool 的示例
EasyExcel.write(out2)
.sheet("第二个")
.head(ExcelExportVO.class)
.doWrite(list2);
ins.add(outputStream2InputStream(out2));
File zipFile = new File("C:\\Users\\乔\\Desktop\\noModelWrite.zip");
// 压缩包内流的文件名
List<String> paths = Arrays.asList("1.xlsx", "2.xlsx");
ZipUtil.zip(zipFile, paths, ins); // 工具类使用
}
/**
* 输出流转输入流;数据量过大请使用其他方法
*
* @param out
* @return
*/
private ByteArrayInputStream outputStream2InputStream(OutputStream out) {
Objects.requireNonNull(out);
ByteArrayOutputStream bos;
bos = (ByteArrayOutputStream) out;
return new ByteArrayInputStream(bos.toByteArray());
}
@Override
public void exportSettleZip(TestDTO dto, HttpServletResponse response) throws IOException {
// 1、参数校验
if (StringUtils.isEmpty(dto.getStartDay()) || StringUtils.isEmpty(dto.getEndDay())) {
throw new IllegalArgumentException("日期不能为空!!");
}
// 2、获取所有入驻企业的行业类型
List<String> filedTypes = FiledTypeEnum.getValues();
// 3、所有Excel导出并压缩
ByteArrayOutputStream zipStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOut = new ZipOutputStream(zipStream)) { // zipOut
for (String filedType : filedTypes) {
// 获取当前行业类型的入驻企业数据
List<TestVO> entSettleList = enterpriseMapper.getEntSettleList(dto, filedType);
// 创建一个字节流,用于存储当前行业类型的 Excel 数据
ByteArrayOutputStream excelStream = new ByteArrayOutputStream();
// 使用 EasyExcel 导出 Excel 数据
EasyExcel.write(excelStream)
.head(getHeadList(dto)) // 获取表头
.registerWriteHandler(new CustomColumnWidthStyleStrategy()) // 自适应列宽策略
.registerWriteHandler(new CustomMergeStrategy(Collections.singletonList(1))) // 单元格合并策略
.sheet(filedType + "模板") // 设置 Excel 表格名
.doWrite(entSettleList); // 写入 Excel 数据
// 将 Excel 写入 ZipOutputStream -- 这三行代码用于将一个 Excel 文件的数据写入到 ZIP 文件中
ZipEntry zipEntry = new ZipEntry(filedType + "信息导出.xlsx"); // 表示 ZIP 文件中的一个文件名称
zipOut.putNextEntry(zipEntry); // 将刚刚创建的 ZipEntry 对象添加到 ZipOutputStream 中,表示开始写入 ZIP 文件的一个新文件。
zipOut.write(excelStream.toByteArray()); // 将之前在内存中生成的 Excel 文件数据写入到 ZIP 文件中的当前条目
// 关闭当前 ZipEntry
zipOut.closeEntry();
// 关闭当前 Excel 字节流
excelStream.close();
}
}
// 4、将压缩包写入响应流
String start = dto.getStartDay().replaceAll("-", "");
String end = dto.getEndDay().replaceAll("-", "");
String zipName = "模板数据" + start + "-" + end + ".zip";
zipName = URLEncoder.encode(zipName, "UTF-8");
try {
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment;filename=" + zipName);
response.getOutputStream().write(zipStream.toByteArray()); // 重点关注
} finally {
// 关闭 ByteArrayOutputStream
zipStream.close();
}
}
代码用途
以下是对代码的详细解释
FiledTypeEnum.getValues()
获取所有的行业类型。(此方法在枚举类里面定义)ZipOutputStream
创建一个 ZIP 文件,然后遍历所有行业类型,为每个行业类型生成对应的 Excel 文件,并将其写入 ZIP 文件中。
ByteArrayOutputStream
用于存储当前行业类型的 Excel 数据。URLEncoder.encode
处理中文文件名。最后,关闭 ByteArrayOutputStream
。以下是一些关于流的概念说明
流(Stream)是用于在程序之间传输数据的抽象。流可以是输入流(Input Stream),用于从某个源读取数据,也可以是输出流(Output Stream),用于将数据写入某个目标。
现在来解释一下这段代码中各个流的用法:
EasyExcel.write
方法的参数是一个输出流,而 ByteArrayOutputStream
就是一个方便在内存中存储字节数据的流。ByteArrayOutputStream
的 close
方法,确保所有关联的资源被释放,尤其是关闭底层的字节数组。ZipOutputStream
将确保 ZIP 文件的完整性。在这里,通过 zipOut.closeEntry()
来关闭当前 ZIP 文件的条目(即一个文件),并准备开始下一个 ZIP 条目。ZipEntry
。在这段代码中,用于表示 ZIP 文件中的每个 Excel 文件。
ZipEntry
就是用于表示 ZIP 文件中的每个文件。ZipOutputStream
中,每次调用 putNextEntry
方法都会创建一个新的 ZipEntry
,表示一个新的文件。通过 zipOut.closeEntry()
来关闭当前 ZIP 条目,以确保下一次写入时不会影响到前一个 ZIP 条目。总体来说,这些流的使用是为了在【内存】中生成多个 Excel 文件,并将这些文件写入一个 ZIP 文件中,最终提供给用户进行下载。关闭流是为了释放资源,确保数据完整性。
使用easyExcel导入导出Date类型的转换问题 (mfbz.cn)
代码规范:禁用e.printStackTrace()打印异常_e.printstacktrace()禁用-CSDN博客
代码规范之e.printStackTrace()-CSDN博客
【温情提醒】工作中要少用e.printStackTrace()的致命原因之一_printtrace问题-CSDN博客
视频:Easy Excel 13:导出图片内容
使用 easyExcel 生成多个 excel 并打包成zip压缩包-CSDN博客