锋盈数科-知识库 Logo
首页
软件开发
计算机基础
Hello Halo
新手必读
关于本知识库
登录 →
锋盈数科-知识库 Logo
首页 软件开发 计算机基础 Hello Halo 新手必读 关于本知识库
登录
  1. 首页
  2. 软件开发
  3. JAVA
  4. SpringBoot集成EasyExcel

SpringBoot集成EasyExcel

0
  • JAVA
  • 发布于 2024-08-06
  • 0 次阅读
黄健
黄健

原文链接:https://blog.csdn.net/Yaml4/article/details/137768477

介绍

EasyExcel 是一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。它能让你在不用考虑性能、内存的等因素的情况下,快速完成 Excel 的读、写等功能。

EasyExcel文档地址:https://easyexcel.opensource.alibaba.com/

快速开始

引入依赖

<dependency>

    <groupId>com.alibaba</groupId>

    <artifactId>easyexcel</artifactId>

    <version>3.1.3</version>

</dependency>

简单导出

以导出用户信息为例,接下来手把手教大家如何使用EasyExcel实现导出功能!

定义实体类

在EasyExcel中,以面向对象思想来实现导入导出,无论是导入数据还是导出数据都可以想象成具体某个对象的集合,所以为了实现导出用户信息功能,首先创建一个用户对象UserDO实体类,用于封装用户信息:

/**

 * 用户信息

 *

 * @author yun

 */

@Data

public class UserDO {

    @ExcelProperty("用户编号")

    @ColumnWidth(20)

    private Long id;

    @ExcelProperty("用户名")

    @ColumnWidth(20)

    private String username;

    @ExcelIgnore

    private String password;

    @ExcelProperty("昵称")

    @ColumnWidth(20)

    private String nickname;

    @ExcelProperty("生日")

    @ColumnWidth(20)

    @DateTimeFormat("yyyy-MM-dd")

    private Date birthday;

    @ExcelProperty("手机号")

    @ColumnWidth(20)

    private String phone;

    @ExcelProperty("身高(米)")

    @NumberFormat("#.##")

    @ColumnWidth(20)

    private Double height;

    @ExcelProperty(value = "性别", converter = GenderConverter.class)

    @ColumnWidth(10)

    private Integer gender;

}

上面代码中类属性上使用了EasyExcel核心注解:

@ExcelProperty:核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器;

@ColumnWidth:用于设置表格列的宽度;

@DateTimeFormat:用于设置日期转换格式;

@NumberFormat:用于设置数字转换格式。

自定义转换器

在EasyExcel中,如果想实现枚举类型到字符串类型转换(例如gender属性:1 -> 男,2 -> 女),需实现Converter接口来自定义转换器,下面为自定义GenderConverter性别转换器代码实现:

/**

 * Excel 性别转换器

 *

 * @author yun

 */

public class GenderConverter implements Converter<Integer> {

    @Override

    public Class<?> supportJavaTypeKey() {

        return Integer.class;

    }

    @Override

    public CellDataTypeEnum supportExcelTypeKey() {

        return CellDataTypeEnum.STRING;

    }

    @Override

    public Integer convertToJavaData(ReadConverterContext<?> context) {

        return GenderEnum.convert(context.getReadCellData().getStringValue()).getValue();

    }

    @Override

    public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {

        return new WriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());

    }

}
/**

 * 性别枚举

 *

 * @author yun

 */

@Getter

@AllArgsConstructor

public enum GenderEnum {

    /**

     * 未知

     */

    UNKNOWN(0, "未知"),

    /**

     * 男性

     */

    MALE(1, "男性"),

    /**

     * 女性

     */

    FEMALE(2, "女性");

    private final Integer value;

    @JsonFormat

    private final String description;

    public static GenderEnum convert(Integer value) {

        return Stream.of(values())

                .filter(bean -> bean.value.equals(value))

                .findAny()

                .orElse(UNKNOWN);

    }

    public static GenderEnum convert(String description) {

        return Stream.of(values())

                .filter(bean -> bean.description.equals(description))

                .findAny()

                .orElse(UNKNOWN);

    }

}

定义接口

/**

 * EasyExcel导入导出

 *

 * @author yun

 */

@RestController

@RequestMapping("/excel")

public class ExcelController {

    @GetMapping("/export/user")

    public void exportUserExcel(HttpServletResponse response) {

        try {

            this.setExcelResponseProp(response, "用户列表");

            List<UserDO> userList = this.getUserList();

            EasyExcel.write(response.getOutputStream())

                    .head(UserDO.class)

                    .excelType(ExcelTypeEnum.XLSX)

                    .sheet("用户列表")

                    .doWrite(userList);

        } catch (IOException e) {

            throw new RuntimeException(e);

        }

    }

