说在前面
封装的easyexcel,基于注解实现excel的导入导出,以场景来说,就是你有一个现成的分页接口或者一个list接口,只需要添加几个简单的注解,就可以实现excel的导出,也是为了方便有模板生成代码的情况下直接生成导出功能。
这是封装的依赖库源码:https://github.com/chenqi92/allbs-excel
这是这个依赖库的使用示例:https://github.com/chenqi92/allbs-excel-test
依赖库运行后在浏览器中打开:http://localhost:8080/ 即可测试各种示例,参照示例进行使用可以不用看后续的使用说明。
这是第二篇了,第一篇可以往前翻翻,后一篇是讲excel中的统计图的,没啥用可以跳过之后看最后一篇导入篇。
使用说明
添加maven依赖
1 2 3 4 5
| <dependency> <groupId>cn.allbs</groupId> <artifactId>allbs-excel</artifactId> <version>3.0.0</version> </dependency>
|
合并单元格
导出中不可避免遇到的情况,但是需要将需要合并的字段进行排序,以便能够识别并自动合并。
主要通过@ExportExcel的autoMerge = true属性实现,并在对应字段添加注解@ExcelMerge
代码示例:
1 2 3 4 5 6 7 8 9 10 11
| @GetMapping("/merge") @ExportExcel( name = "部门员工列表", sheets = @Sheet( sheetName = "员工信息", autoMerge = true // 启用自动合并 ) ) public List<EmployeeDTO> mergeExport(@RequestParam(defaultValue = "20") int count) { return testDataService.generateEmployees(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Data public class EmployeeDTO { @ExcelProperty(value = "部门", index = 0) @ExcelMerge private String department; @ExcelProperty(value = "姓名", index = 1) @ExcelMerge(dependOn = "department") private String name; @ExcelProperty(value = "职位", index = 2) @ExcelMerge(dependOn = "name") private String position; @ExcelProperty(value = "工资", index = 3) private BigDecimal salary; @ExcelProperty(value = "入职日期", index = 4) private String joinDate; }
|
实际效果(数据并不合理,姓名一列专门设置了一样的名字用于测试合并):

数据脱敏
实际导出过程中肯定无法避免遇到需要数据脱敏的情况,如果是数据库层面脱敏过这边可以忽略,但是如果数据库层面没脱敏但是又需要在最后展示进行脱敏,就是这个功能的实际应用。
主要方式依赖字段注解@Desensitize,内置提供手机号、身份证号、邮箱、银行卡号、姓名、地址、固定电话、车牌号、自定义这些脱敏方式,如果内置的脱敏达不到你的要求,可以进行自定义实现,自定义采取设置前几位和最后几位保留的脱敏方式,除了前后的指定位数,其他内容变更为*。本来还想可以自定义字符填充,想想挺傻的,用默认的*就可以了。
使用示例:
1 2 3 4 5 6 7 8
| @GetMapping("/desensitize") @ExportExcel( name = "敏感信息用户列表", sheets = @Sheet(sheetName = "用户信息") ) public List<SensitiveUserDTO> desensitizeExport(@RequestParam(defaultValue = "10") int count) { return testDataService.generateSensitiveUsers(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @Data public class SensitiveUserDTO { @ExcelProperty(value = "用户ID", index = 0) private Long id; @ExcelProperty(value = "姓名", index = 1, converter = DesensitizeConverter.class) @Desensitize(type = DesensitizeType.NAME) private String name; @ExcelProperty(value = "手机号", index = 2, converter = DesensitizeConverter.class) @Desensitize(type = DesensitizeType.MOBILE_PHONE) private String phone; @ExcelProperty(value = "身份证", index = 3, converter = DesensitizeConverter.class) @Desensitize(type = DesensitizeType.ID_CARD) private String idCard; @ExcelProperty(value = "邮箱", index = 4, converter = DesensitizeConverter.class) @Desensitize(type = DesensitizeType.EMAIL) private String email; @ExcelProperty(value = "银行卡", index = 5, converter = DesensitizeConverter.class) @Desensitize(type = DesensitizeType.BANK_CARD) private String bankCard; @ExcelProperty(value = "地址", index = 6, converter = DesensitizeConverter.class) @Desensitize(type = DesensitizeType.ADDRESS) private String address; @ExcelProperty(value = "性别", index = 7, converter = DictConverter.class) @ExcelDict(dictType = "sys_user_sex") private String sex; @ExcelProperty(value = "状态", index = 8, converter = DictConverter.class) @ExcelDict(dictType = "sys_user_status") private String status; }
|
实现效果:

导出进度
如果数据量较大,提供给前端实时进度,避免用户焦躁。这是实际效果,在allbs-excel-test中有具体的实现了,主要通过接口方法注解@ExportProgress进行实现,源码中定义了一个接口ExportProgressListener,通过实现这个接口可以获取导出过程中的进度,然后通过websocket或者sse主动推送给前端即可。

代码实现,注意下面的ExportProgress:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @GetMapping("/with-progress") @ExportExcel( name = "用户列表-带进度", sheets = @Sheet(sheetName = "用户信息") ) @ExportProgress( listener = SseProgressListener.class, interval = 100 // 每 100 行触发一次进度回调 ) public List<SensitiveUserDTO> exportWithProgress(@RequestParam(defaultValue = "5000") int count, @RequestParam(required = false) String sessionId) { return testDataService.generateSensitiveUsers(count); }
|
这是进度的实现,实现进度监听然后处理开始、进度更新、结束、错误几个方法即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| @Component public class SseProgressListener implements ExportProgressListener { private static final Logger log = LoggerFactory.getLogger(SseProgressListener.class); @Autowired private ProgressService progressService; @Override public void onStart(int totalRows, String sheetName) { log.info("========== 开始导出 =========="); log.info("Sheet名称: {}", sheetName); log.info("总行数: {}", totalRows); String sessionId = getSessionId(); if (sessionId != null) { progressService.sendProgress(sessionId, ProgressService.ProgressMessage.start(totalRows, sheetName)); } } @Override public void onProgress(int currentRow, int totalRows, double percentage, String sheetName) { log.info("导出进度: {}/{} ({:.2f}%) - {}", currentRow, totalRows, percentage, sheetName); String sessionId = getSessionId(); if (sessionId != null) { progressService.sendProgress(sessionId, ProgressService.ProgressMessage.progress(currentRow, totalRows, percentage, sheetName)); } } @Override public void onComplete(int totalRows, String sheetName) { log.info("========== 导出完成 =========="); log.info("Sheet名称: {}", sheetName); log.info("总行数: {}", totalRows); String sessionId = getSessionId(); if (sessionId != null) { progressService.sendProgress(sessionId, ProgressService.ProgressMessage.complete(totalRows, sheetName)); new Thread(() -> { try { Thread.sleep(5000); progressService.closeEmitter(sessionId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); } } @Override public void onError(Exception exception, String sheetName) { log.error("========== 导出失败 =========="); log.error("Sheet名称: {}", sheetName); log.error("错误信息: {}", exception.getMessage(), exception); String sessionId = getSessionId(); if (sessionId != null) { progressService.sendProgress(sessionId, ProgressService.ProgressMessage.error(sheetName, exception.getMessage())); progressService.closeEmitter(sessionId); } }
private String getSessionId() { try { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { return attributes.getRequest().getParameter("sessionId"); } } catch (Exception e) { log.warn("无法获取 sessionId: {}", e.getMessage()); } return null; }}
|
冻结窗格和筛选
当前示例为将第一行冻结,无论你滚动到哪一行可以保证知道某列是干什么的,同时实现某一列的筛选功能。说实话这玩意完全可以导出后自己设置一下,但是如果你遇到操作excel有很大进步空间的的现场运维,那么这个功能还是有点意义的。看下方示例的ExcelSheetStyle注解。
实现代码:
1 2 3 4 5 6 7 8
| @GetMapping("/formula") @ExportExcel( name = "销售统计数据", sheets = @Sheet(sheetName = "销售统计") ) public List<FormulaDataDTO> formulaExport(@RequestParam(defaultValue = "10") int count) { return testDataService.generateFormulaData(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Data @NoArgsConstructor @AllArgsConstructor @ExcelSheetStyle( freezeRow = 1, autoFilter = true, defaultColumnWidth = 15 ) public class FormulaDataDTO { @ExcelProperty(value = "Product Name", index = 0) private String productName; @ExcelProperty(value = "Unit Price", index = 1) private BigDecimal unitPrice; @ExcelProperty(value = "Quantity", index = 2) private Integer quantity; @ExcelProperty(value = "Total Price", index = 3) private BigDecimal totalPrice; @ExcelProperty(value = "Tax (10%)", index = 4) private BigDecimal taxAmount; @ExcelProperty(value = "Final Amount", index = 5) private BigDecimal finalAmount; }
|
实际效果(不是2-10没了,是我往下滚动隐藏了,体现了冻结窗口之功能):

图标集
该功能是为了让冰冷无趣的文字鲜活一些,说直白一点就是没什么卵用的功能。既然是没啥用的功能,我就不详细说了,贴个代码示例,有兴趣的去看源码好了。
实现效果:

代码示例:
1 2 3 4 5 6 7 8 9
| @GetMapping("/conditional-format") @ExportExcel( name = "员工绩效-条件格式", sheets = @Sheet(sheetName = "员工绩效"), writeHandler = {ConditionalFormatWriteHandler.class} ) public List<PerformanceDataDTO> conditionalFormatExport(@RequestParam(defaultValue = "15") int count) { return testDataService.generatePerformanceData(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Data @NoArgsConstructor @AllArgsConstructor public class PerformanceDataDTO { @ExcelProperty(value = "Employee", index = 0) private String employeeName; @ExcelProperty(value = "Department", index = 1) private String department; @ExcelProperty(value = "Score", index = 2) @ConditionalFormat( type = ConditionalFormat.FormatType.ICON_SET, iconSet = ConditionalFormat.IconSetType.THREE_TRAFFIC_LIGHTS_1 ) private Integer score; @ExcelProperty(value = "Sales Amount", index = 3) @ConditionalFormat( type = ConditionalFormat.FormatType.ICON_SET, iconSet = ConditionalFormat.IconSetType.THREE_ARROWS ) private BigDecimal sales; @ExcelProperty(value = "Completion Rate", index = 4) @ConditionalFormat( type = ConditionalFormat.FormatType.ICON_SET, iconSet = ConditionalFormat.IconSetType.THREE_FLAGS ) private Double completionRate; }
|
Excel 加密
包含两种方式,一种简单的设置即可,还有一种是自定义加密算法(这种方法不支持注解形式,但是我会提供示例)
这是简单加密的示例,看到那个password属性没,加个这玩意即可。
1 2 3 4 5 6 7 8 9
| GetMapping("/encrypted") @ExportExcel( name = "加密文件-密码password123", sheets = @Sheet(sheetName = "敏感数据"), password = "password123" // ✨ 使用注解方式设置密码 ) public List<SensitiveUserDTO> encryptedExport(@RequestParam(defaultValue = "20") int count) { return testDataService.generateSensitiveUsers(count); }
|
这是指定加密算法的方式,非注解,根据传入的密码加密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @GetMapping("/encrypted-advanced") public void encryptedAdvancedExport(@RequestParam(defaultValue = "20") int count, @RequestParam(defaultValue = "password123") String password, HttpServletResponse response) throws IOException { List<SensitiveUserDTO> data = testDataService.generateSensitiveUsers(count); File tempFile = File.createTempFile("excel_", ".xlsx"); tempFile.deleteOnExit(); EasyExcel.write(tempFile, SensitiveUserDTO.class) .sheet("敏感数据") .doWrite(data); File encryptedFile = File.createTempFile("encrypted_", ".xlsx"); encryptedFile.deleteOnExit(); ExcelEncryptionUtil.encryptFile( tempFile, encryptedFile, password, ExcelEncryption.EncryptionAlgorithm.AGILE ); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("加密文件-AGILE-密码" + password, "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); try (FileInputStream fis = new FileInputStream(encryptedFile)) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { response.getOutputStream().write(buffer, 0, bytesRead); } } tempFile.delete(); encryptedFile.delete(); }
|
多 Sheet 关联导出
可能会有人问,前面一篇明明有过多sheet啊,实际上并不是同一种用法。前一篇是通过返回list的list实现,这边是视图对象中有额外的对象list,所以说完全是两种截然不同的实现。看示例代码
示例代码:
1 2 3 4 5 6 7 8
| @GetMapping("/multi-sheet") @ExportExcel( name = "订单及明细-多Sheet", sheets = @Sheet(sheetName = "订单") ) public List<MultiSheetOrderDTO> multiSheetExport(@RequestParam(defaultValue = "5") int count) { return testDataService.generateMultiSheetOrders(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @Data @Builder @NoArgsConstructor @AllArgsConstructor public class MultiSheetOrderDTO {
@ExcelProperty(value = "订单号", index = 0) private String orderNo;
@ExcelProperty(value = "客户名称", index = 1) private String customerName;
@ExcelProperty(value = "订单金额", index = 2) private BigDecimal totalAmount;
@ExcelProperty(value = "订单状态", index = 3) private String status;
@ExcelProperty(value = "创建时间", index = 4) private LocalDateTime createTime;
@ExcelProperty(value = "明细数量", index = 5) private Integer itemCount;
@ExcelProperty(value = "操作", index = 6) @RelatedSheet(sheetName = "订单明细", relationKey = "orderNo", dataType = MultiSheetOrderItemDTO.class, createHyperlink = true, hyperlinkText = "查看明细") private List<MultiSheetOrderItemDTO> items; }
|
嵌套对象
上面是视图对象中有对象的list,那么肯定也会存在对象中有对象的情况,对象的对象中还有对象,那么肯定就会存在某干层级下有所需字段的情况:
以下代码包含了多种不同的情况所以稍微有点复杂,包括了嵌套单个对象,嵌套基本类型的列表,嵌套Map,看代码和结果理解:
1 2 3 4 5 6 7 8
| @GetMapping("/nested-property") @ExportExcel( name = "嵌套对象示例", sheets = @Sheet(sheetName = "员工信息") ) public List<NestedPropertyExampleDTO> nestedPropertyExport(@RequestParam(defaultValue = "10") int count) { return testDataService.generateNestedPropertyExamples(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| @Data @NoArgsConstructor @AllArgsConstructor public class NestedPropertyExampleDTO { @ExcelProperty("员工ID") private Long id; @ExcelProperty("员工姓名") private String name; @ExcelProperty(value = "部门名称", converter = NestedObjectConverter.class) @NestedProperty("name") private Department department; @ExcelProperty(value = "部门编码", converter = NestedObjectConverter.class) @NestedProperty(value = "code", nullValue = "未分配") private Department department2; @ExcelProperty(value = "直属领导", converter = NestedObjectConverter.class) @NestedProperty(value = "leader.name", nullValue = "暂无") private Department department3; @ExcelProperty(value = "领导电话", converter = NestedObjectConverter.class) @NestedProperty(value = "leader.phone", nullValue = "-") private Department department4; @ExcelIgnore private List<String> skills; @ExcelProperty(value = "主要技能", converter = NestedObjectConverter.class) @NestedProperty(value = "[0]", nullValue = "无") private List<String> mainSkill; @ExcelProperty(value = "所有技能", converter = NestedObjectConverter.class) @NestedProperty(value = "[*]", separator = ",", maxJoinSize = 5) private List<String> allSkills; @ExcelIgnore private Map<String, Object> properties; @ExcelProperty(value = "工作城市", converter = NestedObjectConverter.class) @NestedProperty(value = "[city]", nullValue = "-") private Map<String, Object> city; @ExcelProperty(value = "入职年份", converter = NestedObjectConverter.class) @NestedProperty(value = "[joinYear]", nullValue = "-") private Map<String, Object> joinYear; }
|
实际导出的excel为(第一个红框是对象的对象中的内容,第二个框为对象的对象的对象中的内容,第三个箭头为list只获取第一个的情况,第四个箭头为获取所有list数据并用逗号分隔的情况,最后俩箭头就是从Map中取数据的情况,不要被最后俩map字段名称搞混,实际上上面的[city]和[joinYear]是map的key):
![[Pasted image 20251123194422.png]]
嵌套对象自动横展开
上面的一个示例讲的是从对象的对象中获取一个值的方法,这个示例讲的是讲对象中的对象所有值都带上并展示到excel中的方法。
代码示例(注意看department、parentDept、managerDept三个字段):
1 2 3 4 5 6 7 8 9
| @GetMapping("/flatten-property") @ExportExcel( name = "对象展开示例", sheets = @Sheet(sheetName = "员工信息") ) public List<FlattenPropertyExampleDTO> flattenPropertyExport(@RequestParam(defaultValue = "10") int count) { return testDataService.generateFlattenPropertyExamples(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Data @NoArgsConstructor @AllArgsConstructor public class FlattenPropertyExampleDTO { @ExcelProperty("员工ID") private Long id; @ExcelProperty("员工姓名") private String name; @ExcelProperty("年龄") private Integer age; @FlattenProperty(prefix = "部门-") @ExcelIgnore private Department department; @FlattenProperty(prefix = "上级部门-") @ExcelIgnore private Department parentDept; @FlattenProperty(prefix = "主管-", recursive = true, maxDepth = 2) @ExcelIgnore private Department managerDept; }
|
实际效果:

嵌套对象垂直展开
视图对象中嵌套对象的list,将这个list对象的内容一并输出到excel表格中。
代码示例:
1 2 3 4 5 6 7 8
| @GetMapping("/flatten-list-student") @ExportExcel( name = "学生课程奖项列表", sheets = @Sheet(sheetName = "学生信息") ) public List<FlattenListStudentDTO> flattenListStudentExport(@RequestParam(defaultValue = "5") int count) { return testDataService.generateFlattenListStudents(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Data @NoArgsConstructor @AllArgsConstructor public class FlattenListStudentDTO { @ExcelProperty("学生姓名") private String name; @ExcelProperty("学号") private String studentNo; @ExcelProperty("班级") private String className; @FlattenList(prefix = "课程-") private List<Course> courses; @FlattenList(prefix = "奖项-") private List<Award> awards; }
|
实际导出效果:

动态表头
如果视图对象中存在Map,那么会将该Map内容动态生成表头并填充数据,注意下面的@DynamicHeaders注解的使用。
1 2 3 4 5 6 7 8
| @GetMapping("/dynamic-header") @ExportExcel( name = "动态表头示例", sheets = @Sheet(sheetName = "产品列表") ) public List<DynamicHeaderDTO> dynamicHeaderExport(@RequestParam(defaultValue = "15") int count) { return testDataService.generateDynamicHeaderData(count); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Data @NoArgsConstructor @AllArgsConstructor public class DynamicHeaderDTO { @ExcelProperty("产品ID") private Long productId; @ExcelProperty("产品名称") private String productName; @ExcelProperty("分类") private String category;
@DynamicHeaders(strategy = DynamicHeaderStrategy.FROM_DATA, headerPrefix = "属性-", order = DynamicHeaders.SortOrder.ASC) private Map<String, Object> properties;
@DynamicHeaders(strategy = DynamicHeaderStrategy.FROM_CONFIG, headers = { "备注1", "备注2", "备注3" }, headerPrefix = "扩展-") private Map<String, Object> extFields; }
|
实际效果:
