起因:
最近需要用到excel导出功能,使用EasyExcel可以快速实现导出,又需要优雅的对EasyExcel进行封装,在实现自己的导出功能时又可以制定一定的规则,让其他同事方便使用,最近研究了下网上的常规写法,站在巨人的肩上重新添加了自己的思路,供大家参考,有任何问题请多指教
痛点:
1、导出代码写在业务中,调用不统一且代码层次混乱
2、数据库查询的数据远多于对象中定义的数据,导出时默认导出了不想要的数据
3、相同的对象,在导出时需要根据不同条件进行设置不同的Excel表头字段,又不想定义多个导出对象
4、导出时如果设置过某个属性的表头宽度,优先按照设置的宽度导出,如果未设置,希望表头宽度有个默认值,而不是挤压在一起
5、excel需要导出大数据,都在一个sheet页中查看卡顿
6、导出的数据是数据库字符,查看excel人员看不懂,需要做数据转化
如果你有以上痛点问题,可以继续啦;如果没有也可以看看我的封装思路,还请各位高手不吝赐教,这里非常感谢 雪孤城的这篇文章 给了我很大的启发和参考
1. 工具类
1.1 EasyExcelUtil封装类
package cn.well.cloud.core.util.excel;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
/**
* @Description: Easyexcel工具类封装及表格自动列宽设置
* @Author: jiawei
* @Date: 2023/7/26
*/
@Slf4j
public class EasyExcelUtil {
/**
* 每个sheet的容量,即超过60000时就会把数据分sheet
*/
private static final int PAGE_SIZE = 60000;
/**
* 无过滤字段的导出excel
* @param data 需要导出的数据集
* @param fileName 需要导出的excel名
* @param excelClass 需要导出的excel表头实体
* @return
* @author jiawei
* @since 2023/7/27 下午1:39
*/
public static void exportByExcel(List<?> data, String fileName, Class<?> excelClass){
exportByExcel(data, fileName, excelClass, AbstractEasyExcel.getExcludeColumnFieldNames());
}
/**
* 自定义过滤字段的导出excel
* @param data 需要导出的数据集
* @param fileName 需要导出的excel名
* @param excelClass 需要导出的excel表头实体
* @param excludeColumnFieldNames 需要过滤的字段集合
* @return
* @author jiawei
* @since 2023/7/27 下午1:41
*/
public static void exportByExcel(List<?> data, String fileName, Class<?> excelClass, Collection<String> excludeColumnFieldNames){
try {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = servletRequestAttributes.getResponse();
long exportStartTime = System.currentTimeMillis();;
log.info("报表导出Size: " + data.size() + "条。");
// 把查询到的数据按设置的sheet的容量进行切割
List<? extends List<?>> lists = SplitList.splitList(data, PAGE_SIZE);
// 设置响应头
AbstractEasyExcel.setHead(response, fileName + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN));
// 浏览器访问url直接下载文件的方式
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), excelClass)
.registerWriteHandler(AbstractEasyExcel.formatExcel())
.registerWriteHandler(new AbstractEasyExcel.ExcelWidthStyleStrategy())
.includeColumnFieldNames(AbstractEasyExcel.getIncludeColumnFiledNames(excelClass))
.excludeColumnFieldNames(excludeColumnFieldNames)
.build();
ExcelWriterSheetBuilder excelWriterSheetBuilder;
WriteSheet writeSheet;
if(ObjectUtil.hasNull(lists)){
log.info("未查询到要导出的数据,导出终止");
return;
}
for (int i = 1; i <= lists.size(); ++i) {
excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
excelWriterSheetBuilder.sheetNo(i).sheetName("sheet" + i);
writeSheet = excelWriterSheetBuilder.build();
excelWriter.write(lists.get(i - 1), writeSheet);
}
// 必须要finish才会写入,不finish只会创建empty的文件
excelWriter.finish();
log.info("报表导出结束时间:" + new Date() + ";导出耗时: " + (System.currentTimeMillis() - exportStartTime) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.2AbstractEasyExcel EasyExcel的基础封装,包含响应头设置,Excel的格式设置,根据实体设置需要导出的字段,设置需要过滤的实体中字段(字符串模式和lambda表达获取get方法引用的属性+连续方法调用模式),设置头部单元格宽度
AbstractEasyExcel
package cn.well.cloud.core.util.excel;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
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.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.*;
/**
* @Description: EasyExcel的基础封装
* @Author: jiawei
* @Date: 2023/7/26
*/
@Slf4j
public class AbstractEasyExcel {
/**
* 设置响应头
*
* @param response 回应的请求数据
* @param fileName 文件名字
*/
public static void setHead(HttpServletResponse response, String fileName) {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码
try {
fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("编码异常");
}
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
/**
* 设置Excel的格式
*
* @return 格式化后的Excel
*/
public static HorizontalCellStyleStrategy formatExcel() {
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillBackgroundColor(IndexedColors.WHITE.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 10);
headWriteCellStyle.setWriteFont(headWriteFont);
// 内容的策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
WriteFont contentWriteFont = new WriteFont();
// 字体大小
contentWriteFont.setFontHeightInPoints((short) 10);
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 设置自动换行
contentWriteCellStyle.setWrapped(true);
// 设置垂直居中
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置水平居中
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
}
/**
* 设置需要导出的字段
* @param clazz
* @return {@link Integer}
* @author jiawei
* @since 2023/7/27 上午11:21
*/
public static Collection<String> getIncludeColumnFiledNames(Class<?> clazz) {
Set<String> set = new HashSet<String>();
Field[] fields;
do {
fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
set.add(fields[i].getName());
}
clazz = clazz.getSuperclass();
} while (clazz != Object.class && clazz != null);
return set;
}
/**
* 设置需要过滤的字段方式一(不推荐)
* @param fields 字段名
* @return {@link Collection< String>}
* @author jiawei
* @since 2023/7/27 下午1:32
*/
public static Collection<String> getExcludeColumnFieldNames(Object ... fields) {
Set<String> excludeColumnFieldNames = new HashSet<String>();
for (Object field : fields) {
excludeColumnFieldNames.add(StrUtil.toString(field));
}
return excludeColumnFieldNames;
}
/**
* 设置需要过滤的字段方式二(推荐)
* @param excludeColumn
* @return {@link Collection< String>}
* @author jiawei
* @since 2023/7/27 下午2:57
*/
public static Collection<String> getExcludeColumnFieldNames(ExcludeColumn excludeColumn) {
return excludeColumn.excludeColumnFieldNames;
}
public static class ExcludeColumn{
Set<String> excludeColumnFieldNames = new HashSet<String>();
public <T> ExcludeColumn add(SFunction<T, ?> fn) {
String field= FieldUtil.getField(fn).getName();
if (field instanceof String) {
excludeColumnFieldNames.add(StrUtil.toString(field));
return this;
}
throw ExceptionUtils.mpe("not support this column :" + field);
}
}
/**
* 获取class的 包括父类的
*
* @param clazz
* @return
*/
public static Field[] getClassFields(Class<?> clazz) {
List<Field> list = new ArrayList<Field>();
Field[] fields;
do {
fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
list.add(fields[i]);
}
clazz = clazz.getSuperclass();
} while (clazz != Object.class && clazz != null);
return list.toArray(fields);
}
/**
* 设置头部单元格宽度
*/
public static class ExcelWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
/**
* 设置列表,默认50
* @param writeSheetHolder
* @param list
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
* @return
* @author jiawei
* @since 2023/7/27 上午10:52
*/
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> list, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
Sheet sheet = writeSheetHolder.getSheet();
boolean needSetWidth = relativeRowIndex != null && (isHead || relativeRowIndex == 0);
if (!needSetWidth) {
return;
}
Integer width = columnWidth(head, cell.getColumnIndex());
if (width != null) {
width = width * 256;
sheet.setColumnWidth(cell.getColumnIndex(), width);
}else{
sheet.setColumnWidth(cell.getColumnIndex(), 5000);
}
}
/**
* 获取设置的自定义宽度
* @param head
* @param columnIndex
* @return {@link java.lang.Integer}
* @author jiawei
* @since 2023/7/27 上午10:50
*/
protected Integer columnWidth(Head head, Integer columnIndex) {
if (head == null) {
return null;
} else {
return head.getColumnWidthProperty() != null ? head.getColumnWidthProperty().getWidth() : null;
}
}
}
}
1.3 getField bean属性获取工具类,此工具中使用可序列化的 SFunction,引用的是mybatisplus 核心包底下现成的,当然你如果未引用mybatisplus 的话,也可以自己添加上该类,非常简单,附在该工具类后面了
package cn.well.cloud.core.util.excel;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @Description: bean属性获取工具类
* @Author: jiawei
* @Date: 2023/7/27
*/
public class FieldUtil {
/**
* 将bean的属性的get方法,作为lambda表达式传入时,获取get方法对应的属性Field
*
* @param fn lambda表达式,bean的属性的get方法
* @param <T> 泛型
* @return 属性对象
*/
public static <T> Field getField(SFunction<T, ?> fn) {
// 从function取出序列化方法
Method writeReplaceMethod;
try {
writeReplaceMethod = fn.getClass().getDeclaredMethod("writeReplace");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
// 从序列化方法取出序列化的lambda信息
boolean isAccessible = writeReplaceMethod.isAccessible();
writeReplaceMethod.setAccessible(true);
SerializedLambda serializedLambda;
try {
serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(fn);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
writeReplaceMethod.setAccessible(isAccessible);
// 从lambda信息取出method、field、class等
String implMethodName = serializedLambda.getImplMethodName();
// 确保方法是符合规范的get方法,boolean类型是is开头
if (!implMethodName.startsWith("is") && !implMethodName.startsWith("get")) {
throw new RuntimeException("get方法名称: " + implMethodName + ", 不符合java bean规范");
}
// get方法开头为 is 或者 get,将方法名 去除is或者get,然后首字母小写,就是属性名
int prefixLen = implMethodName.startsWith("is") ? 2 : 3;
String fieldName = implMethodName.substring(prefixLen);
String firstChar = fieldName.substring(0, 1);
fieldName = fieldName.replaceFirst(firstChar, firstChar.toLowerCase());
Field field;
try {
field = Class.forName(serializedLambda.getImplClass().replace("/", ".")).getDeclaredField(fieldName);
} catch (ClassNotFoundException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
return field;
}
}
1.3.1 SFunction 对,你没看错,就是一个空的接口类
package com.baomidou.mybatisplus.core.toolkit.support;
import java.io.Serializable;
import java.util.function.Function;
/**
* 支持序列化的 Function
*
* @author miemie
* @since 2018-05-12
*/
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}
1.4 SplitList EasyExcelUtils类的辅助类(切割查询的数据)
package cn.well.cloud.core.util.excel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @Description: EasyExcelUtils类的辅助类(切割查询的数据)
* @Author: jiawei
* @Date: 2023/7/26
*/
public class SplitList {
/**
* 切割查询的数据
* @param list 需要切割的数据
* @param len 按照什么长度切割
* @param <T>
* @return
*/
public static <T> List<List<T>> splitList(List<T> list, int len) {
if (list == null || list.size() == 0 || len < 1) {
return null;
}
List<List<T>> result = new ArrayList<List<T>>();
int size = list.size();
int count = (size + len - 1) / len;
for (int i = 0; i < count; i++) {
List<T> subList = list.subList(i * len, (Math.min((i + 1) * len, size)));
result.add(subList);
}
return result;
}
/**
* 集合平均分组
* @param source 源集合
* @param n 分成n个集合
* @param <T> 集合类型
* @return 平均分组后的集合
*/
public static <T> List<List<T>> groupList(List<T> source, int n) {
if (source == null || source.size() == 0 || n < 1) {
return null;
}
if (source.size() < n) {
return Collections.singletonList(source);
}
List<List<T>> result = new ArrayList<List<T>>();
int number = source.size() / n;
int remaider = source.size() % n;
// 偏移量,每有一个余数分配,就要往右偏移一位
int offset = 0;
for (int i = 0; i < n;i++) {
List<T> list1 = null;
if (remaider > 0){
list1 = source.subList(i * number + offset,(i + 1) * number + offset + 1);
remaider--;
offset++;
}else {
list1 = source.subList(i * number + offset, (i+1) * number + offset);
}
result.add(list1);
}
return result;
}
}
数据转换工具
2.1 AbstactBaseConverter 抽象类,自定义的类只需要实现该类即可
package cn.well.cloud.core.util.excel.converter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;
/**
* @Description:
* @Author: jiawei
* @Date: 2023/7/27
*/
public class AbstactBaseConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 这里读的时候会调用
* @param context
* @return {@link java.lang.String}
* @author jiawei
* @since 2023/7/27 上午9:47
*/
@Override
public String convertToJavaData(ReadConverterContext<?> context) {
return context.getReadCellData().getStringValue();
}
/**
* 这里是写的时候会调用
* @param context
* @return {@link com.alibaba.excel.metadata.data.WriteCellData<?>}
* @author jiawei
* @since 2023/7/27 上午9:47
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
return new WriteCellData<>(context.getValue());
}
}
2.2 YesOrNotConverter自定义某个字段值导出时,“Y"导出"是”,“N"导出"否”
package cn.well.cloud.core.util.excel.converter;
import cn.well.cloud.core.enums.YesOrNotEnum;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.metadata.data.WriteCellData;
/**
* @Description:
* @Author: jiawei
* @Date: 2023/7/27
*/
public class YesOrNotConverter extends AbstactBaseConverter {
/**
* 重写读方法
* @param context
* @return {@link java.lang.String}
* @author jiawei
* @since 2023/7/27 上午9:46
*/
@Override
public String convertToJavaData(ReadConverterContext<?> context) {
return super.convertToJavaData(context);
}
/**
* 重写写方法
* @param context
* @return {@link com.alibaba.excel.metadata.data.WriteCellData<?>}
* @author jiawei
* @since 2023/7/27 上午9:47
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
return new WriteCellData<>( YesOrNotEnum.N.getCode().equals(context.getValue())? YesOrNotEnum.N.getMessage():YesOrNotEnum.Y.getMessage() );
}
}
2.3 YesOrNotEnum 枚举类
package cn.well.cloud.core.enums;
import lombok.Getter;
/**
* 是或否的枚举
*
* @Author: jiawei
* @Date: 2023/7/27
*/
@Getter
public enum YesOrNotEnum {
/**
* 是
*/
Y("Y",1, "是"),
/**
* 否
*/
N("N",0, "否");
private final String code;
private final int value;
private final String message;
YesOrNotEnum(String code,Integer value, String message) {
this.code = code;
this.value = value;
this.message = message;
}
}
以上是基础工具的封装,大家可以写到底层包里,供业务层调用,我的目录格式如下
业务层
3.1 EapCardNumBo 导出对象
package cn.well.cloud.modular.eap.contract.card.bo;
import cn.well.cloud.core.util.excel.converter.YesOrNotConverter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentStyle;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* @Description: 卡号表
* @author jaiwei
* @since 2023-07-23
*/
@Data
@EqualsAndHashCode
public class EapCardNumBo implements Serializable {
@ExcelProperty(value = "序号")
private Integer serialNumber;
@ExcelProperty(value = "合同唯一标识")
@ContentStyle(dataFormat = 1)
@ColumnWidth(80)
private Long contractUid;
@ExcelProperty(value = "卡号")
private String cardNumber;
@ExcelProperty(value = "是否绑定", converter = YesOrNotConverter.class)
private String hasBinding;
@ExcelProperty(value = "卡号类型")
private Integer cardType;
@ExcelProperty(value = "创建时间")
@DateTimeFormat("yyyy-MM-dd hh:mm:ss")
private Date createTime;
}
3.2 调用方法,此处是业务层方法中的调用方式,mybatisplus查询数据的代码就补贴出来了
@Override
public void export(Long contractUid) {
List<EapCardNumPO> eapCardNumPOList = baseEapCardNumMapper.selectList(new QueryWrapperX<EapCardNumPO>().lambda().eq(EapCardNumPO::getContractUid, contractUid));
//调用方式一(推荐)
AbstractEasyExcel.ExcludeColumn excludeColumn = new AbstractEasyExcel.ExcludeColumn().add(EapCardNumBo::getContractUid).add(EapCardNumBo::getCreateTime);
EasyExcelUtil.exportByExcel(eapCardNumPOList,"卡号",EapCardNumBo.class, AbstractEasyExcel.getExcludeColumnFieldNames(excludeColumn));
//调用方式二(不推荐)
EasyExcelUtil.exportByExcel(eapCardNumPOList,"卡号",EapCardNumBo.class, AbstractEasyExcel.getExcludeColumnFieldNames("contractUid","createTime"));
}
总结如下
新增工具类
EasyExcelUtil excel读写工具类
具体实现方法
exportByExcel() 浏览器直接导出
自定义导出实体注解
@ContentStyle 设置数据格式,dataFormat = 1 会避免较长数字科学计数法显示,其他格式参见easyexcel-core包底下的com.alibaba.excel.constant.BuiltinFormats
converter = YesOrNotConverter.class 自定义转换导出的内容,比如Y需要导出是,N需要导出否,不建议在数据和业务层面进行处理,而是抽取出转换器类进行处理
@DateTimeFormat 时间格式化
@ColumnWidth(80) 该注解不添加,默认单元格宽度是50,超过50长度时需要自定义列宽
方法调用一(推荐)
AbstractEasyExcel.ExcludeColumn excludeColumn = new AbstractEasyExcel.ExcludeColumn().add(EapCardNumBo::getContractUid).add(EapCardNumBo::getCreateTime);
EasyExcelUtil.exportByExcel(eapCardNumPOList,"卡号",EapCardNumBo.class, AbstractEasyExcel.getExcludeColumnFieldNames(excludeColumn));
`
方法调用二(不推荐)
EasyExcelUtil.exportByExcel(eapCardNumPOList,"卡号",EapCardNumBo.class, AbstractEasyExcel.getExcludeColumnFieldNames("contractUid","createTime"));
非常感谢阿里巴巴对EasyExcel的开源,且用且珍惜