掘金 后端 ( ) • 2024-04-07 16:25

公司业务大范围改造,包括一些代码重构,数据来源迁移等,其中很小的一个点就是对列表数据的导出。当时参考了一下公司前辈写的代码。代码不好贴出来,但是流程基本如下:

public void exportData(Object...params){
    // 参数校验,包括非法校验和时间跨度校验 
    // 封装Workbook(在方法里进行了导出数据的查询)
    // 设置名字
    // 设置response参数
    // 具体的数据写出
    // 关闭相关资源
}

这么看过来似乎流程有些混乱,但是没法去评判之前人写代码的好与坏,毕竟不知道当时是什么样的开发周期和情况。

文件名设置,文件内容写出,参数设置,关闭资源。等等这些,完全可以封装到一起,不然每次写导出,都和这个一样写一串,对于后来者而言就是灾难了。

我想要的是抽成公用的方法,参数是传入具体的需要导出的list数据,然后方法里把别的都干了。当然,就单单这个功能而言,easyexcel有更加完备的技术实现。我看了看代码里,并没有引入easyxcel的依赖,本着小心谨慎引入新依赖的想法,决定自己封一个比较类似的。

接下来就直接贴代码了,可以将如下几个类放在一个包下。

ExcelWriteDataBuilder用于构建导出时需要的文件名,sheet名等数据:


import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.io.OutputStream;

@Slf4j
@Data
public class ExcelWriteDataBuilder {

    private String fileName;
    private String sheetName;
    private Class clazz;
    private OutputStream outputStream;


    public static ExcelWriteDataBuilder outputStream(OutputStream outputStream) {
        ExcelWriteDataBuilder data = new ExcelWriteDataBuilder();
        data.outputStream = outputStream;
        return data;
    }

    public ExcelWriteDataBuilder clazz(Class clazz) {
        this.clazz = clazz;
        return this;
    }

    public ExcelWriteDataBuilder sheetName(String sheetName) {
        this.sheetName = sheetName;
        return this;
    }

    public ExcelExportHandler fileName(String fileName) {
        this.fileName = fileName;
        ExcelExportHandler excelExportHandler = new ExcelExportHandler();
        excelExportHandler.setWriteDataBuilder(this);
        return excelExportHandler;
    }
}

ExcelExportHandler作为方法调用入口


import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

@Data
@Slf4j
public class ExcelExportHandler {
    private ExcelWriteDataBuilder writeDataBuilder;

    public <T extends ExcelWriteTool,R> void write(List<R> list, Class<T> clazz) {
        try {
            log.info( "write excel start ==== ");
            T t = clazz.newInstance();
            t.setWriteDataBuilder(writeDataBuilder);
            t.doAction(list);
        } catch (Exception e) {
            log.error("write error e :", e);
        }
    }
}

ExcelWriteTool 具体的写excel逻辑,同时支持字类对它进行扩展,支持自定义的excel格式,比如前后增加特定提示语等。

import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Data
public class ExcelWriteTool {
    private ExcelWriteDataBuilder writeDataBuilder;
    // 当前行数
    private Integer index;
    private XSSFWorkbook wb;
    private XSSFSheet sheet;
    // 需要写出的属性
    private List<Field> fields;

    @SneakyThrows
    public void pre() {
        // 前置操作
        HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        if (resp != null) {
            log.info("ExcelWriteTool.pre resp set ");
            resp.reset();
            resp.setContentType("application/vnd.ms-excel;charset=utf-8");
            resp.setHeader("Content-Disposition", "attachment;filename=" + new String((writeDataBuilder.getFileName() + ".xlsx").getBytes(), "iso-8859-1"));
        }
    }

    public <T> void action(List<T> data) {
        // 写表头
        writeHead();
        // 写数据
        writeData(data);
    }

    public <T> void writeData(List<T> data) {
        // 具体的写数据逻辑
        log.info("writeData start ");
        for (T item : data) {
            XSSFRow row = sheet.createRow(index++);
            for (int i = INT_ZERO; i < fields.size(); i++) {
                val field = fields.get(i);
                createCell(row, i, ReflectUtils.getValue(item, field, String.class));
            }
        }
        log.info("writeData end  ");
    }