    /**

     * 设置响应结果

     *

     * @param response    响应结果对象

     * @param rawFileName 文件名

     * @throws UnsupportedEncodingException 不支持编码异常

     */

    private void setExcelResponseProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

        response.setCharacterEncoding("utf-8");

        String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");

        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

    }

    

    /**

     * 读取用户列表数据

     *

     * @return 用户列表数据

     * @throws IOException IO异常

     */

    private List<UserDO> getUserList() throws IOException {

        ObjectMapper objectMapper = new ObjectMapper();

        ClassPathResource classPathResource = new ClassPathResource("mock/users.json");

        InputStream inputStream = classPathResource.getInputStream();

        return objectMapper.readValue(inputStream, new TypeReference<List<UserDO>>() {

        });

    }

}

测试接口

运行项目,通过 Postman 或者 Apifox 工具来进行接口测试

注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。

接口地址:http://localhost:8080/excel/export/user

复杂导出

由于 EasyPoi 支持嵌套对象导出,直接使用内置 @ExcelCollection 注解即可实现,遗憾的是 EasyExcel 不支持一对多导出,只能自行实现,通过此issues了解到,项目维护者建议通过自定义合并策略方式来实现一对多导出。

解决思路:只需把订单主键相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出

自定义注解

创建一个自定义注解,用于标记哪些属性需要合并单元格,哪个属性是主键:

 

/**

 * 用于判断是否需要合并以及合并的主键

 *

 * @author yun

 */

@Target({ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface ExcelMerge {

    /**

     * 是否合并单元格

     *

     * @return true || false

     */

    boolean merge() default true;

    /**

     * 是否为主键(即该字段相同的行合并)

     *

     * @return true || false

     */

    boolean isPrimaryKey() default false;

}

定义实体类

在需要合并单元格的属性上设置 @ExcelMerge 注解,二级表头通过设置 @ExcelProperty 注解中 value 值为数组形式来实现该效果:

/**

 * @author

 */

@Data

public class OrderBO {

    @ExcelProperty(value = "订单主键")

    @ColumnWidth(16)

    @ExcelMerge(merge = true, isPrimaryKey = true)

    private String id;

    @ExcelProperty(value = "订单编号")

    @ColumnWidth(20)

    @ExcelMerge(merge = true)

    private String orderId;

    @ExcelProperty(value = "收货地址")

    @ExcelMerge(merge = true)

    @ColumnWidth(20)

    private String address;

    @ExcelProperty(value = "创建时间")

    @ColumnWidth(20)

    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")

    @ExcelMerge(merge = true)

    private Date createTime;

    @ExcelProperty(value = {"商品信息", "商品编号"})

    @ColumnWidth(20)

    private String productId;

    @ExcelProperty(value = {"商品信息", "商品名称"})

    @ColumnWidth(20)

    private String name;

    @ExcelProperty(value = {"商品信息", "商品标题"})

    @ColumnWidth(30)

    private String subtitle;

    @ExcelProperty(value = {"商品信息", "品牌名称"})

    @ColumnWidth(20)

    private String brandName;

    @ExcelProperty(value = {"商品信息", "商品价格"})

    @ColumnWidth(20)

    private BigDecimal price;

    @ExcelProperty(value = {"商品信息", "商品数量"})

    @ColumnWidth(20)

    private Integer count;

}

数据映射与平铺

导出之前,需要对数据进行处理,将订单数据进行平铺,orderList为平铺前格式,exportData为平铺后格式:

自定义单元格合并策略

当 Excel 中两列主键相同时,合并被标记需要合并的列:

/**

 * 自定义单元格合并策略

 *

 * @author yun

 */

public class ExcelMergeStrategy implements RowWriteHandler {

    /**

     * 主键下标

     */

    private Integer primaryKeyIndex;

    /**

     * 需要合并的列的下标集合

     */

    private final List<Integer> mergeColumnIndexList = new ArrayList<>();

    /**

     * 数据类型

     */

    private final Class<?> elementType;

    public ExcelMergeStrategy(Class<?> elementType) {

        this.elementType = elementType;

    }

    @Override

    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {

        // 判断是否为标题

        if (isHead) {

            return;

        }

        // 获取当前工作表

        Sheet sheet = writeSheetHolder.getSheet();

        // 初始化主键下标和需要合并字段的下标

        if (primaryKeyIndex == null) {

            this.initPrimaryIndexAndMergeIndex(writeSheetHolder);

        }

        // 判断是否需要和上一行进行合并

        // 不能和标题合并,只能数据行之间合并

        if (row.getRowNum() <= 1) {

            return;

        }

        // 获取上一行数据

        Row lastRow = sheet.getRow(row.getRowNum() - 1);

        // 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并

        if (lastRow.getCell(primaryKeyIndex).getStringCellValue().equalsIgnoreCase(row.getCell(primaryKeyIndex).getStringCellValue())) {

            for (Integer mergeIndex : mergeColumnIndexList) {

                CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(), mergeIndex, mergeIndex);

                sheet.addMergedRegionUnsafe(cellRangeAddress);

            }

        }

    }

    /**

     * 初始化主键下标和需要合并字段的下标

     *

     * @param writeSheetHolder WriteSheetHolder

     */

    private void initPrimaryIndexAndMergeIndex(WriteSheetHolder writeSheetHolder) {

        // 获取当前工作表

        Sheet sheet = writeSheetHolder.getSheet();

        // 获取标题行

        Row titleRow = sheet.getRow(0);

        // 获取所有属性字段

        Field[] fields = this.elementType.getDeclaredFields();

        // 遍历所有字段

        for (Field field : fields) {

            // 获取@ExcelProperty注解,用于获取该字段对应列的下标

            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);

            // 判断是否为空

            if (null == excelProperty) {

                continue;

            }

            // 获取自定义注解,用于合并单元格

            ExcelMerge excelMerge = field.getAnnotation(ExcelMerge.class);

            // 判断是否需要合并

            if (null == excelMerge) {

                continue;

            }

            for (int i = 0; i < fields.length; i++) {

                Cell cell = titleRow.getCell(i);

                if (null == cell) {

                    continue;

                }

                // 将字段和表头匹配上

                if (excelProperty.value()[0].equalsIgnoreCase(cell.getStringCellValue())) {

                    if (excelMerge.isPrimaryKey()) {

                        primaryKeyIndex = i;

                    }

                    if (excelMerge.merge()) {

                        mergeColumnIndexList.add(i);

                    }

                }

            }

        }

        // 没有指定主键,则异常

        if (null == this.primaryKeyIndex) {

            throw new IllegalStateException("使用@ExcelMerge注解必须指定主键");

        }

    }

}

