玩转EasyExcel,看这一篇就够了!!(合并导入 自定义导出 动态表头 合并单元格)

Java操作excel表格,除了运用POI技术,基于EasyExcel,接下来我们来实战操作下自定义动态化导出excel,自定义动态化为自定义标题,合并单元格,废话不多说,直接上干货。

引入依赖:

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>4.0.3</version>
</dependency>

普通导出(粘贴即用):

typescript 复制代码
public static void exportExcel(HttpServletResponse response, String fileName, String sheetName, List<?> list, Class<?> pojoClass) {
    if (StringUtils.isBlank(fileName)) {
        fileName = DateUtils.format(new Date());
    }
    try {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".xlsx");

        EasyExcel.write(response.getOutputStream(), pojoClass)
                .registerConverter(new LongStringConverter())
                .registerWriteHandler(new HorizontalCellStyleStrategy(null, getWriteCellStyle()))
                .sheet(sheetName).doWrite(list);
    } catch (Exception e) {
        throw new ServiceException("导出文件失败:", e.getMessage());
    }

}

单个Sheet页导出到本地(粘贴即用):

arduino 复制代码
/**
 * excel单个sheet导出
 */
public static <T> void export(String path, String fileName, Class<T> exportType, List<T> list) {
    //创建文件
    String filePath = getFilePath(path, fileName);
    String sheetName = fileName.replace(".xlsx", "").replace(".xls", "");
    EasyExcel.write(filePath, exportType).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).sheet(sheetName).doWrite(list);
}

多Sheet页导出到本地(粘贴即用):

scss 复制代码
public static void exportMultipleSheet(String path, String fileName, List<Class<?>> tClassList, List<String> sheetNameList, List<List<Object>> resultList) {
    //创建文件
    String filePath = getFilePath(path, fileName);
    File file = new File(filePath);
    try {
        FileOutputStream output = new FileOutputStream(file);
        ExcelWriter excelWriter = EasyExcel.write(output).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
        for (int i = 0; i < sheetNameList.size(); i++) {
            WriteSheet writeSheet = EasyExcel.writerSheet(i, sheetNameList.get(i)).head(tClassList.get(i)).build();
            excelWriter.write(resultList.get(i), writeSheet);
        }
        //关闭流
        excelWriter.finish();
        output.flush();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
typescript 复制代码
public static String getFilePath(String path, String fileName) {
    //创建文件夹
    File fileOld = new File(path);
    if (!fileOld.exists()) {
        try {
            FileUtil.mkdir(fileOld);
        } catch (Exception e) {
            throw new ServiceException("创建文件夹失败");
        }
    }
    //写入文件绝对地址
    return String.format("%s/%s", path, fileName);
}

自定义表头

第一步:构建表头

typescript 复制代码
@Data
public class EasyExcelTitleInfo {

    // 索引 即列顺序
    private Integer index;


    // 标题 支持多级
    private List<String> title;


    // 对应返回数据的字段名称
    private String columnName;

}

第二步:引入工具类方法

typescript 复制代码
public static void exportExcelCustomizationHead(HttpServletResponse response, String fileName, String sheetName, List<?> list, List<EasyExcelTitleInfo> titleInfoList) {
    if (CollectionUtils.isEmpty(titleInfoList)) {
        throw new ServiceException("标题不能为空");
    }

    if (StringUtils.isBlank(fileName)) {
        fileName = DateUtils.format(new Date());
    }
    try {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".xlsx");

        EasyExcel.write(response.getOutputStream())
                // excel表头处理
                .head(getHead(titleInfoList))
                .registerConverter(new LongStringConverter())
                // 这里定义统一列宽
                .registerWriteHandler(new AbstractColumnWidthStyleStrategy() {
                    @Override
                    protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
                        Sheet sheet = writeSheetHolder.getSheet();
                        sheet.setColumnWidth(cell.getColumnIndex(), 5120);
                    }
                })
                // 样式定义
                .registerWriteHandler(new HorizontalCellStyleStrategy(null, getWriteCellStyle()))
                .sheet(sheetName)
                // excel数据处理
                .doWrite(getDataList(list, titleInfoList));

    } catch (Exception e) {
        throw new ServiceException("导出文件失败:", e.getMessage());
    }

}
ini 复制代码
// 获取表头
private static List<List<String>> getHead(List<EasyExcelTitleInfo> titleInfoList) {
    List<List<String>> result = Lists.newArrayList();
    titleInfoList.stream().sorted(Comparator.comparingInt(EasyExcelTitleInfo::getIndex))
            .forEach(title -> {
                List<String> currHead = Lists.newArrayList();
                currHead.addAll(title.getTitle());
                result.add(currHead);
            });
    return result;
}
ini 复制代码
// 转换数据
private static List<Map<Integer, String>> getDataList(List<?> list, List<EasyExcelTitleInfo> titleInfoList) {
    List<Map<Integer, String>> dataList = Lists.newArrayList();

    if (CollectionUtils.isEmpty(list)) {
        return dataList;
    }
    List<String> columnList = Lists.newArrayList();
    titleInfoList.stream().sorted(Comparator.comparingInt(EasyExcelTitleInfo::getIndex))
            .forEach(x -> columnList.add(x.getColumnName()));
    list.forEach(x -> {
        Map<String, Object> map = BeanUtil.beanToMap(x);
        Map<Integer, String> data = Maps.newLinkedHashMap();
        for (int i = 0; i < columnList.size(); i++) {
            String column = columnList.get(i);
            if (map.containsKey(column)) {
                Object o = map.get(column);
                if (null == o) {
                    data.put(i, "");
                } else if (o instanceof String) {
                    data.put(i, o.toString());
                } else if (o instanceof Date) {
                    data.put(i, DateUtil.formatDateTime((Date) o));
                } else {
                    data.put(i, o.toString());
                }
            } else {
                data.put(i, "");
            }
        }
        dataList.add(data);
    });

    return dataList;
}
scss 复制代码
// 定义导出数据样式
private static WriteCellStyle getWriteCellStyle() {
    WriteCellStyle writeCellStyle = new WriteCellStyle();
    writeCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
    writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
    writeCellStyle.setBorderTop(BorderStyle.THIN);
    writeCellStyle.setBorderLeft(BorderStyle.THIN);
    writeCellStyle.setBorderRight(BorderStyle.THIN);
    writeCellStyle.setBorderBottom(BorderStyle.THIN);
    return writeCellStyle;
}

