使用EasyExcel实现无模板、全自定义Excel导出

您所在的位置:网站首页 Excel自定义模板怎么创 使用EasyExcel实现无模板、全自定义Excel导出

使用EasyExcel实现无模板、全自定义Excel导出

2024-05-21 21:55| 来源: 网络整理| 查看: 265

1 需求背景

最近公司需要做一个动态字段的Excel导出,大致的样式如下:

image-20230606104726048.png

实体类如下:

// 部门实体类 public class Department { private String companyName; private String name; private String fullName; private String leaderName; private String business; private Long count; private String location; private String status; private List staffList; } //员工实体类 public class Staff { private Long id; private String name; private Long age; private String position; private BigDecimal salary; private String code; }

具体要求如下:

标题部分需要合并单元格; 展示部门信息的表格需要横向展示数据,且用户可以选择导出哪些字段的数据; 展示员工信息部分的数据为一个表格,用户可以选择导出哪些字段的数据。

如图所示,Excel可以被分成三个部分:

企业微信截图_16860187599012.png

这三个部分都需要动态生成,理由如下:

标题部分需要展示公司信息以及导出的是什么Excel(例如销售单、物流单等等),后续可能需要展示更多的信息,也就是说行数是动态的。并且图中的第三个部分也就是列表数据部分字段的个数是动态的,也就是说标题部分的列数也是动态的; 公共部分同理,且公共部分的字段也需要可选择的导出; 列表数据部分的字段个数以及数据的条数都是动态的,那么行和列都是动态的;

综上,我们现在的问题如下:

三个部分的表格行、列都是动态的; 三个部分在一个Excel中导出,列宽都会互相影响,如何自适应列宽? 公共部分的表格并不是传统的竖向表格,是横向排列的。 如何对应字段名和在Excel中显示的字段名的关系?比如某个字段在代码中为name,但Excel中应该显示为姓名。 如何确定字段之间在Excel导出时的顺序,譬如name age salary三个字段,我希望它们按照name-age-salary的顺序显示,如果用户只导出name和salary字段,那顺序也应该是name-salary而不是其他的。 2 抽象导出实体类

如之前所说,可以将Excel分为三个部分,那么此时就可以构建一个类用以支持Excel的动态导出,具体代码如下:

// Excel实体类 public class ExportCustomCommon { // 标题部分,应该有几行就有几条 List headerTable; // 公共部分 ExcelCommonData commonTable; // 列表数据部分 List commonDataClass = commonTable.getClass(); //通过类对象获取该类中的所有属性 List fields = this.getAllField(commonDataClass); List fieldList = new ArrayList(); try { for (Field field : fields) { //如果在Maven打包时报错,Spring项目中可以替换为Spring中的BeanUtils.getPropertyDescriptor() PropertyDescriptor pd = new PropertyDescriptor(field.getName(), commonDataClass); Assert.notNull(pd, Exception::new); //反射获取读方法 Method readMethod = pd.getReadMethod(); //读到属性值 Object fieldValue = readMethod.invoke(commonTable); //获取属性注解 ExcelProperty property = field.getAnnotation(ExcelProperty.class); //获取Excel显示名称 String excelFieldName = property.value()[0]; //获取Excel中排序 int excelFieldOrder = property.order(); //构建对象 ExcelField excelField = new ExcelField(); excelField.setFieldName(field.getName()); excelField.setShowName(excelFieldName); excelField.setOrder(excelFieldOrder); excelField.setValue(fieldValue); fieldList.add(excelField); } } catch (Exception e) { e.printStackTrace(); } //根据Order排序 fieldList.sort(Comparator.comparingInt(ExcelField::getOrder)); int count = fieldList.size(); //计算一行显示属性的个数,除以3是因为一个属性需要属性名--属性值--空字符串三个单元格 int lineCount = maxColumn / 3; //计算行数 int rows = (count + lineCount - 1) / lineCount; List cellList = new ArrayList(); List rowList = new ArrayList(); //自定义样式 WriteCellStyle cellStyle = new WriteCellStyle(); //水平靠左 cellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT); //遍历所有行,一行作为一个表 for (int row = 0; row < rows; ++row) { WriteTable table = EasyExcel.writerTable(tableNoCounting.getAndIncrement()) .needHead(Boolean.FALSE) .registerWriteHandler( new HorizontalCellStyleStrategy(cellStyle, cellStyle) ).build(); //构建List类型的数据给EasyExcel导出 for (int i = 0; i < lineCount && row * lineCount + i < count; ++i) { ExcelField field = fieldList.get(row * lineCount + i); cellList.add(field.getShowName() + ":"); cellList.add(field.getValue()); cellList.add(""); } rowList.add(cellList); //指定写入的sheet和table writer.write(rowList, sheet, table); cellList.clear(); rowList.clear(); } } } /** * 构建列表部分 */ private void buildList(List listTable, WriteSheet sheet, ExcelWriter writer, AtomicInteger tableNoCounting, List fields) { //自定义样式 WriteCellStyle headStyle = new WriteCellStyle(); //设置header背景颜色为透明 headStyle.setFillForegroundColor(IndexedColors.AUTOMATIC.getIndex()); //水平居中 headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); //上下左右四个边框 headStyle.setBorderBottom(BorderStyle.THIN); headStyle.setBorderTop(BorderStyle.THIN); headStyle.setBorderLeft(BorderStyle.THIN); headStyle.setBorderRight(BorderStyle.THIN); WriteFont writeFont = new WriteFont(); //字体加粗 writeFont.setBold(Boolean.TRUE); //字号 writeFont.setFontHeightInPoints((short) 12); headStyle.setWriteFont(writeFont); WriteCellStyle contentStyle = new WriteCellStyle(); //内容上下左右四个边框 contentStyle.setBorderBottom(BorderStyle.THIN); contentStyle.setBorderTop(BorderStyle.THIN); contentStyle.setBorderLeft(BorderStyle.THIN); contentStyle.setBorderRight(BorderStyle.THIN); //水平居中 contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); WriteTable table = EasyExcel.writerTable(tableNoCounting.getAndIncrement()) .needHead(Boolean.TRUE)//需要Header .registerWriteHandler(new HorizontalCellStyleStrategy(headStyle, contentStyle))//传入自定义样式 .includeColumnFiledNames(fields)//选择需要哪些属性 .build(); writer.write(listTable, sheet, table); } //获取该类的所有属性,包括父类中不重名的属性 private List getAllField(Class clazz) { List resultList = new ArrayList(); for (List fieldNameList = new ArrayList(); clazz != null && !clazz.getName().toLowerCase().equals(ExportCustomCommon.class.getName()); clazz = clazz.getSuperclass()) { List subFields = Arrays.asList(clazz.getDeclaredFields()); List list = subFields.stream() .filter((f) -> !fieldNameList.contains(f.getName())) .collect(Collectors.toList()); List nameList = list.stream().map(Field::getName).collect(Collectors.toList()); resultList.addAll(list); fieldNameList.addAll(nameList); } return resultList; } }

