文章目录
- easyexcel案例之类型转换,合并单元格,批注,下拉框,导入校验
- 一、依赖
- 二、导出
- 1.类型转换导出
- 2.自定义文件标题
- 3.合并单元格导出
- 注解方式
- 通过 registerWriteHandler 方法注册进去
- 自定义合并规则进行合并
- 4.批注和下拉框导出
- 批注信息类
- 自定义批注处理器
- 自定义下拉框处理器
- 工具类
- 测试
- 三、导入
- 1、修改导入模板实体类
- 2、添加expiringmap依赖
- 3、自定义监听器处理导入数据
- 4、读取文件和错误信息下载
easyexcel案例之类型转换,合并单元格,批注,下拉框,导入校验
一、依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>
二、导出
1.类型转换导出
工具类
package com.hl.easyexcel.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
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.hl.easyexcel.util.ExcelListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
@Slf4j
public class ExcelUtil {
/**
* 导出数据为excel文件
*
* @param filename 文件名称
* @param dataResult 集合内的bean对象类型要与clazz参数一致
* @param clazz 集合内的bean对象类型要与clazz参数一致
* @param response HttpServlet响应对象
*/
public static void export(String filename, String sheetName,List<?> dataResult, Class<?> clazz, HttpServletResponse response) {
response.setStatus(200);
OutputStream outputStream = null;
try {
if (StringUtils.isBlank(filename)) {
throw new RuntimeException("'filename' 不能为空");
}
String fileName = filename.concat(".xlsx");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));
outputStream = response.getOutputStream();
EasyExcel.write(response.getOutputStream(), clazz)
.registerWriteHandler(getStyleStrategy())
// 导出文件名
.autoCloseStream(Boolean.TRUE).sheet(sheetName)
.doWrite(dataResult);
} catch (Exception e) {
log.error("导出excel数据异常:", e);
throw new RuntimeException(e);
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("导出数据关闭流异常", e);
}
}
}
}
/**
* 设置表格内容居中显示策略
*/
public static HorizontalCellStyleStrategy getStyleStrategy(){
// 这里需要设置不关闭流
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
//设置背景颜色
headWriteCellStyle.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex());
//设置头字体
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short)15);
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
//设置头居中
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
//内容策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
WriteFont headWriteFont2 = new WriteFont();
headWriteFont2.setFontHeightInPoints((short)13);
//设置 水平居中
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 垂直居中
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
}
}
导出实体类
package com.hl.easyexcel.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.format.NumberFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.hl.easyexcel.controller.CustomSexConverterHandler;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 资质信息导出实体
*/
@Data
@Accessors(chain = true) //Lombok注解,链式赋值使用
public class ExportExcelVo implements Serializable {
private static final long serialVersionUID = 1L;
@ColumnWidth(25)
@ExcelProperty(value = {"企业信息", "企业名称"}, index = 0)
private String name;
@ColumnWidth(25)
@ExcelProperty(value = {"企业信息", "社会统一信用代码"}, index = 1)
private String creditCode;
@ColumnWidth(15)
@ExcelProperty(value = {"企业信息","公司法人"}, index = 2)
private String legalPerson;
@ColumnWidth(15)
@ExcelProperty(value = {"企业信息","性别"}, index = 3,converter = CustomSexConverterHandler.class)
private Integer gender;
@ExcelProperty(value = {"企业信息","区域"}, index = 4)
private String province;
@ColumnWidth(25)
@DateTimeFormat("yyyy年MM月dd日 HH时mm分ss秒")
@ExcelProperty(value = {"企业信息","录入时间"}, index = 5)
private Date createTime;
@ColumnWidth(15)
@ExcelProperty(value = {"企业信息","公司股东"}, index = 6)
private String stockholder;
@ColumnWidth(25)
@ExcelProperty(value = {"企业信息","企业联系方式"}, index = 7)
private String contact;
@NumberFormat("#.##%")
@ColumnWidth(15)
@ExcelProperty(value = {"企业信息","增长收益"},index = 8)
private Double doubleData;
}
自定义性别类型转换器
package com.hl.easyexcel.controller;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* @ClassName CustomSexConverterHandler
* @Description 自定义性别类型转换器
* @Author hl
* @Date 2022/12/28 10:39
* @Version 1.0
*/
public class CustomSexConverterHandler implements Converter<Integer> {
@Override
public Class supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 导入
*/
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return "男".equals(cellData.getStringValue()) ? 1 : 0;
}
/**
* 导出
*/
@Override
public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
if (integer.equals(1)){
return new CellData("男");
}else {
return new CellData("女");
}
}
}
controller中的请求方法
/**
* excel数据导出
* @param size 导出条数, 也可以是用户需要导出数据的条件
*/
@RequestMapping(value = "/export", method = RequestMethod.GET)
public HttpResponseTemp<?> export(Long size, HttpServletResponse response){
// 模拟根据条件在数据库查询数据
ArrayList<ExportExcelVo> excelVos = new ArrayList<>();
for (int i = 1; i <= size; i++) {
ExportExcelVo excelVo = new ExportExcelVo();
excelVo.setContact(String.valueOf(10000000000L + i));
excelVo.setName("公司名称" + i);
excelVo.setCreditCode("社会性用代码" + i);
excelVo.setProvince("地区" + i);
excelVo.setLegalPerson("法人" + i);
excelVo.setStockholder("投资人" + i);
excelVo.setCreateTime(new Date());
if (i % 2 == 0){
excelVo.setGender(1);
}else {
excelVo.setGender(0);
}
excelVo.setDoubleData(0.11);
excelVos.add(excelVo);
}
String fileName = "数据导出" + System.currentTimeMillis();
try {
ExcelUtil.export(fileName,"数据", excelVos, ExportExcelVo.class, response);
} catch (Exception e) {
return ResultStat.SERVER_INTERNAL_ERROR.wrap("数据导出失败!" + e.getMessage());
}
return ResultStat.OK.wrap("数据导出成功");
}
2.自定义文件标题
自定义文件表头名称处理器,这里需要合并多少列需要看情况
package com.hl.easyexcel.controller;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
/**
* @ClassName CustomerSheetWriteHandler
* @Description 自定义文件表头名称
* @Author hl
* @Date 2022/12/28 13:58
* @Version 1.0
*/
public class CustomSheetTitleHandler implements SheetWriteHandler {
@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Workbook workbook = writeWorkbookHolder.getWorkbook();
Sheet sheet = workbook.getSheetAt(0);
//设置标题
Row row2 = sheet.createRow(0);
row2.setHeight((short) 800);
Cell cell1 = row2.createCell(0);
cell1.setCellValue("企业信息统计报表");
CellStyle cellStyle = workbook.createCellStyle();
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
cellStyle.setAlignment(HorizontalAlignment.CENTER);
cellStyle.setBorderLeft(BorderStyle.THIN);
cellStyle.setBorderRight(BorderStyle.THIN);
cellStyle.setBorderTop(BorderStyle.THIN);
cellStyle.setBorderBottom(BorderStyle.THIN);
cellStyle.setLocked(true);
Font font = workbook.createFont();
font.setBold(true);
font.setFontHeight((short) 400);
cellStyle.setFont(font);
cell1.setCellStyle(cellStyle);
//这里看有多少列就合并多少列
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 8));
}
}
请求接口和实体类
@RequestMapping(value = "/export3", method = RequestMethod.GET)
public HttpResponseTemp<?> export3(Long size, HttpServletResponse response){
// 模拟根据条件在数据库查询数据
ArrayList<ExportExcelVo2> excelVos = new ArrayList<>();
for (int i = 1; i <= size; i++) {
ExportExcelVo2 excelVo = new ExportExcelVo2();
excelVo.setContact(String.valueOf(10000000000L + i));
excelVo.setName("公司名称" + i);
excelVo.setCreditCode("社会性用代码" + i);
excelVo.setProvince("地区" + i);
excelVo.setLegalPerson("法人" + i);
excelVo.setStockholder("投资人" + i);
excelVo.setCreateTime(new Date());
if (i % 2 == 0){
excelVo.setGender(1);
}else {
excelVo.setGender(0);
}
excelVo.setDoubleData(0.11);
excelVos.add(excelVo);
}
String fileName = "数据导出" + System.currentTimeMillis();
try {
ExcelUtil.exportWithTitle(fileName,"数据", excelVos, ExportExcelVo2.class, response);
} catch (Exception e) {
return ResultStat.SERVER_INTERNAL_ERROR.wrap("数据导出失败!" + e.getMessage());
}
return ResultStat.OK.wrap("数据导出成功");
}
@Data
@Accessors(chain = true) //Lombok注解,链式赋值使用
public class ExportExcelVo2 implements Serializable {
private static final long serialVersionUID = 1L;
@ColumnWidth(25)
@ExcelProperty(value = {"企业名称"}, index = 0)
private String name;
@ColumnWidth(25)
@ExcelProperty(value = {"社会统一信用代码"}, index = 1)
private String creditCode;
@ColumnWidth(15)
@ExcelProperty(value = {"公司法人"}, index = 2)
private String legalPerson;
@ColumnWidth(15)
@ExcelProperty(value = {"性别"}, index = 3,converter = CustomSexConverterHandler.class)
private Integer gender;
@ExcelProperty(value = {"区域"}, index = 4)
private String province;
@ColumnWidth(25)
@DateTimeFormat("yyyy年MM月dd日 HH时mm分ss秒")
@ExcelProperty(value = {"录入时间"}, index = 5)
private Date createTime;
@ColumnWidth(15)
@ExcelProperty(value = {"公司股东"}, index = 6)
private String stockholder;
@ColumnWidth(25)
@ExcelProperty(value = {"企业联系方式"}, index = 7)
private String contact;
@NumberFormat("#.##%")
@ColumnWidth(15)
@ExcelProperty(value = {"增长收益"},index = 8)
private Double doubleData;
}
工具类
public static void exportWithTitle(String filename, String sheetName, ArrayList<ExportExcelVo2> dataResult, Class<ExportExcelVo2> clazz, HttpServletResponse response) {
response.setStatus(200);
OutputStream outputStream = null;
try {
if (StringUtils.isBlank(filename)) {
throw new RuntimeException("'filename' 不能为空");
}
String fileName = filename.concat(".xlsx");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));
outputStream = response.getOutputStream();
EasyExcel.write(response.getOutputStream(), clazz)
.registerWriteHandler(new CustomSheetTitleHandler())//自定义表头文件名称
.registerWriteHandler(getStyleStrategy())
.relativeHeadRowIndex(1)//标题占了一行
// 导出文件名
.autoCloseStream(Boolean.TRUE).sheet(sheetName)
.doWrite(dataResult);
} catch (Exception e) {
log.error("导出excel数据异常:", e);
throw new RuntimeException(e);
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("导出数据关闭流异常", e);
}
}
}
}
3.合并单元格导出
版本 2.2.6+
注解方式
注解这里只能使用一种,不可以合并多个,要合并多个使用下面那种方式
@Data
@Accessors(chain = true) //Lombok注解,链式赋值使用
// 将第3-7行的8列合并成一个单元格
@OnceAbsoluteMerge(firstRowIndex = 2, lastRowIndex = 6, firstColumnIndex = 8, lastColumnIndex = 8)
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class ExportExcelVo implements Serializable {}
通过 registerWriteHandler 方法注册进去
EasyExcel.write(response.getOutputStream(), clazz)
.registerWriteHandler(getStyleStrategy())
//将第3-7行的8列合并成一个单元格
.registerWriteHandler(new OnceAbsoluteMergeStrategy(2,6,8,8))
//将第6-7行的2-3列合并成一个单元格
.registerWriteHandler(new OnceAbsoluteMergeStrategy(5,6,1,2))
// 导出文件名
.autoCloseStream(Boolean.TRUE).sheet(sheetName)
.doWrite(dataResult);
自定义合并规则进行合并
自定义合并策略
package com.hl.easyexcel.controller;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.List;
public class ExcelFillCellMergeStrategy implements CellWriteHandler {
private int[] mergeColumnIndex;
private int mergeRowIndex;
public ExcelFillCellMergeStrategy() {
}
public ExcelFillCellMergeStrategy(int mergeRowIndex, int[] mergeColumnIndex) {
this.mergeRowIndex = mergeRowIndex;
this.mergeColumnIndex = mergeColumnIndex;
}
public int[] getMergeColumnIndex() {
return mergeColumnIndex;
}
public void setMergeColumnIndex(int[] mergeColumnIndex) {
this.mergeColumnIndex = mergeColumnIndex;
}
public int getMergeRowIndex() {
return mergeRowIndex;
}
public void setMergeRowIndex(int mergeRowIndex) {
this.mergeRowIndex = mergeRowIndex;
}
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {
//当前行
int curRowIndex = cell.getRowIndex();
//当前列
int curColIndex = cell.getColumnIndex();
if (curRowIndex > mergeRowIndex) {
for (int columnIndex : mergeColumnIndex) {
if (curColIndex == columnIndex) {
mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex);
break;
}
}
}
}
/**
* 当前单元格向上合并
* @param cell 当前单元格
* @param curRowIndex 当前行
* @param curColIndex 当前列
*/
private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) {
//获取当前行的当前列的数据和上一行的当前列列数据,通过上一行数据是否相同进行合并
Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
// 比较当前行的第一列的单元格与上一行是否相同,相同合并当前单元格与上一行
//
if (curData.equals(preData)) {
Sheet sheet = writeSheetHolder.getSheet();
List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();
boolean isMerged = false;
for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {
CellRangeAddress cellRangeAddr = mergeRegions.get(i);
// 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
sheet.removeMergedRegion(i);
cellRangeAddr.setLastRow(curRowIndex);
sheet.addMergedRegion(cellRangeAddr);
isMerged = true;
}
}
// 若上一个单元格未被合并,则新增合并单元
if (!isMerged) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
sheet.addMergedRegion(cellRangeAddress);
}
}
}
}
工具类
public static void exportMerge(String filename, String sheetName,List<?> dataResult, Class<?> clazz, HttpServletResponse response,ExcelFillCellMergeStrategy excelFillCellMergeStrategy) {
response.setStatus(200);
OutputStream outputStream = null;
try {
if (StringUtils.isBlank(filename)) {
throw new RuntimeException("'filename' 不能为空");
}
String fileName = filename.concat(".xlsx");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));
outputStream = response.getOutputStream();
EasyExcel.write(response.getOutputStream(), clazz)
.registerWriteHandler(getStyleStrategy())
.registerWriteHandler(excelFillCellMergeStrategy)//合并策略
// 导出文件名
.autoCloseStream(Boolean.TRUE).sheet(sheetName)
.doWrite(dataResult);
} catch (Exception e) {
log.error("导出excel数据异常:", e);
throw new RuntimeException(e);
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("导出数据关闭流异常", e);
}
}
}
}
测试
@RequestMapping(value = "/export2", method = RequestMethod.GET)
public HttpResponseTemp<?> export2(Long size, HttpServletResponse response){
// 模拟根据条件在数据库查询数据
ArrayList<ExportExcelVo> excelVos = new ArrayList<>();
for (int i = 1; i <= size; i++) {
ExportExcelVo excelVo = new ExportExcelVo();
excelVo.setContact(String.valueOf(10000000000L + i));
excelVo.setName("公司名称" + i);
excelVo.setCreditCode("社会性用代码" + i);
excelVo.setProvince("地区" + i);
excelVo.setLegalPerson("法人" + i);
excelVo.setStockholder("投资人" + i);
excelVo.setCreateTime(new Date());
if (i == 1 || i ==2){
excelVo.setGender(1);
excelVo.setDoubleData(0.11);
}else {
excelVo.setGender(0);
excelVo.setDoubleData(0.32);
}
excelVos.add(excelVo);
}
String fileName = "数据导出" + System.currentTimeMillis();
try {
//需要合并的列
int[] mergeColumnIndex = {3,8};
//从第几行后开始合并
int mergeRowIndex = 2;
//自定义合并策略
ExcelFillCellMergeStrategy excelFillCellMergeStrategy = new ExcelFillCellMergeStrategy(mergeRowIndex, mergeColumnIndex);
ExcelUtil.exportMerge(fileName,"数据", excelVos, ExportExcelVo.class, response,excelFillCellMergeStrategy);
} catch (Exception e) {
return ResultStat.SERVER_INTERNAL_ERROR.wrap("数据导出失败!" + e.getMessage());
}
return ResultStat.OK.wrap("数据导出成功");
}
4.批注和下拉框导出
批注信息类
package com.hl.easyexcel.controller.anno;
import lombok.Data;
/**
* 批注信息类
*
* @author xudongmaster
*/
@Data
public class CommentModel {
/**
* sheet页名称
*/
private String sheetName;
/**
* 列索引
*/
private int colIndex;
/**
* 行索引
*/
private int rowIndex;
/**
* 行索引
*/
private String commentContent;
/**
* 生成批注信息
*
* @param sheetName sheet页名称
* @param rowIndex 行号
* @param columnIndex 列号
* @param commentContent 批注内容
* @return
*/
public static CommentModel createCommentModel(String sheetName, int rowIndex, int columnIndex, String commentContent) {
CommentModel commentModel = new CommentModel();
//sheet页名称
commentModel.setSheetName(sheetName);
//行号
commentModel.setRowIndex(rowIndex);
//列号
commentModel.setColIndex(columnIndex);
//批注内容
commentModel.setCommentContent(commentContent);
return commentModel;
}
}
自定义批注处理器
package com.hl.easyexcel.controller.anno;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.write.handler.AbstractRowWriteHandler;
import com.alibaba.excel.write.metadata.holder.*;
import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
import org.apache.poi.hssf.usermodel.HSSFRichTextString;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* 自定义批注处理器
*
* @author xudongmaster
*/
public class CommentWriteHandler extends AbstractRowWriteHandler {
/**
* sheet页名称列表
*/
private List<String> sheetNameList;
List<CommentModel> commentList = new ArrayList<>();
/**
* 自定义批注适配器构造方法
*
* @param commentList 批注信息
* @param extension 文件后缀(xlsx、xls)
*/
public CommentWriteHandler(List<CommentModel> commentList, String extension) {
if (CollUtil.isEmpty(commentList)) {
return;
}
//文件不为指定的格式时,默认为Xlsx
if (!StrUtil.equals(extension, "xlsx") && !StrUtil.equals(extension, "xls")) {
extension = "xlsx";
}
this.commentList = commentList.stream().filter(x ->
StrUtil.isNotBlank(x.getSheetName()) && x.getColIndex() >=0 && x.getRowIndex() >= 0 && StrUtil.isNotBlank(x.getCommentContent())
).collect(Collectors.toList());
sheetNameList = this.commentList.stream().map(CommentModel::getSheetName).distinct().collect(Collectors.toList());
this.extension = extension;
}
/**
* 文档后缀名
*/
private String extension;
@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
Integer relativeRowIndex, Boolean isHead) {
Sheet sheet = writeSheetHolder.getSheet();
//不需要添加批注,或者当前sheet页不需要添加批注
if (CollUtil.isEmpty(commentList) || !sheetNameList.contains(sheet.getSheetName())) {
return;
}
//获取当前行的批注信息
List<CommentModel> rowCommentList = commentList.stream().filter(x ->
StrUtil.equals(x.getSheetName(), sheet.getSheetName())
&& Objects.equals(relativeRowIndex, x.getRowIndex())).collect(Collectors.toList());
//当前行没有批注信息
if (CollUtil.isEmpty(rowCommentList)) {
return;
}
List<Integer> colIndexList = rowCommentList.stream().map(CommentModel::getColIndex).distinct().collect(Collectors.toList());
for (Integer colIndex : colIndexList) {
//同一单元格的批注信息
List<CommentModel> cellCommentList = rowCommentList.stream().filter(x ->
Objects.equals(colIndex, x.getColIndex())).collect(Collectors.toList());
if (CollUtil.isEmpty(cellCommentList)) {
continue;
}
//批注内容拼成一条
String commentContent = cellCommentList.stream().map(CommentModel::getCommentContent).collect(Collectors.joining());
Cell cell = row.getCell(colIndex);
addComment(cell, commentContent, extension);
}
//删除批注信息
commentList.remove(rowCommentList);
//重新获取要添加的sheet页姓名
sheetNameList = commentList.stream().map(CommentModel::getSheetName).distinct().collect(Collectors.toList());
}
/**
* 给Cell添加批注
*
* @param cell 单元格
* @param value 批注内容
* @param extension 扩展名
*/
public static void addComment(Cell cell, String value, String extension) {
Sheet sheet = cell.getSheet();
cell.removeCellComment();
if ("xls".equals(extension)) {
ClientAnchor anchor = new HSSFClientAnchor();
// 关键修改
anchor.setDx1(0);
anchor.setDx2(0);
anchor.setDy1(0);
anchor.setDy2(0);
anchor.setCol1(cell.getColumnIndex());
anchor.setRow1(cell.getRowIndex());
anchor.setCol2(cell.getColumnIndex() + 3);
anchor.setRow2(cell.getRowIndex() + 4);
// 结束
Drawing<?> drawing = sheet.createDrawingPatriarch();
Comment comment = drawing.createCellComment(anchor);
// 输入批注信息
comment.setString(new HSSFRichTextString(value));
// 将批注添加到单元格对象中
cell.setCellComment(comment);
} else if ("xlsx".equals(extension)) {
ClientAnchor anchor = new XSSFClientAnchor();
// 关键修改
anchor.setDx1(0);
anchor.setDx2(0);
anchor.setDy1(0);
anchor.setDy2(0);
anchor.setCol1(cell.getColumnIndex());
anchor.setRow1(cell.getRowIndex());
anchor.setCol2(cell.getColumnIndex() + 3);
anchor.setRow2(cell.getRowIndex() + 4);
// 结束
Drawing<?> drawing = sheet.createDrawingPatriarch();
Comment comment = drawing.createCellComment(anchor);
// 输入批注信息
comment.setString(new XSSFRichTextString(value));
// 将批注添加到单元格对象中
cell.setCellComment(comment);
}
}
}
自定义下拉框处理器
package com.hl.easyexcel.controller;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import java.util.Map;
import java.util.TreeMap;
/**
* 这个类的作用主要是给列增加下拉框
* 主要是为了方便用户填写数据
*/
public class CustomSheetSelectHandler implements SheetWriteHandler {
/**
* 存放下拉内容的集合
* key为列的下标, value为下拉内容数组
*/
private final Map<Integer, String[]> map = new TreeMap<>();
/**
* 工作簿下标,从0开始
*/
private int index = 0;
/**
* 给多少行添加下拉框,这里默认给2000行
*/
private final int batchSize = 2000;
@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
}
/**
* 宝藏在此:如果下拉框内容总的长度超过255,会导致Cell有下拉框,但是下拉内容显示不了,
* 这时我们可以新建一个sheet,将其隐藏,然后将里面的内容引用到我们的下拉框列就可以。
* 值得细品
*/
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// excel下标从0开始,这里第二列的下拉选择内容
map.put(2, new String[]{"张三", "李四"});
// excel下标从0开始,这里第三列的下拉选择内容
map.put(3, new String[]{"男", "女"});
DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper();
map.forEach((k, v) -> {
// 创建sheet,突破下拉框255的限制
// 获取一个workbook
Workbook workbook = writeWorkbookHolder.getWorkbook();
// 定义sheet的名称
String sheetName = "sheet" + k;
// 1.创建一个隐藏的sheet 名称为 proviceSheet
Sheet proviceSheet = workbook.createSheet(sheetName);
// 从第二个工作簿开始隐藏
this.index++;
// 设置隐藏
workbook.setSheetHidden(this.index, true);
// 2.循环赋值(为了防止下拉框的行数与隐藏域的行数相对应,将隐藏域加到结束行之后)
for (int i = 0, length = v.length; i < length; i++) {
// i:表示你开始的行数 0表示你开始的列数
proviceSheet.createRow(i).createCell(0).setCellValue(v[i]);
}
Name category1Name = workbook.createName();
category1Name.setNameName(sheetName);
// 4 $A$1:$A$N代表 以A列1行开始获取N行下拉数据
category1Name.setRefersToFormula(sheetName + "!$A$1:$A$" + (v.length));
// 5 将刚才设置的sheet引用到你的下拉列表中,1表示从行的序号1开始(开始行,通常行的序号为0的行是表头),50表示行的序号50(结束行),表示从行的序号1到50,k表示开始列序号和结束列序号
CellRangeAddressList addressList = new CellRangeAddressList(1, batchSize, k, k);
DataValidationConstraint constraint8 = helper.createFormulaListConstraint(sheetName);
DataValidation dataValidation3 = helper.createValidation(constraint8, addressList);
// 阻止输入非下拉选项的值
dataValidation3.setErrorStyle(DataValidation.ErrorStyle.STOP);
dataValidation3.setShowErrorBox(true);
dataValidation3.setSuppressDropDownArrow(true);
dataValidation3.createErrorBox("提示", "此值与单元格定义格式不一致");
// validation.createPromptBox("填写说明:","填写内容只能为下拉数据集中的单位,其他单位将会导致无法入仓");
writeSheetHolder.getSheet().addValidationData(dataValidation3);
});
}
}
工具类
public static void exportTemplate(String filename, String sheetName,List<?> dataResult, Class<?> clazz, HttpServletResponse response) {
response.setStatus(200);
OutputStream outputStream = null;
try {
if (StringUtils.isBlank(filename)) {
throw new RuntimeException("'filename' 不能为空");
}
String fileName = filename.concat(".xlsx");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));
outputStream = response.getOutputStream();
List<CommentModel> commentList = new ArrayList<>();
commentList.add(CommentModel.createCommentModel(sheetName, 1, 0, "第一条批注。"));
commentList.add(CommentModel.createCommentModel(sheetName, 1, 1, "第二条批注。"));
EasyExcel.write(response.getOutputStream(), clazz)
.registerWriteHandler(getStyleStrategy())
.registerWriteHandler(new CommentWriteHandler(commentList,"xlsx"))
.registerWriteHandler(new CustomSheetSelectHandler())
// 导出文件名
.autoCloseStream(Boolean.TRUE).sheet(sheetName)
.doWrite(dataResult);
} catch (Exception e) {
log.error("导出excel数据异常:", e);
throw new RuntimeException(e);
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("导出数据关闭流异常", e);
}
}
}
}
测试
@RequestMapping(value = "/template", method = RequestMethod.GET)
public HttpResponseTemp<?> template(HttpServletResponse response){
String fileName = "导入模板下载" + System.currentTimeMillis();
try {
ExcelUtil.exportTemplate(fileName,"模板", null, ImportExcelVo.class, response);
} catch (Exception e) {
return ResultStat.SERVER_INTERNAL_ERROR.wrap("模板下载失败" + e.getMessage());
}
return ResultStat.OK.wrap("模板下载成功!");
}
@Data
public class ImportExcelVo implements Serializable {
private static final long serialVersionUID = 1L;
@ColumnWidth(25)
@ExcelProperty(value = {"企业信息", "企业名称"}, index = 0)
private String name;
@ColumnWidth(25)
@ExcelProperty(value = {"企业信息", "社会统一信用代码"}, index = 1)
private String creditCode;
@ColumnWidth(15)
@ExcelProperty(value = {"企业信息","公司法人"}, index = 2)
private String legalPerson;
@ColumnWidth(15)
@ExcelProperty(value = {"企业信息","性别"}, index = 3,converter = CustomSexConverterHandler.class)
private Integer gender;
@ExcelProperty(value = {"企业信息","区域"}, index = 4)
private String province;
@ColumnWidth(25)
@DateTimeFormat("yyyy年MM月dd日 HH时mm分ss秒")
@ExcelProperty(value = {"企业信息","录入时间"}, index = 5)
private Date createTime;
@ColumnWidth(15)
@ExcelProperty(value = {"企业信息","公司股东"}, index = 6)
private String stockholder;
@ColumnWidth(25)
@ExcelProperty(value = {"企业信息","企业联系方式"}, index = 7)
private String contact;
}
三、导入
1、修改导入模板实体类
添加字段rowNum记录是哪一行数据,和重写equals和hashCode方法判断数据是否重复
@Data
public class ImportExcelVo implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelIgnore
private Integer rowNum;
@ColumnWidth(25)
@ExcelProperty(value = {"企业名称"}, index = 0)
private String name;
@ColumnWidth(25)
@ExcelProperty(value = {"社会统一信用代码"}, index = 1)
private String creditCode;
@ColumnWidth(15)
@ExcelProperty(value = {"公司法人"}, index = 2)
private String legalPerson;
@ColumnWidth(15)
@ExcelProperty(value = {"性别"}, index = 3,converter = CustomSexConverterHandler.class)
private Integer gender;
@ExcelProperty(value = {"区域"}, index = 4)
private String province;
@ColumnWidth(25)
@DateTimeFormat("yyyy年MM月dd日 HH时mm分ss秒")
@ExcelProperty(value = {"录入时间"}, index = 5)
private Date createTime;
@ColumnWidth(15)
@ExcelProperty(value = {"公司股东"}, index = 6)
private String stockholder;
@ColumnWidth(25)
@ExcelProperty(value = {"企业联系方式"}, index = 7)
private String contact;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImportExcelVo that = (ImportExcelVo) o;
return Objects.equals(name, that.name) && Objects.equals(creditCode, that.creditCode) && Objects.equals(legalPerson, that.legalPerson) && Objects.equals(gender, that.gender) && Objects.equals(province, that.province) && Objects.equals(createTime, that.createTime) && Objects.equals(stockholder, that.stockholder) && Objects.equals(contact, that.contact);
}
@Override
public int hashCode() {
return Objects.hash(name, creditCode, legalPerson, gender, province, createTime, stockholder, contact);
}
}
2、添加expiringmap依赖
添加expiringmap依赖,用来解决,导入错误信息,放入一个有时间限制的map中,然后进行下载错误信息
<dependency>
<groupId>net.jodah</groupId>
<artifactId>expiringmap</artifactId>
<version>0.5.10</version>
</dependency>
expiringmap的使用
private static final ExpiringMap<String,List<ImportErrorInfo>> expiryFileMap = ExpiringMap.builder()
.maxSize(100)
.expiration(180, TimeUnit.SECONDS)
.expirationPolicy(ExpirationPolicy.ACCESSED)
.variableExpiration()
.build();
3、自定义监听器处理导入数据
package com.hl.easyexcel.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.hl.easyexcel.domain.ImportErrorInfo;
import com.hl.easyexcel.domain.ImportExcelVo;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
public final class ExcelListener extends AnalysisEventListener<ImportExcelVo> {
/**
* 此线程变量用来存储错误提示,只是提示表头是否符合模板要求
*/
public static final ThreadLocal<Exception> RESP = new ThreadLocal<>();
/**
* 处理重复数据
*/
private static final ThreadLocal<List<ImportExcelVo>> threadLocal = new ThreadLocal<>();
/**
* 批处理阈值1000
*/
private static final int BATCH_COUNT = 1000;
/**
* 自定义用于暂时存储data
* 可以通过实例获取该值
*/
private final List<ImportExcelVo> dataList = new ArrayList<>();
/**
* 返回提示语
*/
private final List<ImportErrorInfo> errorInfoList = new ArrayList<>();
/**
* 第几行有错误,这里根据表头来进行判断
*/
private int row = 1;
/**
* 重写invokeHeadMap方法,校验表头
*headMap的key是表头下标,value是内容
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// headMap.containsKey(下标),先判空
// headMap.get(0).equals() 判内容是否相符
log.info(JSONUtil.toJsonStr(headMap));
if ((!headMap.containsKey(0) || !headMap.get(0).trim().equals("企业名称"))
|| (!headMap.containsKey(1) || !headMap.get(1).trim().equals("社会统一信用代码"))
|| (!headMap.containsKey(2) || !headMap.get(2).trim().equals("公司法人"))
|| (!headMap.containsKey(3) || !headMap.get(3).trim().equals("性别"))
|| (!headMap.containsKey(4) || !headMap.get(4).trim().equals("区域"))
|| (!headMap.containsKey(5) || !headMap.get(5).trim().equals("录入时间"))
|| (!headMap.containsKey(6) || !headMap.get(6).trim().equals("公司股东"))
|| (!headMap.containsKey(7) || !headMap.get(7).trim().equals("企业联系方式")) ){
RESP.set(new RuntimeException("表头错误"));
}
}
/**
* 具体校验逻辑
*/
@Override
public void invoke(ImportExcelVo importExcelVo, AnalysisContext analysisContext) {
log.info(importExcelVo.toString());
//给每条数据设置行数
importExcelVo.setRowNum(++row);
//检查重复数据
checkDuplicateData(importExcelVo,errorInfoList);
//校验参数
Integer gender = importExcelVo.getGender();
if (gender != 0){
saveError(importExcelVo.getRowNum(),"性别不能为男",errorInfoList);
}
//存入数据
dataList.add(importExcelVo);
}
/**
* excel读取完都会调用此方法
*/
@SneakyThrows
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (ObjectUtil.isNotNull(RESP) && ObjectUtil.isNotNull(RESP.get())){
try {
throw RESP.get();
} finally {
//最后清空线程变量的值
RESP.remove();
}
}
//解析结束销毁不用的资源
//注意不要调用datas.clear(),否则getDatas为null
// 获取总行数(含表头)
Integer totalRowNum = context.readSheetHolder().getApproximateTotalRowNumber();
totalRowNum = totalRowNum-1;
if(totalRowNum.compareTo(0) == 0){
throw new RuntimeException("数据为空");
}
if(totalRowNum > BATCH_COUNT){
throw new RuntimeException("excel中的数据条数大于1000条");
}
}
private void checkDuplicateData(ImportExcelVo inputEntity, List<ImportErrorInfo> errorInfoList) {
List<ImportExcelVo> threadLocalVal = threadLocal.get();
if (CollUtil.isEmpty(threadLocalVal)) {
threadLocalVal = new ArrayList<>();
}
threadLocalVal.forEach(e -> {
if (e.equals(inputEntity)) {
ImportErrorInfo errorInfo = new ImportErrorInfo();
errorInfo.setLine(inputEntity.getRowNum());
errorInfo.setMsg("数据与第" + e.getRowNum() + "行重复");
errorInfoList.add(errorInfo);
}
});
// 添加本行数据对象到ThreadLocal中
threadLocalVal.add(inputEntity);
threadLocal.set(threadLocalVal);
}
/**
* 保存提示信息到集合
* @param rowIndex 行数
* @param desc 提示信息
* @param errorInfoList 存入集合
*/
private void saveError(Integer rowIndex, String desc, List<ImportErrorInfo> errorInfoList) {
ImportErrorInfo errorInfo = new ImportErrorInfo();
errorInfo.setLine(rowIndex);
errorInfo.setMsg(desc);
errorInfoList.add(errorInfo);
}
/**
* 返回数据
* @return 返回读取的数据集合
**/
public List<ImportExcelVo> getDataList() {
return dataList;
}
/**
* 返回数据
* @return 返回提示集合
**/
public List<ImportErrorInfo> getErrorInfoList() {
return errorInfoList;
}
/**
* 返回重复数据
* @return 返回提示集合
*/
public ThreadLocal<List<ImportExcelVo>> getThreadLocal() {
return threadLocal;
}
}
4、读取文件和错误信息下载
private static final ExpiringMap<String,List<ImportErrorInfo>> expiryFileMap = ExpiringMap.builder()
.maxSize(100)
.expiration(180, TimeUnit.SECONDS)
.expirationPolicy(ExpirationPolicy.ACCESSED)
.variableExpiration()
.build();
/**
* 导入
*/
@PostMapping("/import")
@ResponseBody
public HttpResponseTemp<?> importManualEntry(@RequestParam(value = "file") MultipartFile file){
ExcelListener listener = new ExcelListener();
try {
//headRowNumber(2)从第3行开始读取,使用getDataList()方法取出数据
EasyExcel.read(file.getInputStream(), ImportExcelVo.class,listener).headRowNumber(1).sheet(0).doRead();
}catch (Exception e){
throw new RuntimeException(e);
}finally {
// 清除threadLocal 防止内存泄漏
ThreadLocal<List<ImportExcelVo>> threadLocal = listener.getThreadLocal();
if (threadLocal != null) {
threadLocal.remove();
}
}
//正确信息
List<ImportExcelVo> excelList = listener.getDataList();
//基础校验有错误,则直接返回错误信息
List<ImportErrorInfo> errorInfoList = listener.getErrorInfoList();
if(ObjectUtil.isNotEmpty(errorInfoList)){
errorInfoList.sort(Comparator.comparing(ImportErrorInfo::getLine));
String uuid = IdUtil.randomUUID();
expiryFileMap.put(uuid,errorInfoList);
return ResultStat.SERVER_INTERNAL_ERROR.wrap("数据导入失败:" + uuid);
}else {
System.out.println(excelList);
return ResultStat.OK.wrap("数据导入完成");
}
}
@GetMapping("/downloadErrorFile/{fileId}")
public void downloadErrorFile(@PathVariable String fileId,HttpServletResponse response){
//从存储错误信息的map中把数据取出来,然后再下载导出错误文件模板
List<ImportErrorInfo> dataList = expiryFileMap.get(fileId);
if (CollUtil.isNotEmpty(dataList)) {
String name = "导入错误信息";
ExcelUtil.export(name,"数据", dataList, ImportErrorInfo.class, response);
} else {
throw ApiException.wrapMessage(ResultStat.SERVER_INTERNAL_ERROR,"文件不存在");
}
}
导入文件样式
下载的错误信息