Excel动态列导出,使用SpringMvc的AbstractXlsxView

导出excel,动态列,设置批注,设置下拉框选值,也可直接生成文件

问题原因

在开发中一定会遇见不一样的导出功能,比如说导出报表,导出列表数据,下载模板等。
公司最近在重构一些功能,此处涉及每一个列表的导出功能,且列有扩展字段。

pom依赖及版本

springmvc:5.3.9
poi:4.1.1
hutool:5.5.4

代码

package com.hulunbuir.admin.controller;

import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.poi.excel.style.StyleUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.web.servlet.view.document.AbstractXlsxView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 导出excel视图
 * </p>
 *
 * @author wangjunming
 */
@Slf4j
@Getter
public class ExcelViewImproveLine extends AbstractXlsxView {

    /**
     * 导出的文件名
     */
    private String filename;

    /**
     * 是否需要直接转换为文件
     */
    private Boolean convertFileFLag = Boolean.FALSE;

    /**
     * 设置是否需要下载
     *
     * @param convertFileFLag 默认-false-需要下载。true-不需要下载。
     */
    public void setConvertFileFLag(Boolean convertFileFLag) {
        this.convertFileFLag = convertFileFLag;
    }

    /**
     * 导出的文件指定文件路径,默认只有在 convertFileFLag 为true的情况下使用
     */
    private String fileAbsolutePath;

    public void setFileAbsolutePath(String fileAbsolutePath) {
        this.fileAbsolutePath = fileAbsolutePath;
    }

    /**
     * 多个sheet页
     */
    private final List<ExcelSheet> sheets;

    @Getter
    @Setter
    public static class ExcelSheet {
        {
            timePattern = "yyyy-MM-dd HH:mm";
            datePattern = "yyyy-MM-dd";
            scale = 2;
        }

        /**
         * sheet名称
         */
        private String sheetName;
        /**
         * 标题头字段编码 + 标题头翻译
         */
        private List<ExportField> fields;
        /**
         * 数据集合
         */
        private Collection<?> data;
        /**
         * 导出的对象
         */
        private Class<?> model;
        /**
         * 日期类型默认转换的格式,针对于 Date 、 LocalDateTime
         */
        private String timePattern;
        /**
         * 日期类型默认转换的格式 针对于 LocalDate
         */
        private String datePattern;
        /**
         * 四舍五入的位数
         */
        private int scale;

        private ExcelSheet() {

        }

        private ExcelSheet(String sheetName, Class<?> model, Collection<?> data) {
            this.sheetName = sheetName;
            this.data = data;
            this.model = model;
            fillAnnotationTitles(model, this);
        }

        private ExcelSheet(String sheetName, List<ExportField> fieldList, Collection<?> data) {
            this.sheetName = sheetName;
            this.data = data;
            fillCustomizeTitles(this, fieldList);
        }

    }

    /**
     * 创建自定义列的excel
     *
     * @param sheetName sheet页
     * @param fieldList 字段集合
     * @param data      数据集
     * @return ExcelSheet
     */
    public static ExcelSheet createSheet(String sheetName, List<ExportField> fieldList, Collection<?> data) {
        return new ExcelSheet(sheetName, fieldList, data);
    }

    /**
     * 从注解中获取导出字段名
     * // TODO 在此处将换掉注解,或者使用自定注解此处可以自定义改进,或者支持多个注解,即第一个注解没找到时,找第二个注解进行构建所需参数
     */
    private static void fillAnnotationTitles(Class<?> model, ExcelViewImproveLine.ExcelSheet sheet) {
        //将通过对model中的 Excel注解进行解析,然后设置标题头。
        List<ExcelViewImproveLine.ExportField> easyPoiExcelFieldList = Arrays.stream(model.getDeclaredFields())
                .filter(f -> f.isAnnotationPresent(Excel.class)).map(f -> {

                    String fieldCode = f.getName();
                    ExcelViewImproveLine.ExportField exportField = new ExcelViewImproveLine.ExportField();
                    exportField.setFieldCode(fieldCode);

                    Excel excelAnnotation = f.getAnnotation(Excel.class);
                    String name = excelAnnotation.name();
                    exportField.setFieldName(name);

                    int index = excelAnnotation.fixedIndex();
                    exportField.setOrder(index);

                    return exportField;
                }).sorted().collect(Collectors.toList());

        if (CollUtil.isNotEmpty(easyPoiExcelFieldList)) {
            log.info("此处使用easy-poi内部注解设置动态列");
            sheet.setFields(easyPoiExcelFieldList);
            return;
        }

    }

