目录
EasyExcel工具类
pom导入依赖
1.相关代码
1.1 EasyExcel导出工具类
1.2CommonListener导入侦听器
1.3CustomMergeStrategy 合并策略
1.4 DownHandler 下拉处理
1.5 CustomMerge 合并单元格的注解
1.6 FieldDispose 导入自定义解析注解
1.7 DropDown 下拉注解
1.8 ExcelFieldType自定义数据解析常量
2 .使用方法
2.1 导出导入实体--注解使用
2.2 Controller
2.3 service 调用
2.4 自定义批量方法
项目中常见的导出 一般使用 EasyExcel/POI,这里简单的列出了easyExcel 的用,easyExcel 使用简单方便,内存占用低,不会导致oom,不过导入导出要求多的话,写解析器也难受,哎本来写了很多的,结果好多找不到,然后暂时又没用到。在这里取出了常用的一些工具类,导出添加下拉框,单元格合并,多sheet导出等。导入则通过注解 解析字典值或校验唯一值,通过自定义的批量方法每500条插入一次
com.alibaba easyexcel 2.2.6
/**
* Excel相关处理
* @author pw
*/
public class ExcelUtil {
/**
* 单个sheet页导出
*/
public static void writeExcel(HttpServletResponse response, List data, String fileName, String sheetName, Class clazz) throws Exception {
EasyExcel.write(getOutputStream(fileName, response), clazz)
.excelType(ExcelTypeEnum.XLSX).sheet(sheetName)
.registerWriteHandler(getCellStyle()).doWrite(data);
}
/**
* sheet页导出.设置下拉框
*/
public static void writeExcel(HttpServletResponse response, List data, String fileName, String sheetName,Map> dropDownMap, Class clazz) throws Exception {
EasyExcel.write(getOutputStream(fileName, response), clazz)
.excelType(ExcelTypeEnum.XLSX).sheet(sheetName)
.registerWriteHandler(new DownHandler(dropDownMap))
.registerWriteHandler(getCellStyle()).doWrite(data);
}
/**
* 合并导出
*/
public static void writeExcelCustomMerge(HttpServletResponse response, List data, String fileName, String sheetName, Class clazz) throws Exception {
EasyExcel.write(getOutputStream(fileName, response), clazz).excelType(ExcelTypeEnum.XLSX).sheet(sheetName).registerWriteHandler(getCellStyle())
.head(clazz)
.registerWriteHandler(new CustomMergeStrategy(clazz))
.doWrite(data);
}
/**
* 多个sheet页导出
*/
public static void writeExcels(HttpServletResponse response,List> data, String fileName, Class clazz) throws Exception{
ExcelWriter excelWriter = null;
try{
excelWriter = EasyExcel.write(getOutputStream(fileName, response), clazz).excelType(ExcelTypeEnum.XLSX).registerWriteHandler(getCellStyle()).build();
for(int i=0;i> data, String fileName,String templateFile) throws Exception{
//excel模板
File file = new File(templateFile);
ExcelWriter excelWriter = null;
try (FileInputStream fileInputStream = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
//通过poi复制出需要的sheet个数的模板
XSSFWorkbook workbook = new XSSFWorkbook(fileInputStream);
workbook.setSheetName(0, "sheet1");
for (int i = 1; i < data.size();i++) {
workbook.cloneSheet(0, "sheet"+(i+1));
}
workbook.write(bos);
byte[] bArray = bos.toByteArray();
InputStream is = new ByteArrayInputStream(bArray);
//通过easyExcel塞入参数
excelWriter = EasyExcel.write(getOutputStream(fileName, response)).withTemplate(is).build();
fillTemplateData(excelWriter,data);
} finally {
if(excelWriter != null){
excelWriter.finish();
}
}
}
/**
* 模板导出
*/
public static void fillTemplate(HttpServletResponse response,List> data, String fileName,String templateFile) throws Exception{
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(getOutputStream(fileName, response)).withTemplate(new File(templateFile) ).build();
fillTemplateData(excelWriter,data);
} finally {
if(excelWriter != null){
excelWriter.finish();
}
}
}
/**
* 填充模板数据
* @param data 数据结构最外层为需要导出的sheet页个数,里层为多种数据格式的集合,一般为list和map组合成的混合list
*/
private static void fillTemplateData(ExcelWriter excelWriter, List> data){
for (int i = 0; i < data.size(); i++) {
WriteSheet writeSheet = EasyExcel.writerSheet(i).build();
for(Object o:data.get(i)){
excelWriter.fill(o, writeSheet);
}
}
}
/**
* 设置请求
*/
private static OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
fileName = URLEncoder.encode(fileName +"."+ ExcelTypeEnum.XLSX, "UTF-8");
// 告诉浏览器用什么软件可以打开此文件
response.setHeader("content-Type", "application/vnd.ms-excel");
response.setCharacterEncoding("utf8");
response.setHeader("content-disposition", "attachment;filename="+fileName+";"+"filename*=utf-8''"+fileName);
return response.getOutputStream();
}
/**
* 设置样式
*/
private static HorizontalCellStyleStrategy getCellStyle(){
//表头样式
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
//设置表头居中对齐
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
//内容样式
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
//设置内容靠左对齐
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
return new HorizontalCellStyleStrategy(headWriteCellStyle,contentWriteCellStyle);
}
}
插入方法已优化,使用的是1.8的consumer,传入自定义方法
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.read.metadata.holder.ReadRowHolder;
import com.cloud.common.core.excel.annotation.FieldDispose;
import com.cloud.common.core.exception.CommonException;
import lombok.Getter;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Consumer;
/**
* 基于EasyExcel的导入工具类,插入使用批量插入,BATCH_COUNT=500
* @author pw
* @param 中间接收类
*/
@Getter
public class CommonListener extends AnalysisEventListener {
/**
* 执行的插入的service
*/
private final Consumer> p;
/**
* 字典
*/
private final Map> itemMap;
/**
* 中间类的字段
*/
Field[] fields ;
/**
* 初始化参数,server 执行
* @param itemMap 字典数据,从里面去对应的数据
* @param p 执行方法
*/
public CommonListener(Map> itemMap,Class tClass, Consumer> p) {
this.p = p;
this.itemMap = itemMap;
this.fields = tClass.getDeclaredFields();
}
/**
* 批量插入数据的大小
*/
private final int BATCH_COUNT = 500;
/**
* 存储数据的集合
*/
private final List rows = new ArrayList<>();
/**
* 存储数据的集合
*/
private final Map> unique = new HashMap<>();
/**
* 存放失败的数据及原因
*/
private final List msgList = new ArrayList<>();
@Override
public void onException(Exception exception, AnalysisContext context) {
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
msgList.add(String.format("第%s行,第%s列解析异常,数据为:%s",excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex(),excelDataConvertException.getCellData()));
}
}
@Override
public void invoke(T data, AnalysisContext context) {
ReadRowHolder readRowHolder = context.readRowHolder();
try {
for ( Field field : fields ) {
//得到属性值
field.setAccessible(true);
String value = (String) field.get(data);
if(field.isAnnotationPresent(FieldDispose.class)){
//获取字段注解
FieldDispose annotation = field.getAnnotation(FieldDispose.class);
switch (annotation.type()){
//字典替换
case DICT:
if(!itemMap.get(annotation.value()).containsKey(value)){
msgList.add(String.format("第%s行异常,原因%s,数据为:%s",readRowHolder.getRowIndex()+1,annotation.errorMessage(),value));
return;
}
field.set(data, itemMap.get(annotation.value()).get(value));
break;
//唯一值判断
case UNIQUE:
if(unique.containsKey(annotation.value())){
if(unique.get(annotation.value()).contains(value)){
msgList.add(String.format("第%s行异常,原因%s,数据为:%s",readRowHolder.getRowIndex()+1,annotation.errorMessage(),value));
return;
}
}else {
Set m = new HashSet<>();
m.add(value);
unique.put(annotation.value(),m);
}
break;
default:break;
}
}
}
rows.add(data);
if(rows.size()>=BATCH_COUNT){
this.saveBatch();
}
}catch (Exception e){
msgList.add(String.format("第%s行异常,原因:%s",readRowHolder.getRowIndex()+1,e.getMessage()));
}
}
/**
* 数据解析完成执行
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if(rows.size()>0){
this.saveBatch();
}
}
private void saveBatch(){
try {
p.accept(rows);
}catch (CommonException e){
rows.clear();
msgList.add(e.getMessage());
}
}
}
/**
* 自定义单元格合并策略
* @author pw
*/
public class CustomMergeStrategy implements RowWriteHandler {
/**
* 主键下标
*/
private Integer pkIndex;
/**
* 需要合并的列的下标集合
*/
private final List needMergeColumnIndex = new ArrayList<>();
/**
* DTO数据类型
*/
private final Class elementType;
public CustomMergeStrategy(Class elementType) {
this.elementType = elementType;
}
@Override
public void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer integer, Integer integer1, Boolean aBoolean) {
}
@Override
public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer integer, Boolean aBoolean) {
}
@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
// 如果是标题,则直接返回
if (isHead) {
return;
}
// 获取当前sheet
Sheet sheet = writeSheetHolder.getSheet();
if (null == pkIndex) {
this.lazyInit(writeSheetHolder);
}
// 判断是否需要和上一行进行合并
// 不能和标题合并,只能数据行之间合并
int rowNum = row.getRowNum();
if (rowNum <= 1) {
return;
}
// 获取上一行数据
Row lastRow = sheet.getRow(row.getRowNum() - 1);
// 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并
if (lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {
boolean isMerged = false;
for (Integer needMerIndex : needMergeColumnIndex) {
//获得合并的区域
List mergeRegions = sheet.getMergedRegions();
for (int i = 0; i < mergeRegions.size(); i++) {
CellRangeAddress cellRangeAddress = mergeRegions.get(i);
// 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
if (cellRangeAddress.isInRange(rowNum - 1, needMerIndex)) {
sheet.removeMergedRegion(i);
cellRangeAddress.setLastRow(rowNum);
sheet.addMergedRegionUnsafe(cellRangeAddress);
isMerged = true;
}
}
if (!isMerged) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(rowNum - 1, row.getRowNum(),
needMerIndex, needMerIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
}
}
}
/**
* 初始化主键下标和需要合并字段的下标
*/
private void lazyInit(WriteSheetHolder writeSheetHolder) {
// 获取当前sheet
Sheet sheet = writeSheetHolder.getSheet();
// 获取标题行
Row titleRow = sheet.getRow(0);
// 获取DTO所有的属性
Field[] fields = this.elementType.getDeclaredFields();
// 遍历所有的字段,因为是基于DTO的字段来构建excel,所以字段数 >= excel的列数
for (Field theField : fields) {
// 获取@ExcelProperty注解,用于获取该字段对应在excel中的列的下标
ExcelProperty easyExcelAnn = theField.getAnnotation(ExcelProperty.class);
// 为空,则表示该字段不需要导入到excel,直接处理下一个字段
if (null == easyExcelAnn) {
continue;
}
// 获取自定义的注解,用于合并单元格
CustomMerge customMerge = theField.getAnnotation(CustomMerge.class);
// 没有@CustomMerge注解的默认不合并
if (null == customMerge) {
continue;
}
for (int index = 0; index < fields.length; index++) {
Cell theCell = titleRow.getCell(index);
// 当配置为不需要导出时,返回的为null,这里作一下判断,防止NPE
if (null == theCell) {
continue;
}
// 将字段和excel的表头匹配上
if (easyExcelAnn.value()[0].equalsIgnoreCase(theCell.getStringCellValue())) {
if (customMerge.isPk()) {
pkIndex = index;
}
if (customMerge.needMerge()) {
needMergeColumnIndex.add(index);
}
}
}
}
// 没有指定主键,则异常
if (null == this.pkIndex) {
throw new IllegalStateException("使用@CustomMerge注解必须指定主键");
}
}
}
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.cloud.common.core.excel.annotation.DropDown;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
import java.lang.reflect.Field;
import java.util.Map;
/**
* @author admin
* @date 2020/5/17
* @desc
*/
public class DownHandler implements SheetWriteHandler {
private final Map dropDownMap;
private int index;
public DownHandler(Map dropDownMap) {
this.dropDownMap = dropDownMap;
this.index = 0;
}
@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Sheet sheet = writeSheetHolder.getSheet();
///开始设置下拉框 HSSFWorkbook
DataValidationHelper helper = sheet.getDataValidationHelper();
Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
int length = fields.length;
for (int i = 0; i < length; i++) {
if(fields[i].isAnnotationPresent(DropDown.class)){
dropDown(helper, sheet, i, fields[i].getDeclaredAnnotation(DropDown.class).value());
}
}
if (dropDownMap == null) {
return;
}
Workbook workbook = writeWorkbookHolder.getWorkbook();
dropDownMap.forEach((celIndex, value) -> {
if(value.length>20){
dropDownBigData( helper, workbook ,sheet,celIndex, value);
}else {
dropDown(helper, sheet,celIndex, value);
}
});
}
private void dropDown(DataValidationHelper helper, Sheet sheet, Integer celIndex, String[] value) {
if(null== value || value.length<=0){
return;
}
this.dropDown(helper, sheet,celIndex, helper.createExplicitListConstraint(value));
}
private void dropDownBigData(DataValidationHelper helper,Workbook workbook,Sheet sheet, Integer celIndex, String[] v) {
// 定义sheet的名称
String sheetName = "sheet" + celIndex;
// 1.创建一个隐藏的sheet 名称为 proviceSheet
Sheet sheet1 = workbook.createSheet(sheetName);
// 从第二个工作簿开始隐藏
this.index++;
// 设置隐藏
workbook.setSheetHidden(this.index, true);
// 2.循环赋值
for (int i = 0, length = v.length; i < length; i++) {
// i:表示你开始的行数 0表示你开始的列数
sheet1.createRow(i).createCell(0).setCellValue(v[i]);
}
Name name = workbook.createName();
name.setNameName(sheetName);
//代表 以A列1行开始获取N行下拉数据
name.setRefersToFormula(sheetName + "!$A$1:$A$" + (v.length));
// 设置下拉
this.dropDown(helper, sheet,celIndex, helper.createFormulaListConstraint(sheetName));
}
private void dropDown(DataValidationHelper helper, Sheet sheet, Integer celIndex, DataValidationConstraint constraint) {
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
CellRangeAddressList addressList = new CellRangeAddressList(1, 100, celIndex, celIndex);
// 数据有效性对象
DataValidation dataValidation = helper.createValidation(constraint, addressList);
// 处理Excel兼容性问题
if (dataValidation instanceof XSSFDataValidation) {
//数据校验
dataValidation.setSuppressDropDownArrow(true);
//错误提示
dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
dataValidation.setShowErrorBox(true);
//选定提示
dataValidation.createPromptBox("填写说明:","填写内容只能为下拉中数据,其他数据将导致导入失败");
dataValidation.setShowPromptBox(true);
sheet.addValidationData(dataValidation);
} else {
dataValidation.setSuppressDropDownArrow(false);
}
sheet.addValidationData(dataValidation);
}
}
/**
* 自定义注解,用于判断是否需要合并以及合并的主键
* @author pw
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge {
/**
* 是否需要合并单元格
*/
boolean needMerge() default false;
/**
* 是否是主键,即该字段相同的行合并
*/
boolean isPk() default false;
}
/**
* 数据来源解析
* @author pw
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD})
@Documented
public @interface FieldDispose {
/**
* 解析类型
*/
AnalyticalType type() default AnalyticalType.DICT;
/**
*绑定的数据,类型dict对应字典(传入的map),其他类型对应实体类字段
*/
String[] value();
/**
* 错误提示
*/
String errorMessage() default "";
/**
*仅拼接下使用,拼接间隔符,数组第几位即代表第几位后添加
*/
String[] mark() default {};
}
/**
* @author pw
* @date 2022/11/17
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface DropDown {
String[] value();
}
/**
* @author pw
*/
public enum ExcelFieldType{
/**
* 字典
*/
DICT ,
/**
* 唯一值
*/
UNIQUE
}
/**
* @author pw
*/
@Data
@ApiModel(value = "用户信息")
@ColumnWidth(20)
public class UserModel implements Serializable {
@ApiModelProperty(value = "登录账号")
@ExcelProperty(value = "登录账号",index=0)
@FieldDispose(type = ExcelFieldType.UNIQUE,value = "username",errorMessage = "登录账号不允许重复" )
private String username;
@ApiModelProperty(value = "姓名")
@ExcelProperty(value = "姓名",index=1)
@FieldDispose(type = ExcelFieldType.UNIQUE,value ="nickName",errorMessage = "姓名不允许重复" )
private String nickName;
@ApiModelProperty(value = "性别")
@ExcelProperty(value = "性别",index=2)
@FieldDispose(value = DictCodeConstants.SEX,errorMessage = "该性别标识不存在" )
private String sex;
@ApiModelProperty(value = "手机号")
@ExcelProperty(value = "手机号",index=3)
private String phone;
@ApiModelProperty(value = "密码")
@ExcelProperty(value = "初始密码",index = 4)
private String password;
@ApiModelProperty(value = "职务")
@ExcelProperty(value = "职务",index = 5)
@FieldDispose(value = DictCodeConstants.USER_POST,errorMessage = "该职务标识不存在")
private String post;
@ApiModelProperty(value = "角色")
@ExcelProperty(value ="角色",index = 6)
@FieldDispose(value = "role",errorMessage = "该角色标识不存在")
private String role;
@ApiModelProperty("状态标识")
@ExcelProperty(value = "状态标识",index = 7)
@FieldDispose(value = DictCodeConstants.F_STATE,errorMessage = "该状态标识不存在")
private String fstate;
@ApiModelProperty("备注")
@ExcelProperty(value = "备注",index = 8)
private String remarks;
}
@ApiOperation(value = "导入数据")
@PostMapping("/importData")
public R