springboot之Exel工具类

背景

后台需要批量处理excel数据,常见的就是导入导出了。故自己写了一个小工具。

依赖

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

工具类

java 复制代码
package org.test.exceldemo.excel;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.CellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.read.listener.PageReadListener;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * Excel 导入导出工具类
 * 包含只读写、只写、流式读、流式写
 */
public class EasyExcelPipelineUtil {
    /**
     * ⭐⭐⭐ 同步读取(小文件用)
     */
    public static <T> List<T> readSync(InputStream inputStream, Class<T> clazz) {
        return EasyExcel.read(inputStream)
                .head(clazz)
                .sheet()
                .doReadSync();
    }

    /**
     * ⭐⭐⭐ 分页读取(推荐)
     */
    public static <T> void readByPage(InputStream inputStream,
                                      Class<T> clazz,
                                      Consumer<List<T>> consumer,
                                      int pageSize) {

        EasyExcel.read(inputStream, clazz,
                        new PageReadListener<T>(consumer, pageSize))
                .sheet()
                .doRead();
    }

    /**
     * ⭐⭐⭐ MultipartFile 分页读取
     */
    public static <T> void readByMultipart(MultipartFile file,
                                           Class<T> clazz,
                                           Consumer<List<T>> consumer,
                                           int pageSize) throws IOException {

        readByPage(file.getInputStream(), clazz, consumer, pageSize);
    }

    /**
     * ⭐⭐⭐ URL 分页读取
     */
    public static <T> void readByUrl(String url,
                                     Class<T> clazz,
                                     Consumer<List<T>> consumer,
                                     int pageSize) throws Exception {

        try (InputStream in = new URL(url).openStream()) {
            readByPage(in, clazz, consumer, pageSize);
        }
    }

    /**
     * ====================================
     * ⭐⭐⭐⭐⭐ 直接写 Excel(最常用导出)
     * ====================================
     */
    public static <T> void write(String filePath,
                                 Class<T> clazz,
                                 List<T> data, String sheetName) {

        EasyExcel.write(filePath, clazz)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(new ExcelCellWriteWidthConfig())
                .sheet(sheetName)
                .doWrite(data);
    }

    /**
     * ⭐ 写到 OutputStream(浏览器下载用)
     */
    public static <T> void write(OutputStream outputStream,
                                 Class<T> clazz,
                                 List<T> data, String sheetName) {

        EasyExcel.write(outputStream, clazz)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(new ExcelCellWriteWidthConfig())
                .sheet(sheetName)
                .doWrite(data);
    }

    /**
     * ⭐⭐⭐⭐ 超大数据流式写(百万行推荐)
     */
    public static <T> void writeStream(String filePath,
                                       Class<T> clazz,
                                       Consumer<ExcelWriter> writerConsumer) {

        ExcelWriter writer = EasyExcel.write(filePath, clazz)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(new ExcelCellWriteWidthConfig())
                .build();

        try {
            writerConsumer.accept(writer);
        } finally {
            writer.finish();
        }
    }
// 调用样例
//    EasyExcelUtil.writeStream(
//            "D:/big.xlsx",
//    UserExportVO.class,
//    writer -> {
//
//        for (int i = 0; i < 100; i++) {
//
//            List<UserExportVO> page =
//                    userService.queryPage(i);
//
//            EasyExcelUtil.writeBatchSheet(
//                    writer,
//                    page,
//                    0,
//                    "用户表"
//            );
//        }
//    }
//);

    /**
     * ⭐ 分批写 sheet
     */
    public static <T> void writeBatchSheet(ExcelWriter writer,
                                           List<T> data,
                                           int sheetNo,
                                           String sheetName) {

        WriteSheet sheet = EasyExcel.writerSheet(sheetNo, sheetName)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(new ExcelCellWriteWidthConfig())
                .build();
        writer.write(data, sheet);
    }

