# 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
接口,用于实现字段的转换。它有两个核心方法:
#convertToJavaData(...)
方法:将 Excel Row 对应表格的值,转换成 Java 内存中的值。例如说,Excel 的 “状态” 列,将 “状态” 列转换成status = 1
,” 禁用” 列转换成status = 0
。
#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); | |
} | |
} |
导出步骤:
- 查询出对应的数据
- 将查询出的数据转成对应
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; | |
} |
每个字段上,添加
@ExcelProperty
注解,声明 Excel Head 头部的名字。每个字段的值,就是它对应的 Excel Row 行的数据值。如果字段的的注解
converter
属性是 DictConvert 转换器,用于字典的转换。例如说,通过status
字段,将status = 1
转换成 “开启” 列,status = 0
转换成” 禁用” 列。在类上,添加
@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
这是最常用的一个注解,注解中有三个参数 value
、 index
、 converter
分别代表列明、列序号、数据转换方式。 value
和 index
只能二选一,通常不用设置 converter
。
最佳实践
public class ImeiEncrypt { | |
@ExcelProperty(value = "imei") | |
private String imei; | |
} |
# @ColumnWidth
用于设置列宽度的注解,注解中只有一个参数 value
。 value
的单位是字符长度,最大可以设置 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
没有注解的字段都不转换
更多详细使用教程请看官网