定义接口

将自定义合并策略 ExcelMergeStrategy 通过 registerWriteHandler 注册上去:

/**

 * EasyExcel导入导出

 *

 * @author yun

 */

@RestController

@RequestMapping("/excel")

public class ExcelController {

    @GetMapping("/export/order")

    public void exportOrderExcel(HttpServletResponse response) {

        try {

            this.setExcelResponseProp(response, "订单列表");

            List<OrderDO> orderList = this.getOrderList();

            List<OrderBO> exportData = this.convert(orderList);

            EasyExcel.write(response.getOutputStream())

                    .head(OrderBO.class)

                    .registerWriteHandler(new ExcelMergeStrategy(OrderBO.class))

                    .excelType(ExcelTypeEnum.XLSX)

                    .sheet("订单列表")

                    .doWrite(exportData);

        } catch (IOException e) {

            throw new RuntimeException(e);

        }

    }

    /**

     * 设置响应结果

     *

     * @param response    响应结果对象

     * @param rawFileName 文件名

     * @throws UnsupportedEncodingException 不支持编码异常

     */

    private void setExcelResponseProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

        response.setCharacterEncoding("utf-8");

        String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");

        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

    }

}

测试接口

运行项目,通过 Postman 或者 Apifox 工具来进行接口测试

注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。

接口地址:http://localhost:8080/excel/export/order

一对多导出

//需要合并的列

int[] mergeColumeIndex = {0,1,2,3,4,5,8,9,11};

// 从那一列开始合并

int mergeRowIndex = 0;

ExcelWriter excelWriter =  EasyExcel.write(outputStream)

        .sheet("SheetName")

        //设置合并单元格策略

        .registerWriteHandler(new ExcelFillCellMergeStrategy(mergeRowIndex, mergeColumeIndex))

        .doWrite(exportVoList);

自定义单元格合并策略

public class ExcelFillCellMergeStrategy implements CellWriteHandler {

 

    private int[] mergeColumnIndex;

    private int mergeRowIndex;

 

    public ExcelFillCellMergeStrategy() {

    }

 

    public ExcelFillCellMergeStrategy(int mergeRowIndex, int[] mergeColumnIndex) {

        this.mergeRowIndex = mergeRowIndex;

        this.mergeColumnIndex = mergeColumnIndex;

    }

 

