SpringBoot 集成 EasyExcel 3.x 优雅实现 Excel 导入导出

共 15212字,需浏览 31分钟

 ·

2024-04-12 03:44

来源:blog.csdn.net/qq991658923/article/details/128153012


介绍

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 william@StarImmortal */@Datapublic 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 william@StarImmortal */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 william@StarImmortal */@Getter@AllArgsConstructorpublic 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 william@StarImmortal */@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 william@StarImmortal */@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ExcelMerge {    /**     * 是否合并单元格     *     * @return true || false     */    boolean merge() default true;    /**     * 是否为主键(即该字段相同的行合并)     *     * @return true || false     */    boolean isPrimaryKey() default false;}


定义实体类


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

/** * @author william@StarImmortal */@Datapublic 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 william@StarImmortal */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 william@StarImmortal */@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




简单导入


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

/** * EasyExcel导入导出 * * @author william@StarImmortal */@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();        }    }}

参考资料


  • 项目地址:https://github.com/alibaba/easyexcel

  • 官方文档:https://www.yuque.com/easyexcel/doc/easyexcel

  • 一对多导出优雅方案:https://github.com/alibaba/easyexcel/issues/1780



浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报