    /**
     * 自定义标题头
     */
    private static void fillCustomizeTitles(ExcelSheet sheet, List<ExportField> fields) {
        fields = fields.stream().sorted().collect(Collectors.toList());
        sheet.setFields(fields);
    }

    /**
     * 多个sheet页进行导出
     *
     * @param filename 文件名
     * @param sheets   sheet页数组,可使用静态构建方法: ExcelViewImprove.createSheet()
     */
    public ExcelViewImproveLine(String filename, ExcelSheet... sheets) {
        this.filename = filename;
        this.sheets = new ArrayList<>();
        this.sheets.addAll(Arrays.asList(sheets));
    }

    /**
     * 动态列作为参数进行导出
     *
     * @param filename  文件名
     * @param sheetName sheet页名称
     * @param fieldList 列字段编码,列字段名称,列排序
     * @param data      数据集合
     */
    public ExcelViewImproveLine(String filename, String sheetName, List<ExportField> fieldList, Collection<?> data) {
        this.filename = filename;
        ExcelSheet sheet = createSheet(sheetName, fieldList, data);
        sheets = Collections.singletonList(sheet);
    }

    /**
     * 真正执行创建excel的方法
     *
     * @param map
     * @param workbook
     * @param request
     * @param response
     * @throws Exception
     */
    @Override
    protected void buildExcelDocument(Map<String, Object> map, Workbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception {
        for (ExcelSheet sheet : sheets) {
            buildExcel(workbook, sheet);
        }
        //此处判断是否需要将 workbook 直接转换为文件,如果需要则在当前文件夹下,或指定文件夹下生成excel文件。
        if (convertFileFLag) {
            downloadExcel(workbook);
            return;
        }
        //设置响应为文件下载
        response.setContentType("application/force-download");
        if (StrUtil.isBlank(filename)) {
            filename = "Excel导出.xlsx";
        }
        response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
    }

    /**
     * 创建excel表格
     *
     * @param workbook 工作簿
     * @param param    所需参数
     */
    private void buildExcel(Workbook workbook, ExcelSheet param) {
        Sheet sheet;
        if (StrUtil.isNotBlank(param.getSheetName())) {
            sheet = workbook.createSheet(param.getSheetName());
        } else {
            sheet = workbook.createSheet();
        }
        List<ExportField> fields = param.getFields();
        log.info("标题行的信息是:{}", JSONUtil.toJsonStr(fields));
        int rowNumber = 0;
        if (CollUtil.isNotEmpty(fields)) {
            // 创建标题行
            createHeader(sheet, workbook, rowNumber, fields);
            ++rowNumber;
        }
        Collection<?> dataList = param.getData();
        if (CollUtil.isEmpty(dataList)) {
            return;
        }
        //创建数据行信息
        createDataLine(workbook, sheet, rowNumber, fields, param, dataList);
    }

    /**
     * 创建标题行
     */
    private void createHeader(Sheet sheet, Workbook workbook, int rowNumber, List<ExportField> fields) {
        //进行构建表头
        Row header = sheet.createRow(rowNumber);
        //设置行高
        header.setHeight((short) 420);
        for (int column = 0; column < fields.size(); column++) {
            ExportField exportField = fields.get(column);
            String fieldName = exportField.getFieldName();
            Cell cell = header.createCell(column);
            //设置当前单元格的内容
            cell.setCellValue(fieldName);
            //创建样式
            createHeaderCellStyleFont(cell, workbook, exportField);
            //创建批注
            createHeaderComment(cell, sheet, exportField);
            //创建下拉框选项值
            creatHeadSelectCellStyle(workbook, sheet, rowNumber, column, exportField);
            //设置单元格宽度
            sheet.setColumnWidth(column, getColumnWidth(cell));
        }
    }

    /**
     * 获取默认的宽度
     *
     * @param cell 单元格信息
     * @return int
     */
    public int getColumnWidth(Cell cell) {
        return 7000;
    }

    /**
     * 创建下拉框的值
     *
     * @param workbook    工作簿
     * @param sheet       标签页
     * @param rowNumber   第几行
     * @param column      第几列
     * @param exportField 字段信息
     */
    private void creatHeadSelectCellStyle(Workbook workbook, Sheet sheet, int rowNumber, int column, ExportField exportField) {
        List<String> fieldCodeSelect = exportField.getFieldCodeSelect();
        if (CollUtil.isEmpty(fieldCodeSelect)) {
            return;
        }
        String[] fieldSelectArray = fieldCodeSelect.toArray(new String[0]);
        DataValidationHelper dataValidationHelper = sheet.getDataValidationHelper();
        creatDropDownList(sheet, dataValidationHelper, fieldSelectArray, rowNumber, getLastRowNum(sheet), column, column);
    }

    /**
     * 设置下拉框的最大行是 十万
     *
     * @param sheet 当前sheet对象
     * @return 100000
     */
    private Integer getLastRowNum(Sheet sheet) {
        return 100000;
    }

    /**
     * 设置标题样式和颜色
     *
     * @param cell        单元格
     * @param workbook    工作簿
     * @param exportField 是否必填,如果是必填项则字体颜色是红色
     */
    private void createHeaderCellStyleFont(Cell cell, Workbook workbook, ExportField exportField) {
        CellStyle headCellStyle = StyleUtil.createHeadCellStyle(workbook);
        Boolean required = exportField.getRequired();
        if (!required) {
            headCellStyle.setFont(blackFont(workbook));
            cell.setCellStyle(headCellStyle);
            return;
        }
        headCellStyle.setFont(redFont(workbook));
        cell.setCellStyle(headCellStyle);
    }

    /**
     * 创建批注
     *
     * @param cell        当前单元格
     * @param sheet       标签页
     * @param exportField 批注信息
     */
    private void createHeaderComment(Cell cell, Sheet sheet, ExportField exportField) {
        String fieldNotes = exportField.getFieldNotes();
        if (StrUtil.isBlank(fieldNotes)) {
            return;
        }
        // 创建绘图对象
        Drawing<?> patriarch = sheet.createDrawingPatriarch();
        // 前四个参数是坐标点,后四个参数是编辑和显示批注时的大小.
        Comment comment = patriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 12, 12, (short) 16, 16));
        // 输入批注信息
        comment.setString(new XSSFRichTextString(fieldNotes));
        // 添加作者,选中B5单元格,看状态栏
        comment.setAuthor("系统");
        cell.setCellComment(comment);
    }

    /**
     * 红色字
     */
    private Font redFont(Workbook workbook) {
        Font font = StyleUtil.createFont(workbook, HSSFColor.HSSFColorPredefined.RED.getIndex(), (short) 14, "宋体");
        font.setBold(true);
        return font;
    }

    /**
     * 黑色字
     */
    private Font blackFont(Workbook workbook) {
        Font font = StyleUtil.createFont(workbook, HSSFColor.HSSFColorPredefined.BLACK.getIndex(), (short) 14, "宋体");
        font.setBold(true);
        return font;
    }

    /**
     * 创建下拉框
     * <p>
     * 参考:https://www.csdn.net/tags/OtDagg3sODUxMTQtYmxvZwO0O0OO0O0O.html
     *
     * @param taskInfoSheet 当前标签页
     * @param helper        用于创建下拉框的工具
     * @param list          下拉框的数据
     * @param firstRow      开始第一行
     * @param lastRow       最后一行
     * @param firstCol      开始第一列
     * @param lastCol       最后一列
     */
    private static void creatDropDownList(Sheet taskInfoSheet, DataValidationHelper helper, String[] list,
                                          Integer firstRow, Integer lastRow, Integer firstCol, Integer lastCol) {
        //获取需要生成下拉框的格
        CellRangeAddressList addressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);
        //设置下拉框数据
        DataValidationConstraint constraint = helper.createExplicitListConstraint(list);
        DataValidation dataValidation = helper.createValidation(constraint, addressList);
        //处理Excel兼容性问题
        if (dataValidation instanceof XSSFDataValidation) {
            dataValidation.setSuppressDropDownArrow(true);
            dataValidation.setShowErrorBox(true);
        } else {
            dataValidation.setSuppressDropDownArrow(false);
        }
        taskInfoSheet.addValidationData(dataValidation);
    }

    /**
     * 创建数据行信息
     *
     * @param workbook  工作簿
     * @param sheet     标签页
     * @param rowNumber 需要创建第几行
     * @param fields    字段名信息
     * @param param     excel导出所需要的相关参数
     * @param dataList  数据集合
     */
    private void createDataLine(Workbook workbook, Sheet sheet, int rowNumber,
                                List<ExportField> fields, ExcelSheet param, Collection<?> dataList) {
        //构建数据行
        for (Object item : dataList) {
            BeanMap beanMap = BeanMap.create(item);
            Row row = sheet.createRow(rowNumber);
            for (int column = 0; column < fields.size(); column++) {
                ExportField exportField = fields.get(column);
                String fieldCode = exportField.getFieldCode();
                Cell cell = row.createCell(column);
                String filedValue;
                try {
                    filedValue = getValue(item, beanMap, fieldCode, param);
                } catch (Exception e) {
                    log.error("导出数据行异常,字段名:{},此数据对象信息:{},赋值异常", JSONUtil.toJsonStr(exportField), JSONUtil.toJsonStr(beanMap), e);
                    filedValue = "";
                }
                cell.setCellStyle(StyleUtil.createDefaultCellStyle(workbook));
                cell.setCellValue(filedValue);
                sheet.setColumnWidth(column, getColumnWidth(cell));
            }
            ++rowNumber;
        }

    }

    /**
     * 获取当前集合对象对应的字段值
     *
     * @param beanMap   当前对象的字段与值的map
     * @param fieldCode 字段名
     * @param param     设置参数
     * @return String
     */
    private String getValue(Object item, BeanMap beanMap, String fieldCode, ExcelSheet param) {
        String value = "";
        Object obj = beanMap.get(fieldCode);
        if (obj == null) {
            return value;
        }
        Class<?> propertyType = beanMap.getPropertyType(fieldCode);
        if (BigDecimal.class.isAssignableFrom(propertyType)) {
            value = ((BigDecimal) obj).setScale(param.getScale(), RoundingMode.HALF_UP).toString();
        } else if (Date.class.isAssignableFrom(propertyType)) {
            value = new SimpleDateFormat(param.getTimePattern()).format(obj);
        } else if (LocalDate.class.isAssignableFrom(propertyType)) {
            value = ((LocalDate) obj).format(DateTimeFormatter.ofPattern(param.getDatePattern()));
        } else if (LocalDateTime.class.isAssignableFrom(propertyType)) {
            value = ((LocalDateTime) obj).format(DateTimeFormatter.ofPattern(param.getTimePattern()));
        } else {
            value = (String) obj;
        }
        return value;
    }


    /**
     * 直接生成excel文件
     *
     * @param workbook 工作簿
     */
    private void downloadExcel(Workbook workbook) {
        byte[] excelBytes = writeOuts(workbook);
        if (StrUtil.isBlank(fileAbsolutePath)) {
            fileAbsolutePath = "/gen_excel";
        }
        if (StrUtil.isBlank(filename)) {
            filename = "excel_by_view.xlsx";
        }
        //创建文件件
        FileUtil.mkdir(FileUtil.file(fileAbsolutePath));
        fileAbsolutePath = fileAbsolutePath + "\\" + filename;
        fileAbsolutePath = fileAbsolutePath.replaceAll("\\\\", "/");
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(fileAbsolutePath);
            IoUtil.write(fileOutputStream, Boolean.TRUE, excelBytes);
        } catch (Exception e) {
            log.error("生成文件失败,请检查文件路径是否正确。", e);
        } finally {
            IoUtil.close(fileOutputStream);
        }
        log.info("生成的文件名...- file:///{}", fileAbsolutePath);
    }

    /**
     * 将 workbook 转换为 字节数组
     *
     * @param workbook 工作簿
     * @return byte[]
     */
    public static byte[] writeOuts(Workbook workbook) {
        if (workbook == null) {
            return new byte[]{};
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            workbook.write(outputStream);
        } catch (IOException e) {
            log.error("写入流异常", e);
        } finally {
            try {
                workbook.close();
            } catch (IOException e) {
                log.error("关闭workbook异常", e);
            }
        }
        return outputStream.toByteArray();
    }


    /**
     * excel的标题行信息
     */
    @Getter
    @Setter
    @ToString
    @Accessors(chain = true)
    public static class ExportField implements Comparable<ExportField> {
        /**
         * 字段名
         */
        private String fieldCode;
        /**
         * 字段的中文说明
         */
        private String fieldName;
        /**
         * 字段是否必填
         */
        private Boolean required = Boolean.FALSE;
        /**
         * 字段的批注
         */
        private String fieldNotes;
        /**
         * 字段名所属的下拉框的值
         */
        private List<String> fieldCodeSelect;
        /**
         * 排序
         */
        private int order;

        @Override
        public int hashCode() {
            int result = fieldCode.hashCode();
            result = 31 * result + fieldName.hashCode();
            return result;
        }

        @Override
        public int compareTo(ExportField exportField) {
            return exportField.order;
        }
    }


}

使用

main方法:

    public static void main(String[] args) throws Exception {
        List<ExcelViewImproveLine.ExportField> exportTitleList = CollUtil.newArrayList();
        //初始化标题
        exportTitleList.add(new ExcelViewImproveLine.ExportField().setFieldCode("orderName").setFieldNotes("订单名称不能为空。").setFieldName("订单名称").setOrder(1));
        exportTitleList.add(new ExcelViewImproveLine.ExportField().setFieldCode("orderDesc").setRequired(Boolean.TRUE).setFieldName("订单描述").setOrder(1));
        exportTitleList.add(new ExcelViewImproveLine.ExportField().setFieldCode("orderStatus").setFieldName("订单状态")
                .setFieldCodeSelect(CollUtil.newArrayList("待付款", "已取消", "待发货", "已收货"))
                .setOrder(1));
        ExcelViewImproveLine excelViewImproveLine = new ExcelViewImproveLine("订单列表.xlsx", "订单", exportTitleList, CollUtil.newArrayList());
        excelViewImproveLine.setConvertFileFLag(Boolean.TRUE);
        excelViewImproveLine.setFileAbsolutePath("");
        excelViewImproveLine.buildExcelDocument(CollUtil.newHashMap(), new XSSFWorkbook(), null, null);

    }

controller接口使用:

    @ApiOperation("下载导入模板")
    @GetMapping("/dynamic-improve-line-export")
    public ExcelViewImproveLine improveLineExportList() {
        List<ExcelViewImproveLine.ExportField> exportTitleList = CollUtil.newArrayList();
        //初始化标题
        exportTitleList.add(new ExcelViewImproveLine.ExportField().setFieldCode("orderName").setFieldNotes("订单名称不能为空。").setFieldName("订单名称").setOrder(1));
        exportTitleList.add(new ExcelViewImproveLine.ExportField().setFieldCode("orderDesc").setRequired(Boolean.TRUE).setFieldName("订单描述").setOrder(1));
        exportTitleList.add(new ExcelViewImproveLine.ExportField().setFieldCode("orderStatus").setFieldName("订单状态")
                .setFieldCodeSelect(CollUtil.newArrayList("待付款", "已取消", "待发货", "已收货"))
                .setOrder(1));
        return new ExcelViewImproveLine("订单列表.xlsx", "订单", exportTitleList, CollUtil.newArrayList());;
    }

GITHUB:

git文件地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值