导入

引入监听类

(此方法是为了解决读取合并单元格时,取值不统一的问题,另外去除整行都为空的数据)

csharp 复制代码
public class EasyExcelBaseListener<T> extends AnalysisEventListener<T> {

    private final static Log log = LogFactory.getLog(EasyExcelBaseListener.class);

    /**
     * 表头
     */
    private Map<Integer, String> headMap;

    /**
     * 批量操作的数据
     */
    private List<T> dataList = new ArrayList<>();

    /**
     * 动态获取表行数
     */
    private int headRowNum = 0;

    /**
     * 获取所有合并单元格信息
     */
    private final List<CellExtra> cellExtraList = new ArrayList<>();

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        this.headMap = headMap;
        headRowNum++;
    }

    /**
     * 每次读取完一条数据触发的方法
     */
    @Override
    public void invoke(T t, AnalysisContext context) {
        dataList.add(t);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("读取excel完成");

        //读取完成 填充合并过的单元格
        if (CollectionUtils.isNotEmpty(cellExtraList)) {
            mergeExcelData(dataList, cellExtraList, headRowNum);
        }
    }


    public Map<Integer, String> getHeadMap() {
        return headMap;
    }

    public void setHeadMap(Map<Integer, String> headMap) {
        this.headMap = headMap;
    }

    public List<T> getDataList() {
        // 移除全部为空的行
        if (CollectionUtils.isNotEmpty(dataList)) {
            try {
                for (int i = dataList.size() - 1; i >= 0; i--) {
                    T t = dataList.get(i);

                    List<Field> fields = Arrays.stream(t.getClass().getDeclaredFields())
                            .filter(f -> f.isAnnotationPresent(ExcelProperty.class))
                            .collect(Collectors.toList());

                    boolean lineAllNull = Boolean.TRUE;
                    for (Field field : fields) {
                        field.setAccessible(true);
                        Object o = field.get(t);
                        if (!ObjectUtil.isNull(o)) {
                            lineAllNull = Boolean.FALSE;
                            break;
                        }
                    }
                    if (lineAllNull) {
                        dataList.remove(i);
                    } else {
                        break;
                    }
                }
            } catch (Exception e) {
                throw new ServiceException("读取数据失败");
            }
        }
        return dataList;
    }

    public void setDataList(List<T> dataList) {
        this.dataList = dataList;
    }


    @Override
    public void extra(CellExtra extra, AnalysisContext context) {
        CellExtraTypeEnum type = extra.getType();
        if (type == CellExtraTypeEnum.MERGE) {
            if (extra.getRowIndex() >= headRowNum) {
                cellExtraList.add(extra);
            }
        }
    }

    /**
     * 处理合并单元格数据,所有行以第一个为准
     */
    private void mergeExcelData(List<T> excelDataList, List<CellExtra> cellExtraList, int headRowNum) {
        cellExtraList.forEach(cellExtra -> {
            int firstRowIndex = cellExtra.getFirstRowIndex() - headRowNum;
            int lastRowIndex = cellExtra.getLastRowIndex() - headRowNum;
            int firstColumnIndex = cellExtra.getFirstColumnIndex();
            int lastColumnIndex = cellExtra.getLastColumnIndex();
            //获取初始值 合并单元格左上角的值
            Object initValue = getInitValueFromList(firstRowIndex, firstColumnIndex, excelDataList);
            //设置值 把合并单元格左上角的值 设置到合并区域的每一个单元格
            for (int i = firstRowIndex; i <= lastRowIndex; i++) {
                for (int j = firstColumnIndex; j <= lastColumnIndex; j++) {
                    setInitValueToList(initValue, i, j, excelDataList);
                }
            }
        });
    }


    /**
     * 设置值 把合并单元格左上角的值 设置到合并区域的每一个单元格
     */
    private void setInitValueToList(Object filedValue, Integer rowIndex, Integer columnIndex, List<T> data) {
        T object = data.get(rowIndex);

        for (Field field : object.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
            if (annotation != null) {
                if (annotation.value()[annotation.value().length - 1].equals(headMap.get(columnIndex))) {
                    try {
                        field.set(object, filedValue);
                        break;
                    } catch (IllegalAccessException e) {
                        log.error("设置合并单元格的值异常:{}", e);
                    }
                }
            }
        }
    }

    /**
     * 获取初始值 合并单元格左上角的值
     */
    private Object getInitValueFromList(Integer firstRowIndex, Integer firstColumnIndex, List<T> data) {
        Object filedValue = null;
        T object = data.get(firstRowIndex);
        for (Field field : object.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
            if (annotation != null) {
                if (annotation.value()[annotation.value().length - 1].equals(headMap.get(firstColumnIndex))) {
                    try {
                        filedValue = field.get(object);
                        break;
                    } catch (IllegalAccessException e) {
                        log.error("设置合并单元格的值异常:{}", e);
                    }
                }
            }
        }
        return filedValue;
    }

}