    @Override

    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

 

    }

 

    @Override

    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

 

    }

 

    @Override

    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {

        int curRowIndex = cell.getRowIndex();

        int curColIndex = cell.getColumnIndex();

        if (curRowIndex > mergeRowIndex) {

            for (int i = 0; i < mergeColumnIndex.length; i++) {

                if (curColIndex == mergeColumnIndex[i]) {

                    mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex);

                    break;

                }

            }

        }

    }

 

    /**

     * 当前单元格向上合并

     *

     * @param writeSheetHolder

     * @param cell             当前单元格

     * @param curRowIndex      当前行

     * @param curColIndex      当前列

     */

    private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) {

        Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();

        Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);

        Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();

        // 将当前单元格数据与上一个单元格数据比较

        Boolean dataBool = preData.equals(curData);

        //此处需要注意:因为我是按照序号确定是否需要合并的,所以获取每一行第一列数据和上一行第一列数据进行比较,如果相等合并

        Boolean bool = cell.getRow().getCell(0).getNumericCellValue() == cell.getSheet().getRow(curRowIndex - 1).getCell(0).getNumericCellValue();

        if (dataBool && bool) {

            Sheet sheet = writeSheetHolder.getSheet();

            List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();

            boolean isMerged = false;

            for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {

                CellRangeAddress cellRangeAddr = mergeRegions.get(i);

                // 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元

                if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {

                    sheet.removeMergedRegion(i);

                    cellRangeAddr.setLastRow(curRowIndex);

                    sheet.addMergedRegion(cellRangeAddr);

                    isMerged = true;

                }

            }

            // 若上一个单元格未被合并,则新增合并单元

            if (!isMerged) {

                CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);

                sheet.addMergedRegion(cellRangeAddress);

            }

        }

    }

    

}

测试数据

exportVoList 导出的数据。

注意:如果需要合并三个单元格需要产生3条数据 例如:

1,李华,英语,80分

1,李华,语文,70分

1,李华,数学,60分

2,李四,数学,80分

导出效果如下:

简单导入

以导入用户信息为例,接下来手把手教大家如何使用EasyExcel实现导入功能!

定义接口

/**

 * EasyExcel导入导出

 *

 * @author yun

 */

@RestController

@RequestMapping("/excel")

@Api(tags = "EasyExcel")

public class ExcelController {

    

    @PostMapping("/import/user")

    public ResponseVO importUserExcel(@RequestPart(value = "file") MultipartFile file) {

        try {

            List<UserDO> userList = EasyExcel.read(file.getInputStream())

                    .head(UserDO.class)

                    .sheet()

                    .doReadSync();

            return ResponseVO.success(userList);

        } catch (IOException e) {

            return ResponseVO.error();

        }

    }

}
测试接口 

标签: #JAVA 991
相关文章

Spring 实现 3 种异步接口 2024-10-18 09:07

大家好,我是苏三~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地

重学SpringBoot3-集成Redis(五)之布隆过滤器 2024-10-08 11:24

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(五)之布隆过滤器 1. 什么是布隆过滤器? * 基本概念 适用场景 2. 使用 Redis 实现布隆过滤器 * 项目依赖 Redis 配置

SpringBoot整合异步任务执行 2024-10-08 11:24

同步任务: 同步任务是在单线程中按顺序执行,每次只有一个任务在执行,不会引发线程安全和数据一致性等 并发问题 同步任务需要等待任务执行完成后才能执行下一个任务,无法同时处理多个任务,响应慢,影响用 户体验 异步任务: 异步任务是在多线程中同时执行,多个任务可以并发执行,同时处理多个请求,响应快,资源

springboot kafka多数据源,通过配置动态加载发送者和消费者 2024-10-08 11:24

前言 最近做项目,需要支持kafka多数据源,实际上我们也可以通过代码固定写死多套kafka集群逻辑,但是如果需要不修改代码扩展呢,因为kafka本身不处理额外逻辑,只是起到削峰,和数据的传递,那么就需要对架构做一定的设计了。 准备test kafka本身非常容易上手,如果我们需要单元测试,引入ja

SpringBoot 集成 Redis 2024-10-08 11:24

一:SpringBoot 集成 Redis ①Redis是一个 NoSQL(not only)数据库, 常作用缓存 Cache 使用。 ②Redis是一个中间件、是一个独立的服务器;常用的数据类型: string , hash ,set ,zset , list ③通过Redis客户端可以使用多种语

SpringBoot整合QQ邮箱 2024-10-08 11:24

SpringBoot可以通过导入依赖的方式集成多种技术,这当然少不了我们常用的邮箱,现在本章演示SpringBoot整合QQ邮箱发送邮件…. 下面按步骤进行: 1.获取QQ邮箱授权码 1.1 登录QQ邮箱 1.2 开启SMTP服务 找到下图中的SMTP服务区域,如果当前账号未开启的话自己手动开启。

目录

IT 外包服务商

  • 意见投递
  • zyf6619

软件开发应用

主菜单

  • 首页
  • 软件开发
  • 计算机基础
  • Hello Halo
  • 新手必读
  • 关于本知识库
Copyright © 2024 your company All Rights Reserved. Powered by Halo.