java每日精进 5.19【Excel 导入导出】
基于 EasyExcel 实现 Excel 的读写操作,可用于实现最常见的 Excel 导入导出等功能。
Excel 导入导出功能涉及前后端协作,后端处理数据查询、文件生成和解析,前端提供用户交互和文件下载/上传界面。以下是全流程解析,分为导出流程和导入流程。
1.1 导出流程解析
1.1.1 后端导出(PostController.java)
代码:
@GetMapping("/export")
@Operation(summary = "岗位管理")
@PreAuthorize("@ss.hasPermission('system:post:export')")
@ApiAccessLog(operateType = EXPORT)
public void export(HttpServletResponse response, @Validated PostPageReqVO reqVO) throws IOException {// ① 查询数据reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);List<PostDO> list = postService.getPostPage(reqVO).getList();// ② 导出 ExcelExcelUtils.write(response, "岗位数据.xls", "岗位列表", PostRespVO.class,BeanUtils.toBean(list, PostRespVO.class));
}
解析:
- 功能:导出岗位数据为 Excel 文件,供前端下载。
- 步骤:
- 查询数据:
- reqVO.setPageSize(PageParam.PAGE_SIZE_NONE):禁用分页,获取所有符合条件的数据。
- postService.getPostPage(reqVO):调用服务层查询岗位数据,返回 PageResult<PostDO>。
- getList():提取数据列表(List<PostDO>)。
- 数据转换:
- BeanUtils.toBean(list, PostRespVO.class):将 PostDO 列表转换为 PostRespVO 列表,适配 Excel 导出格式。
- 导出 Excel:
- ExcelUtils.write:将数据写入 Excel 文件,设置文件名(岗位数据.xls)、Sheet 名(岗位列表)和响应头。
- 查询数据:
- 权限控制:
- @PreAuthorize("@ss.hasPermission('system:post:export')"):确保用户有导出权限。
- 日志记录:
- @ApiAccessLog(operateType = EXPORT):记录导出操作日志。
1.1.2 VO 类(PostRespVO.java)
代码:
@Schema(description = "管理后台 - 岗位信息 Response VO")
@Data
@ExcelIgnoreUnannotated
public class PostRespVO {@Schema(description = "岗位序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")@ExcelProperty("岗位序号")private Long id;@Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小土豆")@ExcelProperty("岗位名称")private String name;@Schema(description = "岗位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")@ExcelProperty("岗位编码")private String code;@Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")@ExcelProperty("岗位排序")private Integer sort;@Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")@ExcelProperty(value = "状态", converter = DictConvert.class)@DictFormat(DictTypeConstants.COMMON_STATUS)private Integer status;@Schema(description = "备注", example = "快乐的备注")private String remark;@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)private LocalDateTime createTime;
}
解析:
- 功能:定义 Excel 导出的数据结构和格式。
- 注解:
- @ExcelIgnoreUnannotated:忽略未标注 @ExcelProperty 的字段(如 remark、createTime),不导出到 Excel。
- @ExcelProperty:指定 Excel 列名(如 岗位序号、岗位名称)。
- @ExcelProperty(value = "状态", converter = DictConvert.class):使用 DictConvert 转换器,将 status(如 1)转换为文字(如 开启)。
- @DictFormat(DictTypeConstants.COMMON_STATUS):指定字典类型,映射 status 值(0=禁用, 1=开启)。
- 作用:确保导出的 Excel 列名和数据格式符合预期,字段值经过转换(如状态从数字转为文字)。
1.1.3 工具类(ExcelUtils.java - 导出部分)
代码:
public static <T> void write(HttpServletResponse response, String filename, String sheetName,Class<T> head, List<T> data) throws IOException {EasyExcel.write(response.getOutputStream(), head).autoCloseStream(false).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).registerWriteHandler(new SelectSheetWriteHandler(head)).registerConverter(new LongStringConverter()).sheet(sheetName).doWrite(data);response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}
解析:
- 功能:将数据列表写入 Excel 文件,返回给前端。
- 步骤:
- 初始化 EasyExcel:
- EasyExcel.write(response.getOutputStream(), head):使用 EasyExcel 写入响应流,指定 VO 类(如 PostRespVO)。
- 配置写入:
- autoCloseStream(false):不自动关闭流,由 Servlet 管理。
- LongestMatchColumnWidthStyleStrategy:自动调整列宽。
- SelectSheetWriteHandler:支持下拉框(基于 VO 注解)。
- LongStringConverter:防止 Long 类型精度丢失。
- 写入数据:
- sheet(sheetName).doWrite(data):写入数据到指定 Sheet(如 岗位列表)。
- 设置响应头:
- Content-Disposition:指定文件名(如 岗位数据.xls)。
- Content-Type:设置为 Excel 格式(application/vnd.ms-excel)。
- 初始化 EasyExcel:
- 作用:将 PostRespVO 列表转换为 Excel 文件,自动生成表头和数据行。
1.1.4 前端导出(post/index.vue)
代码:
handleExport() {const queryParams = this.queryParams;this.$modal.confirm('是否确认导出所有岗位数据项?').then(() => {this.exportLoading = true;return exportPost(queryParams);}).then(response => {this.$download.excel(response, '岗位数据.xls');}).finally(() => {this.exportLoading = false;});
}
解析:
- 功能:触发岗位数据导出,下载 Excel 文件。
- 步骤:
- 用户交互:
- 用户点击“导出”按钮,弹出确认框。
- 发送请求:
- exportPost(queryParams):调用后端 /admin-api/system/post/export,传递查询参数(如岗位名称、状态)。
- 处理响应:
- this.$download.excel(response, '岗位数据.xls'):将响应(Excel 文件流)保存为本地文件。
- 状态管理:
- exportLoading:控制加载状态,防止重复点击。
- 用户交互:
- 作用:提供用户界面,触发导出请求,处理下载。
1.2 导入流程解析
1.2.1 后端导入(UserController.java)
代码:
@PostMapping("/import")
@Operation(summary = "导入用户")
@Parameters({@Parameter(name = "file", description = "Excel 文件", required = true),@Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
})
@PreAuthorize("@ss.hasPermission('system:user:import')")
public CommonResult<UserImportRespVO> importExcel(@RequestParam("file") MultipartFile file,@RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {List<UserImportExcelVO> list = ExcelUtils.read(file, UserImportExcelVO.class);return success(userService.importUserList(list, updateSupport));
}
解析:
- 功能:从 Excel 文件导入用户数据,保存到数据库。
- 步骤:
- 解析 Excel:
- ExcelUtils.read(file, UserImportExcelVO.class):读取 Excel 文件,转换为 List<UserImportExcelVO>。
- 处理数据:
- userService.importUserList(list, updateSupport):将解析的数据导入数据库,支持更新现有用户(updateSupport=true)。
- 返回结果:
- CommonResult<UserImportRespVO>:返回导入结果(如成功/失败记录数)。
- 解析 Excel:
- 参数:
- file:上传的 Excel 文件(MultipartFile)。
- updateSupport:是否更新已有用户(默认 false)。
- 权限控制:
- @PreAuthorize("@ss.hasPermission('system:user:import')"):确保用户有导入权限。
1.2.2 VO 类(UserImportExcelVO.java)
code:
@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;
}
解析:
- 功能:定义 Excel 导入的数据结构。
- 注解:
- @ExcelProperty:指定 Excel 列名(如 登录名称、用户名称)。
- @ExcelProperty(value = "用户性别", converter = DictConvert.class):将文字(如 男)转换为数字(如 1),基于 USER_SEX 字典。
- @DictFormat:指定字典类型(如 USER_SEX、COMMON_STATUS)。
- @Accessors(chain = false):禁用链式 setter,防止导入时序列化问题。
- 作用:映射 Excel 列到 Java 对象,确保导入数据正确解析。
1.2.3 工具类(ExcelUtils.java - 导入部分)
Code:
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {return EasyExcel.read(file.getInputStream(), head, null).autoCloseStream(false).doReadAllSync();
}
解析:
- 功能:从 Excel 文件读取数据,转换为 Java 对象列表。
- 步骤:
- 初始化 EasyExcel:
- EasyExcel.read(file.getInputStream(), head, null):读取 Excel 文件,指定 VO 类(如 UserImportExcelVO)。
- 读取数据:
- doReadAllSync():同步读取所有数据,返回 List<T>。
- 流管理:
- autoCloseStream(false):不自动关闭流,由 Servlet 管理。
- 初始化 EasyExcel:
- 作用:将 Excel 行转换为 UserImportExcelVO 对象,供后端处理。
1.2.4 前end导入(post/index.vue - 用户导入部分)
Code:
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body><el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers":action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading":on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag><i class="el-icon-upload"></i><div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div><div class="el-upload__tip text-center" slot="tip"><div class="el-upload__tip" slot="tip"><el-checkbox v-model="upload.updateSupport" /> 是否更新已经存在的用户数据</div><span>仅允许导入xls、xlsx格式文件。</span><el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" @click="importTemplate">下载模板</el-link></div></el-upload><div slot="footer" class="dialog-footer"><el-button type="primary" @click="submitFileForm">确 定</el-button><el-button @click="upload.open = false">取 消</el-button></div>
</el-dialog>
解析:
- 功能:提供用户界面,上传 Excel 文件,触发导入。
- 步骤:
- 用户交互:
- 用户点击“导入”按钮,打开对话框,选择 Excel 文件(.xls 或 .xlsx)。
- 勾选 updateSupport(是否更新现有用户)。
- 上传配置:
- accept=".xlsx, .xls":限制文件类型。
- action="upload.url + '?updateSupport=' + upload.updateSupport":动态拼接 URL(如 /admin-api/system/user/import?updateSupport=true)。
- headers:携带认证 token(Authorization: Bearer xxx)。
- on-progress:显示上传进度。
- on-success:处理导入结果,显示成功/失败信息。
- 触发上传:
- submitFileForm:调用 this.$refs.upload.submit(),发送 POST /admin-api/system/user/import 请求。
- 模板下载:
- importTemplate:下载 Excel 模板,引导用户按格式填写。
- 用户交互:
- 作用:提供用户友好的导入界面,支持文件选择和上传。
Excel 全流程总结
导出流程:
- 前端触发:用户在 post/index.vue 点击“导出”按钮,调用 handleExport,发送 GET /admin-api/system/post/export 请求。
- 后端查询:PostController 查询岗位数据(PostDO),转换为 PostRespVO。
- 后端生成 Excel:ExcelUtils.write 使用 EasyExcel 将 PostRespVO 列表写入 Excel,设置响应头。
- 前端下载:前端接收 Excel 文件流,调用 $download.excel 保存为 岗位数据.xls。
导入流程:
- 前端触发:用户在 post/index.vue 打开导入对话框,选择 Excel 文件,设置 updateSupport,点击“确定”触发上传。
- 前端上传:<el-upload> 发送 POST /admin-api/system/user/import 请求,携带 Excel 文件和参数。
- 后端解析:UserController 使用 ExcelUtils.read 解析 Excel 为 List<UserImportExcelVO>。
- 后一致性:userService.importUserList 处理数据,保存到数据库,返回导入结果(UserImportRespVO)。
- 前端反馈:前端显示导入结果(如成功/失败记录数)。
字段转换原理解析
EasyExcel Converter 接口:
- EasyExcel 的 Converter 接口用于在 Excel 和 Java 对象之间进行字段值转换,处理数据格式不一致的情况(如 Excel 中的文字与 Java 中的数字)。
- 核心方法:
- convertToJavaData:将 Excel 单元格值转换为 Java 对象字段值。
- 示例:Excel 的“开启”转换为 Java 的 status = 1。
- convertToExcelData:将 Java 对象字段值转换为 Excel 单元格值。
- 示例:Java 的 status = 1转换为 Excel 的“开启”。
- convertToJavaData:将 Excel 单元格值转换为 Java 对象字段值。
DictConvert 作用:
- DictConvert 是自定义转换器,基于字典框架(DictFrameworkUtils)实现字段值与字典标签的转换。
- 用途:将数字(如 1)与文字(如 开启)相互映射,常用于状态、性别等枚举字段。
2. DictConvert 代码解析
以下是 DictConvert 的代码和逐行解析:
@Slf4j
public class DictConvert implements Converter<Object> {@Overridepublic Class<?> supportJavaTypeKey() {throw new UnsupportedOperationException("暂不支持,也不需要");}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {throw new UnsupportedOperationException("暂不支持,也不需要");}@Overridepublic 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);}@Overridepublic 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();}
}
解析:
- 接口实现:
- 实现 Converter<Object>,支持任意类型的字段转换。
- supportJavaTypeKey 和 supportExcelTypeKey 未实现,表明 DictConvert 不限制字段类型,依靠注解动态处理。
- convertToJavaData(Excel -> Java):
- 获取字典类型:
- getType(contentProperty):从字段的 @DictFormat 注解获取字典类型(如 COMMON_STATUS)。
- 读取 Excel 值:
- readCellData.getStringValue():获取 Excel 单元格的字符串值(如 开启)。
- 字典解析:
- DictFrameworkUtils.parseDictDataValue(type, label):将文字(如 开启)转换为字典值(如 1)。
- 示例:COMMON_STATUS 字典可能定义 { "0": "禁用", "1": "开启" }。
- 类型转换:
- Convert.convert(fieldClazz, value):将字符串值(如 "1")转换为字段类型(如 Integer)。
- 错误处理:
- 如果 value 为 null(字典未找到对应值),记录错误日志,返回 null。
- 获取字典类型:
- convertToExcelData(Java -> Excel):
- 空值处理:
- 如果 object 为 null,返回空字符串。
- 获取字典类型:
- 同上,通过 @DictFormat 获取 type。
- 字典转换:
- DictFrameworkUtils.getDictDataLabel(type, value):将 Java 值(如 1)转换为字典标签(如 开启)。
- 返回结果:
- WriteCellData<String>(label):将标签写入 Excel 单元格。
- 错误处理:
- 如果 label 为 null,记录错误,返回空字符串。
- 空值处理:
- getType:
- 提取字段上的 @DictFormat 注解值(如 COMMON_STATUS),确定字典类型。
作用:
- 实现 Excel 和 Java 之间的双向转换,确保状态、性别等字段在导入导出时保持一致(如 1 <-> 开启)。
- 依赖 DictFrameworkUtils 提供的字典数据(如 COMMON_STATUS、USER_SEX)。
4. EasyExcel 注解解析
以下是文档中提到的 EasyExcel 注解,结合代码和上下文说明其作用,并提供实例。
4.1 @ExcelProperty
- 作用:指定 Excel 列名、序号或转换器。
- 参数:
- value:列名(如 "岗位名称")。
- index:列序号(与 value 二选一)。
- converter:转换器(如 DictConvert.class)。
- 示例:
@ExcelProperty("岗位名称") private String name;
- 输出:Excel 列名为 岗位名称,值来自 name 字段。
4.2 @ColumnWidth
- 作用:设置列宽(单位:字符)。
- 示例:
@ColumnWidth(18) @ExcelProperty("岗位编码") private String code;
- 输出:岗位编码 列宽为 18 个字符。
4.3 @ContentFontStyle
- 作用:设置单元格字体样式。
- 示例:
@ContentFontStyle(fontName = "Arial", fontHeightInPoints = 12, bold = true) @ExcelProperty("岗位名称") private String name;
- 输出:岗位名称 列使用 Arial 字体,12 磅,加粗。
4.4 @ContentLoopMerge
- 作用:合并单元格。
- 示例:
@ContentLoopMerge(eachRow = 2) @ExcelProperty("部门") private String dept;
- 输出:每 2 行合并 部门 列。
4.5 @ContentRowHeight
- 作用:设置行高。
- 示例:
@ContentRowHeight(20) @ExcelProperty("备注") private String remark;
- 输出:备注 行高为 20。
4.6 @ContentStyle
- 作用:设置单元格样式(如对齐、边框、背景色)。
- 示例:
@ContentStyle(horizontalAlignment = HorizontalAlignment.CENTER, fillBackgroundColor = IndexedColors.YELLOW) @ExcelProperty("状态") private Integer status;
- 输出:状态 列居中,黄色背景。
4.7 @HeadFontStyle
- 作用:设置标题字体样式。
- 示例:
@HeadFontStyle(fontName = "Calibri", bold = true) @ExcelProperty("岗位名称") private String name;
- 输出:岗位名称 标题使用 Calibri 字体,加粗。
4.8 @HeadRowHeight
- 作用:设置标题行高。
- 示例:
@HeadRowHeight(25) @ExcelProperty("岗位编码") private String code;
- 输出:标题行高为 25。
4.9 @HeadStyle
- 作用:设置标题样式。
- 示例:
@HeadStyle(fillForegroundColor = IndexedColors.BLUE) @ExcelProperty("状态") private Integer status;
- 输出:状态 标题蓝色背景。
4.10 @ExcelIgnore
- 作用:忽略字段,不导出到 Excel。
- 示例:
@ExcelIgnore private String internalCode;
- 输出:internalCode 不出现在 Excel 中。
4.11 @ExcelIgnoreUnannotated
- 作用:忽略未标注 @ExcelProperty 的字段。
- 示例:
@ExcelIgnoreUnannotated public class PostRespVO { @ExcelProperty("岗位名称") private String name; private String remark; // 未标注 }
- 输出:remark 不导出。