    /**
     * ⭐⭐⭐ 核心流式分页处理(真正生产级)
     * ⭐⭐⭐ 不关心读写,只需要处理数据即可,包含excel文件的读写
     */
    private static <IN, OUT> void processStream(
            InputStream inputStream,
            OutputStream outputStream,
            Class<IN> inClass,
            Class<OUT> outClass,
            Function<List<IN>, List<OUT>> mapper,
            int pageSize) {

        ExcelWriter writer = EasyExcel.write(outputStream, outClass)
                        .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                        .registerWriteHandler(new ExcelCellWriteWidthConfig())
                        .build();
        WriteSheet sheet = EasyExcel.writerSheet("result").build();

        EasyExcel.read(inputStream, inClass,
                        new PageReadListener<IN>(pageData -> {
                            List<OUT> result = mapper.apply(pageData);
                            writer.write(result, sheet);
                        }, pageSize))
                .sheet()
                .doRead();

        writer.finish();
    }

    /**
     * =========================
     * ⭐ 本地文件路径
     * =========================
     */
    public static <IN, OUT> void processByFilePath(
            String inputFile,
            String outputFile,
            Class<IN> inClass,
            Class<OUT> outClass,
            Function<List<IN>, List<OUT>> mapper,
            int pageSize) throws Exception {

        try (InputStream in = new FileInputStream(inputFile);
             OutputStream out = new FileOutputStream(outputFile)) {

            processStream(in, out, inClass, outClass, mapper, pageSize);
        }
    }

    /**
     * =========================
     * ⭐ URL 网络文件
     * =========================
     */
    public static <IN, OUT> void processByUrl(
            String fileUrl,
            String outputFile,
            Class<IN> inClass,
            Class<OUT> outClass,
            Function<List<IN>, List<OUT>> mapper,
            int pageSize) throws Exception {

        try (InputStream in = new URL(fileUrl).openStream();
             OutputStream out = new FileOutputStream(outputFile)) {

            processStream(in, out, inClass, outClass, mapper, pageSize);
        }
    }

    /**
     * =========================
     * ⭐ MultipartFile(Web上传)
     * =========================
     */
    public static <IN, OUT> void processByMultipart(
            MultipartFile file,
            String outputFile,
            Class<IN> inClass,
            Class<OUT> outClass,
            Function<List<IN>, List<OUT>> mapper,
            int pageSize) throws Exception {
        try (InputStream in = file.getInputStream();
             OutputStream out = new FileOutputStream(outputFile)) {

            processStream(in, out, inClass, outClass, mapper, pageSize);
        }
    }

    /**
     * =========================
     * ⭐ InputStream 通用入口
     * =========================
     */
    public static <IN, OUT> void processByStream(
            InputStream inputStream,
            OutputStream outputStream,
            Class<IN> inClass,
            Class<OUT> outClass,
            Function<List<IN>, List<OUT>> mapper,
            int pageSize) {

        processStream(inputStream, outputStream, inClass, outClass, mapper, pageSize);
    }

    /**
     * 自动计算列宽
     *
     */
    private static class ExcelCellWriteWidthConfig extends AbstractColumnWidthStyleStrategy {
        private final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();

        @Override
        protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer integer, Boolean isHead) {
            boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
            if (needSetWidth) {
                Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>());

                Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
                // 单元格文本长度大于60换行
                if (columnWidth >= 0) {
                    if (columnWidth > 60) {
                        columnWidth = 60;
                    }
                    Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
                    if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
                        maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
                        Sheet sheet = writeSheetHolder.getSheet();
                        sheet.setColumnWidth(cell.getColumnIndex(), columnWidth * 2 * 256);
                    }
                }
            }
        }

        /**
         * 计算长度
         */
        private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
            if (isHead) {
                return cell.getStringCellValue().getBytes().length;
            } else {
                CellData<?> cellData = cellDataList.get(0);
                CellDataTypeEnum type = cellData.getType();
                if (type == null) {
                    return -1;
                } else {
                    switch (type) {
                        case STRING:
                            // 换行符(数据需要提前解析好)
                            int index = cellData.getStringValue().indexOf("\n");
                            return index != -1 ?
                                    cellData.getStringValue().substring(0, index).getBytes().length + 1 : cellData.getStringValue().getBytes().length + 1;
                        case BOOLEAN:
                            return cellData.getBooleanValue().toString().getBytes().length;
                        case NUMBER:
                            return cellData.getNumberValue().toString().getBytes().length;
                        default:
                            return -1;
                    }
                }
            }
        }
    }
}