接下来构建测试类

package com.kazusa.excel.demo; import java.io.IOException; import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @Description Excel导出测试类 * @Author kazusa */ public class TestMain { public static void main(String[] args) throws IOException { EasyExcelUtil excelUtil = new EasyExcelUtil(); //构建标题 List header = Arrays.asList("标题1", "标题2"); //构建公共部分 DepartmentExcel department = new DepartmentExcel(); department.setCompanyName("TestCompany"); department.setName("Name"); department.setFullName("FullName"); department.setLeaderName("LeaderName"); department.setBusiness("Business"); department.setCount(1000L); department.setLocation("Location"); department.setStatus("Status"); //构建列表部分 List staffs = new ArrayList(); for (int i = 0; i < 10; i++) { StaffExcel staff = new StaffExcel(); staff.setId((long) i); staff.setName("staff-name" + i); staff.setAge((long) (i + 20)); staff.setPosition("position" + i); staff.setSalary(new BigDecimal(i)); staff.setCode("code" + i); staff.setField1("field1" + i); staff.setField2("field2" + i); staff.setField3("field3" + i); staff.setField4("field4" + i); staffs.add(staff); } //构建属性 List fieldList = Arrays.asList("name", "age", "position", "salary", "code" , "field1", "field2", "field3", "field4"); ExportCustomCommon common = new ExportCustomCommon(); common.setCommonTable(department); common.setHeaderTable(header); common.setListTable(staffs); excelUtil.export(Files.newOutputStream(Paths.get("C:\\Users\\DELL\\Desktop\\test-excel.xlsx")) , common, fieldList); } }

导出结果如下:

image-20230606133919798.png

可以看到有一些问题,就是我们之前说过的自适应列宽的问题,如果不能根据单元格内文本的长度自适应列宽的话,就会出现某些内容显示不全的问题。万幸EasyExcel已经提供了解决方案,我们在EasyExcelUtil的34、221、272行添加了三行代码使导出的Excel可以自适应列宽。

package com.kazusa.excel.demo; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.support.ExcelTypeEnum; import com.alibaba.excel.write.merge.OnceAbsoluteMergeStrategy; 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.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.HorizontalAlignment; import org.apache.poi.ss.usermodel.IndexedColors; import java.beans.PropertyDescriptor; import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** * @Description EasyExcel导出工具类 * @Author Kazusa */ public class EasyExcelUtil { //在这里新增了一个ThreadLocal类变量,用以存储一个自适应列宽的策略 private static final ThreadLocal matchStrategy = new ThreadLocal(); public void export(OutputStream os, ExportCustomCommon params, List fields) { //获取标题部分数据 List headerTable = params.getHeaderTable(); //获取公共部分数据 ExportCustomCommon.ExcelCommonData commonTable = params.getCommonTable(); //获取列表部分数据 List commonDataClass = commonTable.getClass(); //通过类对象获取该类中的所有属性 List fields = this.getAllField(commonDataClass); List fieldList = new ArrayList(); try { for (Field field : fields) { //如果在Maven打包时报错,Spring项目中可以替换为Spring中的BeanUtils.getPropertyDescriptor() PropertyDescriptor pd = new PropertyDescriptor(field.getName(), commonDataClass); Assert.notNull(pd, Exception::new); //反射获取读方法 Method readMethod = pd.getReadMethod(); //读到属性值 Object fieldValue = readMethod.invoke(commonTable); //获取属性注解 ExcelProperty property = field.getAnnotation(ExcelProperty.class); //获取Excel显示名称 String excelFieldName = property.value()[0]; //获取Excel中排序 int excelFieldOrder = property.order(); //构建对象 ExcelField excelField = new ExcelField(); excelField.setFieldName(field.getName()); excelField.setShowName(excelFieldName); excelField.setOrder(excelFieldOrder); excelField.setValue(fieldValue); fieldList.add(excelField); } } catch (Exception e) { e.printStackTrace(); } //根据Order排序 fieldList.sort(Comparator.comparingInt(ExcelField::getOrder)); int count = fieldList.size(); //计算一行显示属性的个数,除以3是因为一个属性需要属性名--属性值--空字符串三个单元格 int lineCount = maxColumn / 3; //计算行数 int rows = (count + lineCount - 1) / lineCount; List cellList = new ArrayList(); List rowList = new ArrayList(); //自定义样式 WriteCellStyle cellStyle = new WriteCellStyle(); //水平靠左 cellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT); //遍历所有行,一行作为一个表 for (int row = 0; row < rows; ++row) { WriteTable table = EasyExcel.writerTable(tableNoCounting.getAndIncrement()) .needHead(Boolean.FALSE) .registerWriteHandler( new HorizontalCellStyleStrategy(cellStyle, cellStyle) ) //添加自适应列宽策略 .registerWriteHandler(matchStrategy.get()) .build(); //构建List类型的数据给EasyExcel导出 for (int i = 0; i < lineCount && row * lineCount + i < count; ++i) { ExcelField field = fieldList.get(row * lineCount + i); cellList.add(field.getShowName() + ":"); cellList.add(field.getValue()); cellList.add(""); } rowList.add(cellList); //指定写入的sheet和table writer.write(rowList, sheet, table); cellList.clear(); rowList.clear(); } } } /** * 构建列表部分 */ private void buildList(List clazz) { List resultList = new ArrayList(); for (List fieldNameList = new ArrayList(); clazz != null && !clazz.getName().toLowerCase().equals(ExportCustomCommon.class.getName()); clazz = clazz.getSuperclass()) { List subFields = Arrays.asList(clazz.getDeclaredFields()); List list = subFields.stream() .filter((f) -> !fieldNameList.contains(f.getName())) .collect(Collectors.toList()); List nameList = list.stream().map(Field::getName).collect(Collectors.toList()); resultList.addAll(list); fieldNameList.addAll(nameList); } return resultList; } }

看完自适应列宽部分代码的小伙伴应该会有一个疑问,为什么需要使用一个线程本地变量来存储这个策略对象?

就像我们之前说过的该代码导出Excel表格使用了多个Table组合成一个Excel的形式,那么每个table中的每一个单元格都会影响跟它同列但是属于其他Table的列宽,查看LongestMatchColumnWidthStyleStrategy的源码可以看到,其内部有一个cache Map,作用就是存储该列的最大长度,从而保证该列上的每一个单元格中的内容都可以显示完全,所以在这里我们需要保证一个Excel文件导出过程中使用的LongestMatchColumnWidthStyleStrategy对象为同一个对象。当然不一定使用线程本地变量来存储,也可以使用其他的方法。

package com.alibaba.excel.write.style.column; import ............ public class LongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy { private static final int MAX_COLUMN_WIDTH = 255; //这里就是存储某一列最大列宽的Map,如果构建同一个Excel时使用了不同的Map,最终的结果会出现错误 private Map cache = new HashMap(8); @Override protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList); if (!needSetWidth) { return; } Map maxColumnWidthMap = cache.get(writeSheetHolder.getSheetNo()); if (maxColumnWidthMap == null) { maxColumnWidthMap = new HashMap(16); cache.put(writeSheetHolder.getSheetNo(), maxColumnWidthMap); } Integer columnWidth = dataLength(cellDataList, cell, isHead); if (columnWidth < 0) { return; } if (columnWidth > MAX_COLUMN_WIDTH) { columnWidth = MAX_COLUMN_WIDTH; } Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex()); if (maxColumnWidth == null || columnWidth > maxColumnWidth) { maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth); writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256); } } private Integer dataLength(List cellDataList, Cell cell, Boolean isHead) { if (isHead) { return cell.getStringCellValue().getBytes().length; } CellData cellData = cellDataList.get(0); CellDataTypeEnum type = cellData.getType(); if (type == null) { return -1; } switch (type) { case STRING: return cellData.getStringValue().getBytes().length; case BOOLEAN: return cellData.getBooleanValue().toString().getBytes().length; case NUMBER: return cellData.getNumberValue().toString().getBytes().length; default: return -1; } } }

最后导出的结果如图:

image-20230606135154929.png

最后,本代码只实现了列表部分可以选择字段导出,在公共部分没有办法选择指定字段导出,但是由于整个Excel都是动态生成的,在构建公共部分表格时只需要把公共部分的字段自己做一个筛选即可。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3