    public void writeHead() {
        log.info("writeHead start ");
        // 可能在实现类中有些前置操作属性发生变化,所以判空
        index = Optional.ofNullable(index).orElse(INT_ZERO);
        wb = Optional.ofNullable(wb).orElseGet(XSSFWorkbook::new);
        sheet = Optional.ofNullable(sheet).orElseGet(() -> wb.createSheet(writeDataBuilder.getSheetName()));
        XSSFRow titleRow = sheet.createRow(index);
        // 获取实体的所有属性
        List<Field> fs = ReflectUtils.getFields(writeDataBuilder.getClazz());
        fields = Optional.ofNullable(fields).orElseGet(() -> fs.stream()
                // 过滤 不可见的
                .filter(item -> item.getAnnotation(ExcelExport.class).visible())
                // 排序
                .sorted(Comparator.comparingInt(item -> item.getAnnotation(ExcelExport.class).index()))
                .collect(Collectors.toList()));
        for (int i = INT_ZERO; i < fields.size(); i++) {
            String titleCell = fields.get(i).getAnnotation(ExcelExport.class).desc();
            String[] cellPair = titleCell.split(COLON);
            createCell(titleRow, i, cellPair[INT_ZERO]);
            String width = cellPair.length == INT_ONE ? DEFAULT_EXCEL_WIDTH : cellPair[INT_ONE];
            sheet.setColumnWidth(i, 256 * Integer.parseInt(width) + 184);
        }
        // index自增
        index++;
        log.info("ExcelWriteTool.writeHead write head success");
    }

    public <T> void doAction(List<T> data) {
        try {
            pre();
            action(data);
            after();
        } catch (Exception e) {
            log.error("ExcelWriteTool.doAction error e:", e);
        } finally {
            // 关闭资源
            closeResource();
        }
    }

    private void closeResource() {
        // 关闭资源
        try {
            if (writeDataBuilder.getOutputStream() != null) {
                writeDataBuilder.getOutputStream().close();
            }
        } catch (Exception e) {
            log.error("ExcelWriteTool.after close outStream error e:", e);
        }

        if (wb != null) {
            try {
                wb.close();
            } catch (IOException e) {
                log.error("ExcelWriteTool.after close wb error e:", e);
            }
        }
    }

    public void after() throws IOException {
        log.info("ExcelWriteTool.after start");
        wb.write(writeDataBuilder.getOutputStream());
    }
    
    private static Cell createCell(Row row, int cellNum, String value) {
        Cell cell = row.createCell(cellNum, Cell.CELL_TYPE_STRING);
        cell.setCellValue(getNotNullStr(value));
        return cell;
    }

}

ExcelExport定义注解,对实体需要写出的字段进行标识


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelExport {

    /**
     * 字段描述:单元格宽度
     */
    String desc();

    /**
     * index
     */
    int index();

    /**
     * 可见性
     */
    boolean visible() default true;
}

注解使用场景,需要导出的实体类上

image.png

MerchantApplyWideWriter继承ExcelWriteTool,可以进行一些功能的扩展


import java.util.List;

public class MerchantApplyWideWriter extends ExcelWriteTool{

    public <T> void action(List<T> data) {
        // 进行定制化的操作...
        super.action(data);
    }
}

使用姿势

// write Data
ExcelWriteDataBuilder.outputStream(resp.getOutputStream())
        .sheetName("商家入驻申请信息")
        .clazz(MerchantApplyWideExcelData.class)
        .fileName("商家入驻申请信息")
        .write(exportData, MerchantApplyWideWriter.class);

其中clazz里面是需要导出的实体类型,write里面放待导出数据和导出的处理器。

这样的话,每次新的导出需求,需要修改的地方,只有新增实体,实体上写注解,然后如果没有特定的格式问题,甚至都可以直接照搬上面的使用姿势,大大缩短了开发流程,也更加规范和统一。

当然了,代码肯定会存在一些瑕疵,比如一些魔法值的处理,类的冗余,或者其他可能出现优化的地方。

时间问题,这些目前没有处理,也欢迎大佬指证和补充。