举例

定义exel文件数据类,定义数据处理函数即可。

导入类

java 复制代码
package org.test.exceldemo.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 *  导入数据类,对应excel表格字段
 */
@Data
public class UserImportVO {

    @ExcelProperty("User Name")
    private String name;

    @ExcelProperty("User Age")
    private Integer age;
}

导出类

java 复制代码
package org.test.exceldemo.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * 导入结果类
 */
@Data
public class UserImportResultVO {

    @ExcelProperty("User Name")
    private String name;

    @ExcelProperty("User Age")
    private Integer age;

    @ExcelProperty("Import Result")
    private String result;
}

数据处理函数类

java 复制代码
package org.test.exceldemo.excel;

import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 *  
 */
@Service
public class TestExcelDemoService {
    /**
     * excel数据处理类
     * @param list excel数据
     * @param globalUnique 全局唯一
     * @return 处理结果
     */
    public List<UserImportResultVO> convertPage(List<UserImportVO> list, Set<String> globalUnique) {

        List<UserImportResultVO> result = new ArrayList<>();

        for (UserImportVO in : list) {
            UserImportResultVO out = new UserImportResultVO();
            BeanUtils.copyProperties(in, out);
            // 判断数据重复
            if (!globalUnique.add(in.getName())){
                out.setResult("重复数据");
                result.add(out);
                continue;
            }
            // 数据处理逻辑

            // 处理结果
            out.setResult("success");
            result.add(out);
        }
        return result;
    }
}

模拟请求处理类

java 复制代码
@RestController
@RequestMapping("/")
public class TestExcelController {
    @Resource
    private TestExcelDemoService testExcelDemoService;

    @PostMapping(path = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
        //通过文件后缀名或文件魔数进行更可靠的校验
        String originalFilename = file.getOriginalFilename();
        // 只处理excel文件
        if (originalFilename != null && !originalFilename.toLowerCase().endsWith(".xlsx")) {
            return "仅支持 EXCEL 文件";
        }

        Set<String> globalUnique = new HashSet<>();
        EasyExcelPipelineUtil.processByMultipart(file,
                "D:/output.xlsx",
                UserImportVO.class,
                UserImportResultVO.class,  list -> testExcelDemoService.convertPage(list, globalUnique), 1000);
        return "上传成功";
    }
}

测试数据

模拟请求

请求文件数据

生成导出结果文件数据

相关推荐
罗超驿2 小时前
独立实现双向链表_LinkedList
java·数据结构·链表·linkedlist
无限大62 小时前
职场逻辑03:3步搞定高效汇报,让领导看到你的价值
后端
盐水冰3 小时前
【烘焙坊项目】后端搭建(12) - 订单状态定时处理,来单提醒和顾客催单
java·后端·学习
凸头3 小时前
CompletableFuture 与 Future 对比与实战示例
java·开发语言
wuqingshun3141593 小时前
线程安全需要保证几个基本特征
java·开发语言·jvm
紫丁香3 小时前
AutoGen详解一
后端·python·flask
努力也学不会java3 小时前
【缓存算法】一篇文章带你彻底搞懂面试高频题LRU/LFU
java·数据结构·人工智能·算法·缓存·面试
攒了一袋星辰3 小时前
高并发强一致性顺序号生成系统 -- SequenceGenerator
java·数据库·mysql
小涛不学习3 小时前
Spring Boot 详解(从入门到原理)
java·spring boot·后端