导入--读取数据

(此方法附带校验表头功能)

scss 复制代码
public static <T> List<T> readExcel(MultipartFile multipartFile, Class<T> tClass) {
    EasyExcelBaseListener<T> listener = new EasyExcelBaseListener<>();
    try {
        readExcel(multipartFile, tClass, listener);
    } catch (Exception e) {
        throw new ServiceException(e.getMessage());
    }
    List<T> dataList = listener.getDataList();
    Map<Integer, String> headMap = listener.getHeadMap();
    List<String> modelHeadList = excelHeadData(tClass);

    for (int i = 0; i < modelHeadList.size(); i++) {
        if (!headMap.containsKey(i) || !StringUtils.equals(headMap.get(i), modelHeadList.get(i))) {
            throw new ServiceException("表头校验:导入模版不正确");
        }
    }

    if (CollectionUtils.isEmpty(dataList)) {
        throw new ServiceException("导入数据为空,请检查导入文件");
    }
    return dataList;
}
scss 复制代码
public static void readExcel(MultipartFile file, Class clazz, AnalysisEventListener listener) throws Exception {
    checkFile(file);
    InputStream inputStream = null;
    try {
        inputStream = new BufferedInputStream(file.getInputStream());
        // 校验表头
        checkTableTitle(file, clazz);

        // 解析每行结果在listener中处理
        EasyExcelFactory.read(file.getInputStream(), clazz, listener)
                .extraRead(CellExtraTypeEnum.MERGE)
                .sheet().doRead();
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
scss 复制代码
/**
 * 校验文件
 */
private static void checkFile(MultipartFile file) {
    //判断文件是否存在
    if (null == file) {
        throw new ServiceException("文件不存在!");
    }
    String fileName = file.getOriginalFilename();
    //判断文件是否是excel文件
    if (StringUtils.isNotBlank(fileName) && !fileName.endsWith(TYPE_OF_XLS) && !fileName.endsWith(TYPE_OF_XLSX)) {
        throw new ServiceException(fileName + "不是excel文件");
    }
}
ini 复制代码
private static <T> List<String> excelHeadData(Class<T> clazz) {
    // 获取所有的私有属性
    Field[] fields = clazz.getDeclaredFields();
    // 遍历所有属性
    List<String> excelHeadData = new ArrayList<>();
    for (Field field : fields) {
        ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
        if (null != annotation) {
            excelHeadData.add(annotation.value()[0]);
        }
    }
    return excelHeadData;
}
ini 复制代码
public static void checkTableTitle(MultipartFile file, Class clazz) throws Exception {
    ByteArrayInputStream is = new ByteArrayInputStream(file.getBytes());
    ExcelReader reader = ExcelUtil.getReader(is);
    List<List<Object>> read = reader.read();
    List<Object> importTitleList = read.get(0);
    if (CollectionUtils.isEmpty(importTitleList)) {
        throw new ServiceException("导入表头为空");
    }

    Field[] declaredFields = clazz.getDeclaredFields();
    List<String> tableTitleList = Lists.newArrayList();
    for (Field field : declaredFields) {
        if (null != field.getAnnotation(ExcelProperty.class)) {
            String[] value = field.getAnnotation(ExcelProperty.class).value();
            if (value.length > 0) {
                tableTitleList.add(value[0]);
            }
        }
    }

    if (importTitleList.size() != tableTitleList.size()) {
        throw new ServiceException("导入模板错误");
    } else {
        for (int i = 0; i < importTitleList.size(); i++) {
            String s = importTitleList.get(i).toString().trim();
            if (!s.equals(tableTitleList.get(i).trim())) {
                throw new ServiceException("导入模板错误");
            }
        }
    }
}
相关推荐
BingoGo3 小时前
PHP 8.5 新特性 闭包可以作为常量表达式了
后端·php
SimonKing3 小时前
Komari:一款专为开发者打造的轻量级服务“看守神器”
后端
间彧3 小时前
Spring Security如何解析JWT,并自行构造SecurityContex
后端
Tech_Lin3 小时前
前端工作实战:如何在vite中配置代理解决跨域问题
前端·后端
间彧3 小时前
在Spring Cloud Gateway中如何具体实现JWT验证和用户信息提取?
后端
间彧3 小时前
SecurityContext在分布式系统(如微服务)中如何传递?有哪些常见方案?
后端
孤廖3 小时前
C++ 模板再升级:非类型参数、特化技巧(含全特化与偏特化)、分离编译破解
linux·服务器·开发语言·c++·人工智能·后端·深度学习
林希_Rachel_傻希希3 小时前
Express 入门全指南:从 0 搭建你的第一个 Node Web 服务器
前端·后端·node.js