假设现在有这样一个需求:
1) 批量导入用户,需要校验用户的信息
2) 如果有错误的数据支持导出,有错误信息的单元格用特殊颜色标出,并将错误信息设置在单元格批注里
针对以上需求,笔者对EasyPoi进行了封装,下面将依次介绍
excel导入数据如下图
EasyPoi支持hibernate-validator注解式校验,如下图
如果要获取校验没通过的错误信息及行号需要实现IExcelDataModel和IExcelModel接口。这些都是基本校验,在实际开发过程中可能会遇到需要写代码来校验,比如校验用户名是否重复,EasyPoi给出了校验处理器,可以实现自定义校验。
@Component
public class UserImportVerifyHandler implements IExcelVerifyHandler {
@Resource
private UserMapper userMapper;
@Override
public ExcelVerifyHandlerResult verifyHandler(UserExcel excelBo) {
StringJoiner joiner = new StringJoiner(",");
UserInfo userInfo = userMapper.findByName(excelBo.getUserName());
if (userInfo != null) {
joiner.add("用户名不允许重复#0");
}
return new ExcelVerifyHandlerResult(false, joiner.toString());
}
}
以上校验需要在导入参数里开启
public void importUser(MultipartFile file) {
//导入参数
ImportParams params = new ImportParams();
//开启校验
params.setNeedVerify(true);
//校验处理器
params.setVerifyHandler(verifyHandler);
//调用模板方法导入excel
this.importExcel(file, UserExcel.class, params);
}
在上面的校验信息里面加了后缀,比如用户名不允许重复#0,这里面的#0是用来标识错误信息所在的列,0就表示列,#是方便将0截取出来,在导出的时候会用到。读者可以采用其它的方式,比如将错误信息与列做个映射。一整行的错误信息都会存到errorMsg这个字段里,并用“,”分隔。
下面看下封装的抽象类
public abstract class AbstractImportService {
@Resource
private TransactionalHelper transactionalHelper;
/**
* 导入excel
* @param file 文件
* @param pojoClass excel模板类
* @param params excel导入参数
*/
public void importExcel(MultipartFile file, Class> pojoClass, ImportParams params) {
ExcelImportResult result = null;
try {
//调用EasyPoi的导入接口
result = ExcelImportUtil.importExcelMore(file.getInputStream(), pojoClass, params);
} catch (Exception e) {
//此处抛异常
}
if (result != null) {
this.checkTitleCell(result, params.getTitleRows(), reqDto.getTitleCells());
if (!CollectionUtils.isEmpty(result.getList())) {
this.findDuplicate(result.getList(), result.getFailList());
}
//开启事务保存数据,这种用法主要是为了解决事务失效的问题,见下面描述
transactionalHelper.apply(this::saveData, result);
}
}
//校验标题格式是否正确
private void checkTitleCell(ExcelImportResult result, int titleRows, int titleCells) {
Row row = result.getWorkbook().getSheetAt(0).getRow(titleRows);
if (row.getLastCellNum() < titleCells) {
//此处抛异常
}
for (int i=0; i importBos, List failList);
//该抽象方法保存解析出来的数据
protected abstract void saveData(ExcelImportResult result);
}
代码中简化了很多东西,读者可以自己去细化。代码中用了一个TransactionalHelper,参见解决事务失效的工具类
导出采用模板方式导出,实体类还是用上文中的UserExcel。导出只介绍封装的抽象类
public abstract class AbstractExportService {
/**
* 导出excel
* @param dataList 需要导出的数据,继承该抽象类后自行获取
* @param templateUrl 模板路径
* @param fileName 文件名
* @param startRows 存放实际数据的开始行,添加批注时需要传该值
* @param hasComment 是否有批注
*/
protected void exportExcel(List dataList, String templateUrl, String fileName, int startRows, boolean hasComment) {
Map resMap = new HashMap<>();
resMap.put("mapList", dataList);
try {
ClassPathResource classPathResource = new ClassPathResource(
templateUrl);
TemplateExportParams params = new TemplateExportParams(
classPathResource.getPath(), true);
Workbook workbook = ExcelExportUtil.exportExcel(params,resMap);
this.buildComment(dataList, workbook, startRows, hasComment);
//将workbook写入到response里,读者自行实现
}catch (Exception e){
//此处抛异常
}
}
private void buildComment(List dataList, Workbook workbook, int startRows, boolean hasComment) {
if (!hasComment) return;
Sheet sheet = workbook.getSheetAt(0);
//创建一个图画工具
Drawing> drawing = sheet.createDrawingPatriarch();
for (T fail : dataList) {
Row row = sheet.getRow(startRows);
//获取批注信息
String commentStr = this.getCommentStr(fail);
if (StringUtils.isNotBlank(commentStr)) {
//解析批注,并传换成map
Map commentMap = this.getCommentMap(commentStr);
for (Map.Entry entry : commentMap.entrySet()) {
Cell cell = row.getCell(entry.getKey());
//创建批注
Comment comment = drawing.createCellComment(this.newClientAnchor(workbook));
//输入批注信息
comment.setString(this.newRichTextString(workbook, entry.getValue()));
//将批注添加到单元格对象中
cell.setCellComment(comment);
//设置单元格背景颜色
CellStyle cellStyle = workbook.createCellStyle();
//设置颜色
cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
//设置实心填充
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cell.setCellStyle(cellStyle);
}
}
startRows++;
}
}
/**
* 批注信息,默认解析:批注#列索引,比如用户名不允许重复#0。可覆盖此方法,解析自定义的批注格式
* @param commentStr 当前行的所有批注信息
* @return key:列索引,value:对应列的所有批注信息
*/
protected Map getCommentMap(String commentStr) {
//每行的所有单元格的批注都在commentStr里,并用”,”分隔
String[] split = commentStr.split(",");
Map commentMap = new HashMap<>();
for (String msg : split) {
String[] cellMsg = msg.split("#");
//如果当前列没有批注,会将该列的索引作为key存到map里;已有批注,以“,“分隔继续拼接
int cellIndex = Integer.parseInt(cellMsg[1]);
if (commentMap.get(cellIndex) == null) {
commentMap.put(cellIndex, cellMsg[0]);
} else {
commentMap.replace(cellIndex, commentMap.get(cellIndex) + "," + cellMsg[0]);
}
}
return commentMap;
}
private ClientAnchor newClientAnchor(Workbook workbook) {
//xls
if (workbook instanceof HSSFWorkbook) {
return new HSSFClientAnchor(0, 0, 0, 0, (short) 3, 3, (short) 5, 6);
}
//xlsx
else {
return new XSSFClientAnchor(0, 0, 0, 0, (short) 3, 3, (short) 5, 6);
}
}
private RichTextString newRichTextString(Workbook workbook, String msg) {
//xls
if (workbook instanceof HSSFWorkbook) {
return new HSSFRichTextString(msg);
}
//xlsx
else {
return new XSSFRichTextString(msg);
}
}
/**
* 获取批注信息
* @param data
* @return
*/
protected abstract String getCommentStr(T data);