# scaffold 项目之 Excel 导入导出

# 简介

Excel 导入通常指的是将 Excel 文件(如 .xls.xlsx 格式)中的数据读取到程序中,以便进行进一步处理和分析。导出相关报表等。本项目封装了 scaffold-spring-boot-starter-excel 技术组件,基于 EasyExcel 实现 Excel 的读写操作,可用于实现最常见的 Excel 导入导出等功能。

# 关于 EasyExcel

EasyExcel 是阿里开源的 Excel 工具库,具有简单易用、低内存、高性能的特点。

在尽可用节约内存的情况下,支持百万行的 Excel 读写操作。例如说,仅使用 64M 内存,20 秒完成 75M(46 万行 25 列)Excel 的读取。并且,还有极速模式能更快,但是内存占用会在 100M 多一点。

# 组件的封装实现

本项目自是做了对 简单的数据(单表) 导入导出,如果需要支持对复杂的 多级表头(多表关联) 导入导出需要在进一步封装, 导出可能简单一些,导入比较复杂, 或者可以使用 报表设计器 来导出复杂的报表。

提供一个工具来处理导入导出,代码如下:

package com.tz.scaffold.framework.excel.core.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
/**
 * <p> Project: scaffold - ExcelUtils </p>
 *
 * Excel 工具类
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
public class ExcelUtils {
    /**
     * 将列表以 Excel 响应给前端
     *
     * @param response 响应
     * @param filename 文件名
     * @param sheetName Excel sheet 名
     * @param head Excel head 头
     * @param data 数据列表哦
     * @param <T> 泛型,保证 head 和 data 类型的一致性
     * @throws IOException 写入失败的情况
     */
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) throws IOException {
        // 输出 Excel
        EasyExcel.write(response.getOutputStream(), head)
                // 不要自动关闭,交给 Servlet 自己处理
                .autoCloseStream(false)
                // 基于 column 长度,自动适配。最大 255 宽度
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .sheet(sheetName).doWrite(data);
        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
       return EasyExcel.read(file.getInputStream(), head, null)
               // 不要自动关闭,交给 Servlet 自己处理
                .autoCloseStream(false)
                .doReadAllSync();
    }
}

对字典值的翻译导出,使用转换器实现,代码如下:

package com.tz.scaffold.framework.excel.core.convert;
import cn.hutool.core.convert.Convert;
import com.tz.scaffold.framework.dict.core.util.DictFrameworkUtils;
import com.tz.scaffold.framework.excel.core.annotations.DictFormat;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
/**
 * <p> Project: scaffold - DictConvert </p>
 *
 * Excel 数据字典转换器
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Slf4j
public class DictConvert implements Converter<Object> {
    @Override
    public Class<?> supportJavaTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
                                    GlobalConfiguration globalConfiguration) {
        // 使用字典解析
        String type = getType(contentProperty);
        String label = readCellData.getStringValue();
        String value = DictFrameworkUtils.parseDictDataValue(type, label);
        if (value == null) {
            log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
            return null;
        }
        // 将 String 的 value 转换成对应的属性
        Class<?> fieldClazz = contentProperty.getField().getType();
        return Convert.convert(fieldClazz, value);
    }
    @Override
    public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
                                                    GlobalConfiguration globalConfiguration) {
        // 空时,返回空
        if (object == null) {
            return new WriteCellData<>("");
        }
        // 使用字典格式化
        String type = getType(contentProperty);
        String value = String.valueOf(object);
        String label = DictFrameworkUtils.getDictDataLabel(type, value);
        if (label == null) {
            log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
            return new WriteCellData<>("");
        }
        // 生成 Excel 小表格
        return new WriteCellData<>(label);
    }
    private static String getType(ExcelContentProperty contentProperty) {
        return contentProperty.getField().getAnnotation(DictFormat.class).value();
    }
}

EasyExcel 定义了 Converter 接口,用于实现字段的转换。它有两个核心方法:

  1. #convertToJavaData(...) 方法:将 Excel Row 对应表格的值,转换成 Java 内存中的值。例如说,Excel 的 “状态” 列,将 “状态” 列转换成 status = 1 ,” 禁用” 列转换成 status = 0

  2. #convertToExcelData(...) 方法:恰好相反,将 Java 内存中的值,转换成 Excel Row 对应表格的值。例如说,Excel 的 “状态” 列,将 status = 1 转换成 “开启” 列, status = 0 转换成” 禁用” 列

定义字典格式注解,代码如下:

package com.tz.scaffold.framework.excel.core.annotations;
import java.lang.annotation.*;
/**
 * <p> Project: scaffold - DictFormat </p>
 *
 * 字典格式化
 * <p>
 * 实现将字典数据的值,格式化成字典数据的标签
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DictFormat {
    /**
     * 例如说,SysDictTypeConstants、InfDictTypeConstants
     *
     * @return 字典类型
     */
    String value();
}

使用:

package com.tz.scaffold.module.infra.controller.admin.logger.vo.apierrorlog;
import com.tz.scaffold.framework.excel.core.annotations.DictFormat;
import com.tz.scaffold.framework.excel.core.convert.DictConvert;
import com.tz.scaffold.module.infra.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * <p> Project: scaffold - ApiErrorLogExcelVO </p>
 *
 * API 错误日志 Excel VO
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Data
public class ApiErrorLogExcelVO {
    @ExcelProperty("编号")
    private Integer id;
    @ExcelProperty("链路追踪编号")
    private String traceId;
    @ExcelProperty("用户编号")
    private Integer userId;
    @ExcelProperty(value = "用户类型", converter = DictConvert.class)
    @DictFormat(com.tz.scaffold.module.system.enums.DictTypeConstants.USER_TYPE)
    private Integer userType;
    @ExcelProperty(value = "处理状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS)
    private Integer processStatus;
}

说明: 当导出 ApiErrorLogExcelVO 的时候 读取到字段 processStatus 会执行 DictConvert 逻辑,让后通过 @DictFormat(DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS) 去查询对应的字典值。

# Excel 导出

有了导入导出工具类,这里直接在 controller 中使用,例子代码:

package com.tz.scaffold.module.system.controller.admin.user;
/**
 * <p> Project: scaffold - UserController </p>
 *
 * 管理后台 - 用户
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Tag(name = "管理后台 - 用户")
@RestController
@RequestMapping("/system/user")
@Validated
public class UserController {
    @Resource
    private AdminUserService userService;
    @Resource
    private DeptService deptService;
   
    @GetMapping("/export")
    @Operation(summary = "导出用户")
    @PreAuthorize("@ss.hasPermission('system:user:export')")
    @OperateLog(type = EXPORT)
    public void exportUserList(@Validated UserExportReqVO reqVO,
                               HttpServletResponse response) throws IOException {
        // 获得用户列表
        List<AdminUserDO> users = userService.getUserList(reqVO);
        // 获得拼接需要的数据
        Collection<Long> deptIds = convertList(users, AdminUserDO::getDeptId);
        Map<Long, DeptDO> deptMap = deptService.getDeptMap(deptIds);
        Map<Long, AdminUserDO> deptLeaderUserMap = userService.getUserMap(
                convertSet(deptMap.values(), DeptDO::getLeaderUserId));
        // 拼接数据
        List<UserExcelVO> excelUsers = new ArrayList<>(users.size());
        users.forEach(user -> {
            UserExcelVO excelVO = UserConvert.INSTANCE.convert02(user);
            // 设置部门
            MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> {
                excelVO.setDeptName(dept.getName());
                // 设置部门负责人的名字
                MapUtils.findAndThen(deptLeaderUserMap, dept.getLeaderUserId(),
                        deptLeaderUser -> excelVO.setDeptLeaderNickname(deptLeaderUser.getNickname()));
            });
            excelUsers.add(excelVO);
        });
        // 输出
        ExcelUtils.write(response, "用户数据.xls", "用户列表", UserExcelVO.class, excelUsers);
    }
}

导出步骤:

  1. 查询出对应的数据
  2. 将查询出的数据转成对应 ExcelVO 对象, 通过 ExcelUtils 转换成 Excel 文件。

导出实体例如:

package com.tz.scaffold.module.system.controller.admin.user.vo.user;
import com.tz.scaffold.framework.excel.core.annotations.DictFormat;
import com.tz.scaffold.framework.excel.core.convert.DictConvert;
import com.tz.scaffold.module.system.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * <p> Project: scaffold - UserExcelVO </p>
 *
 * 用户 Excel 导出 VO
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Data
public class UserExcelVO {
    @ExcelProperty("用户编号")
    private Long id;
    @ExcelProperty("用户名称")
    private String username;
    @ExcelProperty("用户昵称")
    private String nickname;
    @ExcelProperty("用户邮箱")
    private String email;
    @ExcelProperty("手机号码")
    private String mobile;
    @ExcelProperty(value = "用户性别", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.USER_SEX)
    private Integer sex;
    @ExcelProperty(value = "帐号状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.COMMON_STATUS)
    private Integer status;
    @ExcelProperty("最后登录IP")
    private String loginIp;
    @ExcelProperty("最后登录时间")
    private LocalDateTime loginDate;
    @ExcelProperty("部门名称")
    private String deptName;
    @ExcelProperty("部门负责人")
    private String deptLeaderNickname;
}
  1. 每个字段上,添加 @ExcelProperty 注解,声明 Excel Head 头部的名字。每个字段的,就是它对应的 Excel Row 行的数据值。

  2. 如果字段的的注解 converter 属性是 DictConvert 转换器,用于字典的转换。例如说,通过 status 字段,将 status = 1 转换成 “开启” 列, status = 0 转换成” 禁用” 列。

  3. 在类上,添加 @ExcelIgnoreUnannotated 注解,表示未添加 @ExcelProperty 的字段,不进行导出。

# Excel 导入

创建 UserImportExcelVO 类,用户 Excel 导入的 VO 类。它的作用和 Excel 导入是一样的,代码如下:

package com.tz.scaffold.module.system.controller.admin.user.vo.user;
import com.tz.scaffold.framework.excel.core.annotations.DictFormat;
import com.tz.scaffold.framework.excel.core.convert.DictConvert;
import com.tz.scaffold.module.system.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
 * <p> Project: scaffold - UserImportExcelVO </p>
 *
 * 用户 Excel 导入 VO
 * <p>
 * Accessors: 设置 chain = false,避免用户导入有问题
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false)
public class UserImportExcelVO {
    @ExcelProperty("登录名称")
    private String username;
    @ExcelProperty("用户名称")
    private String nickname;
    @ExcelProperty("部门编号")
    private Long deptId;
    @ExcelProperty("用户邮箱")
    private String email;
    @ExcelProperty("手机号码")
    private String mobile;
    @ExcelProperty(value = "用户性别", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.USER_SEX)
    private Integer sex;
    @ExcelProperty(value = "账号状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.COMMON_STATUS)
    private Integer status;
}

# EasyExcel 注解

EasyExcel 提供了一系列注解来简化 Excel 文件的读写操作。以下是 EasyExcel 中常用的注解说明和使用例子:

# @ExcelProperty

这是最常用的一个注解,注解中有三个参数 valueindexconverter 分别代表列明、列序号、数据转换方式。 valueindex 只能二选一,通常不用设置 converter

最佳实践

public class ImeiEncrypt {
    
    @ExcelProperty(value = "imei")
    private String imei;
}

# @ColumnWidth

用于设置列宽度的注解,注解中只有一个参数 valuevalue 的单位是字符长度,最大可以设置 255 个字符,因为一个 Excel 单元格最大可以写入的字符个数,就是 255 个字符。

最佳实践

public class ImeiEncrypt {
    
    @ColumnWidth(value = 18)
    private String imei;
}

# @ContentFontStyle

用于设置单元格内容字体格式的注解。参数如下:

参数含义
fontName字体名称
fontHeightInPoints字体高度
italic是否斜体
strikeout是否设置删除水平线
color字体颜色
typeOffset偏移量
underline下划线
bold是否加粗
charset编码格式

# @ContentLoopMerge

用于设置合并单元格的注解。参数如下:

参数含义
eachRow
columnExtend

# @ContentRowHeight

用于设置行高。参数如下:

参数含义
value行高, -1 代表自动行高

# @ContentStyle

设置内容格式注解。参数如下:

参数含义
dataFormat日期格式
hidden设置单元格使用此样式隐藏
locked设置单元格使用此样式锁定
quotePrefix在单元格前面增加 ` 符号,数字或公式将以字符串形式展示 |
horizontalAlignment设置是否水平居中
wrapped设置文本是否应换行。将此标志设置为 true 通过在多行上显示使单元格中的所有内容可见
verticalAlignment设置是否垂直居中
rotation设置单元格中文本旋转角度。03 版本的 Excel 旋转角度区间为 - 90°90°,07 版本的 Excel 旋转角度区间为 0°180°
indent设置单元格中缩进文本的空格数
borderLeft设置左边框的样式
borderRight设置右边框样式
borderTop设置上边框样式
borderBottom设置下边框样式
leftBorderColor设置左边框颜色
rightBorderColor设置右边框颜色
topBorderColor设置上边框颜色
bottomBorderColor设置下边框颜色
fillPatternType设置填充类型
fillBackgroundColor设置背景色
fillForegroundColor设置前景色
shrinkToFit设置自动单元格自动大小

# @HeadFontStyle

用于定制标题字体格式。参数如下:

参数含义
fontName设置字体名称
fontHeightInPoints设置字体高度
italic设置字体是否斜体
strikeout是否设置删除线
color设置字体颜色
typeOffset设置偏移量
underline设置下划线
charset设置字体编码
bold设置字体是否家畜

# @HeadRowHeight

设置标题行行高。参数如下:

参数含义
value设置行高,-1 代表自动行高

# @HeadStyle

设置标题样式。参数如下:

参数含义
dataFormat日期格式
hidden设置单元格使用此样式隐藏
locked设置单元格使用此样式锁定
quotePrefix在单元格前面增加 ` 符号,数字或公式将以字符串形式展示 |
horizontalAlignment设置是否水平居中
wrapped设置文本是否应换行。将此标志设置为 true 通过在多行上显示使单元格中的所有内容可见
verticalAlignment设置是否垂直居中
rotation设置单元格中文本旋转角度。03 版本的 Excel 旋转角度区间为 - 90°90°,07 版本的 Excel 旋转角度区间为 0°180°
indent设置单元格中缩进文本的空格数
borderLeft设置左边框的样式
borderRight设置右边框样式
borderTop设置上边框样式
borderBottom设置下边框样式
leftBorderColor设置左边框颜色
rightBorderColor设置右边框颜色
topBorderColor设置上边框颜色
bottomBorderColor设置下边框颜色
fillPatternType设置填充类型
fillBackgroundColor设置背景色
fillForegroundColor设置前景色
shrinkToFit设置自动单元格自动大小

# @ExcelIgnore

不将该字段转换成 Excel。

# @ExcelIgnoreUnannotated

没有注解的字段都不转换

更多详细使用教程请看官网