说在前面

封装的easyexcel,基于注解实现excel的导入导出,以场景来说,就是你有一个现成的分页接口或者一个list接口,只需要添加几个简单的注解,就可以实现excel的导出,也是为了方便有模板生成代码的情况下直接生成导出功能。

这是封装的依赖库源码:https://github.com/chenqi92/allbs-excel

这是这个依赖库的使用示例:https://github.com/chenqi92/allbs-excel-test

依赖库运行后在浏览器中打开:http://localhost:8080/ 即可测试各种示例,参照示例进行使用可以不用看后续的使用说明。

前面三篇功能点较为分散,没有特意合并测试,想了想,如果真要那么复杂的表格设置,貌似写模板最方便。

使用说明

添加maven依赖

1
2
3
4
5
<dependency>  
<groupId>cn.allbs</groupId>
<artifactId>allbs-excel</artifactId>
<version>3.0.0</version>
</dependency>

基本导出

第一个导出我贴比较详细点的代码,后续就直接说需要添加的内容了。
假设你原本有一个这样的接口

1
2
3
4
@GetMapping("/simple")  
public List<UserDTO> simpleExport(@RequestParam(defaultValue = "10") int count) {
return testDataService.generateUsers(count);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data  
public class UserDTO {

@NotNull(message = "用户ID不能为空")
private Long id;

@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;

@Email(message = "邮箱格式不正确")
private String email;

@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

private Integer age;

private String status;
}

那么将这个接口变成导出接口,可以增加这些注解:
@ExportExcel,@ExcelProperty就可以变成一个导出接口。
上面的代码就变成了:

1
2
3
4
5
6
7
8
9
@GetMapping("/simple") 
// 导出的excel文件名为用户列表,sheet为用户信息
@ExportExcel(
name = "用户列表",
sheets = @Sheet(sheetName = "用户信息")
)
public List<UserDTO> simpleExport(@RequestParam(defaultValue = "10") int count) {
return testDataService.generateUsers(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
@Data  
public class UserDTO {

// index 就是列的排序
@ExcelProperty(value = "用户ID", index = 0)
@NotNull(message = "用户ID不能为空")
private Long id;

@ExcelProperty(value = "用户名", index = 1)
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;

@ExcelProperty(value = "邮箱", index = 2)
@Email(message = "邮箱格式不正确")
private String email;

@ExcelProperty(value = "创建时间", index = 3)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

@ExcelProperty(value = "年龄", index = 4)
private Integer age;

@ExcelProperty(value = "状态", index = 5)
private String status;
}

导出的效果为:

这是一个最简单的用法,基本上实际业务有这样的也就可以了。

空表头导出

如果实际情况中你的返回结果列表中没有数据,上面的代码不足以导出一个文件,需要添加一个属性clazz = UserDTO.class,那么需要修改一下controller注解为

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/empty")  
@ExportExcel(
name = "空用户列表",
sheets = @Sheet(
sheetName = "用户信息",
// 指定数据类型用于生成表头
clazz = UserDTO.class
)
)
public List<UserDTO> emptyExport() {
return Collections.emptyList();
}

只导出添加了@ExcelProperty注解的字段

当前的实现为如果你的返回结果类中字段没有添加@ExcelProperty注解,那么导出到excel中列名实际上是字段名,那么如何让导出时只展示添加了注解的字段呢?那就是添加onlyExcelProperty = true属性,同时这种方式也可以避免添加过多的@ExcelIgnore

1
2
3
4
5
6
7
8
9
10
@GetMapping("/only-annotated")  
@ExportExcel(
name = "用户列表-仅注解字段",
sheets = @Sheet(sheetName = "用户信息"),
// 只导出有 @ExcelProperty 注解的字段
onlyExcelProperty = true
)
public List<UserDTO> onlyAnnotatedExport(@RequestParam(defaultValue = "10") int count) {
return testDataService.generateUsers(count);
}

多 Sheet 导出

上述内容讲的都是单个sheet导出,实际业务过程肯定会有多个sheet的导出,以下为示例:
实际上就是设置了多个sheet的名称,并返回对于数量的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/multi-sheet")  
@ExportExcel(
name = "综合报表",
sheets = {
@Sheet(sheetName = "用户信息", clazz = UserDTO.class),
@Sheet(sheetName = "订单信息", clazz = OrderDTO.class)
})
public List<List<?>> multiSheetExport(
@RequestParam(defaultValue = "10") int userCount,
@RequestParam(defaultValue = "20") int orderCount
) {
List<UserDTO> users = testDataService.generateUsers(userCount);
List<OrderDTO> orders = testDataService.generateOrders(orderCount);
return Arrays.asList(users, orders);
}

导出效果:

设定表格的列宽和行高

通过注解@ContentRowHeight(25)@ColumnWidth实现
代码示例:

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  
@ContentRowHeight(25)
public class ProductDTO {

@ExcelProperty(value = "产品ID", index = 0)
@ColumnWidth(10)
private Long id;

@ExcelProperty(value = "产品名称", index = 1)
@ColumnWidth(20)
private String name;

@ExcelProperty(value = "产品分类", index = 2)
@ColumnWidth(15)
private String category;

@ExcelProperty(value = "价格", index = 3)
@ColumnWidth(15)
private BigDecimal price;

@ExcelProperty(value = "库存", index = 4)
@ColumnWidth(10)
private Integer stock;

@ExcelProperty(value = "产品描述", index = 5)
@ColumnWidth(50)
private String description;
}

实现效果:

自动行号

实际上没啥用的功能,就是多一列序号。
通过添加注解@ExcelLine实现
代码示例:

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
@Data  
@NoArgsConstructor
@AllArgsConstructor
public class RowNumberDTO {

/**
* Excel行号 (自动填充,从1开始)
*/ @ExcelLine
@ExcelProperty(value = "行号", index = 0)
private Long rowNum;

@ExcelProperty(value = "产品名称", index = 1)
private String productName;

@ExcelProperty(value = "产品代码", index = 2)
private String productCode;

@ExcelProperty(value = "价格", index = 3)
private Double price;

@ExcelProperty(value = "库存", index = 4)
private Integer stock;

@ExcelProperty(value = "分类", index = 5)
private String category;

/**
* 构造方法(不包含rowNum,rowNum由@ExcelLine自动填充)
*/ public RowNumberDTO(String productName, String productCode, Double price, Integer stock, String category) {
this.productName = productName;
this.productCode = productCode;
this.price = price;
this.stock = stock;
this.category = category;
}}

数据验证功能

限制单元格中的内容,作为导入模板还是挺有用的。
代码示例:

1
2
3
4
5
6
7
8
9
@GetMapping("/validation")  
@ExportExcel(
name = "员工信息-数据验证",
sheets = @Sheet(sheetName = "员工信息"),
writeHandler = {ExcelValidationWriteHandler.class}
)
public List<DataValidationDTO> validationExport(@RequestParam(defaultValue = "10") int count) {
return testDataService.generateDataValidationData(count);
}

如果是要设置指定返回行进行验证可以这么写(1~1000行会有验证,其他行不会):

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/validation-custom")  
@ExportExcel(
name = "员工信息-自定义验证",
sheets = @Sheet(
sheetName = "员工信息",
validationStartRow = 1,
validationEndRow = 1000
)
)
public List<DataValidationDTO> validationCustomExport(@RequestParam(defaultValue = "10") int count) {
return testDataService.generateDataValidationData(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
71
72
73
74
75
76
77
@Data  
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DataValidationDTO {

/**
* 姓名 - 文本长度验证
*/
@ExcelProperty(value = "姓名", index = 0)
@ExcelValidation(type = ValidationType.TEXT_LENGTH, minLength = 2, maxLength = 10, errorMessage = "姓名长度必须在2-10个字符之间", promptMessage = "请输入2-10个字符的姓名", showPromptBox = true)
private String name;

/**
* 性别 - 下拉列表验证
*/
@ExcelProperty(value = "性别", index = 1)
@ExcelValidation(type = ValidationType.LIST, options = { "男", "女" }, errorMessage = "性别只能选择:男、女", promptMessage = "请选择性别", showPromptBox = true)
private String gender;

/**
* 年龄 - 整数范围验证
*/
@ExcelProperty(value = "年龄", index = 2)
@ExcelValidation(type = ValidationType.INTEGER, min = 18, max = 65, errorMessage = "年龄必须在18-65之间", promptMessage = "请输入18-65之间的整数", showPromptBox = true)
private Integer age;

/**
* 工资 - 小数范围验证
*/
@ExcelProperty(value = "工资", index = 3)
@ExcelValidation(type = ValidationType.DECIMAL, min = 3000.0, max = 50000.0, errorMessage = "工资必须在3000-50000之间", promptMessage = "请输入3000-50000之间的数值", showPromptBox = true)
private Double salary;

/**
* 入职日期 - 日期验证
*/
@ExcelProperty(value = "入职日期", index = 4)
@ExcelValidation(type = ValidationType.DATE, dateFormat = "yyyy-MM-dd", errorMessage = "请输入有效的日期格式", promptMessage = "请输入日期,格式:yyyy-MM-dd", showPromptBox = true)
private LocalDate hireDate;

/**
* 部门 - 下拉列表验证
*/
@ExcelProperty(value = "部门", index = 5)
@ExcelValidation(type = ValidationType.LIST, options = { "技术部", "销售部", "人事部", "财务部", "运营部" }, errorMessage = "请从下拉列表中选择部门", promptMessage = "请选择部门", showPromptBox = true)
private String department;

/**
* 职位 - 下拉列表验证
*/
@ExcelProperty(value = "职位", index = 6)
@ExcelValidation(type = ValidationType.LIST, options = { "初级工程师", "中级工程师", "高级工程师", "技术经理", "技术总监" }, errorMessage = "请从下拉列表中选择职位", promptMessage = "请选择职位", showPromptBox = true)
private String position;

/**
* 工作年限 - 整数验证(最小值)
*/
@ExcelProperty(value = "工作年限", index = 7)
@ExcelValidation(type = ValidationType.INTEGER, min = 0, max = 50, errorMessage = "工作年限必须大于等于0", promptMessage = "请输入工作年限(年)", showPromptBox = true)
private Integer workYears;

/**
* 绩效评分 - 小数验证
*/
@ExcelProperty(value = "绩效评分", index = 8)
@ExcelValidation(type = ValidationType.DECIMAL, min = 0.0, max = 10.0, errorMessage = "绩效评分必须在0-10之间", promptMessage = "请输入0-10之间的评分", showPromptBox = true)
private Double performanceScore;

/**
* 邮箱 - 任意值(仅提示)
*/
@ExcelProperty(value = "邮箱", index = 9)
@ExcelValidation(type = ValidationType.ANY, promptMessage = "请输入有效的邮箱地址,例如:example@company.com", showPromptBox = true, showErrorBox = false)
private String email;

}

实际的效果如图:

条件样式

根据单元格值自动应用不同样式,不同的颜色、不同字体等,比如以学生分数90分以上一个颜色,60-90一个颜色,60以下一个颜色。注意下方的注解ConditionalStyle的用法。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/conditional-style")  
@ExportExcel(
name = "条件样式示例",
sheets = @Sheet(sheetName = "条件样式示例"),
writeHandler = {ConditionalStyleWriteHandler.class}
)
public List<ConditionalStyleDTO> conditionalStyleExport(
@RequestParam(defaultValue = "20") int count
) {
return testDataService.generateConditionalStyleData(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
@Data  
@NoArgsConstructor
@AllArgsConstructor
public class ConditionalStyleDTO {

@ExcelProperty("学生姓名")
private String studentName;

@ExcelProperty("考试分数")
@ConditionalStyle(conditions = { @Condition(value = ">=90", style = @CellStyleDef(backgroundColor = "#00FF00", // 绿色
bold = true)), @Condition(value = ">=60", style = @CellStyleDef(backgroundColor = "#FFFF00" // 黄色
)), @Condition(value = "<60", style = @CellStyleDef(backgroundColor = "#FF0000", // 红色
fontColor = "#FFFFFF")) })
private Integer score;

@ExcelProperty("任务状态")
@ConditionalStyle(conditions = {
@Condition(value = "已完成", style = @CellStyleDef(backgroundColor = "#00FF00", fontColor = "#FFFFFF")),
@Condition(value = "进行中", style = @CellStyleDef(backgroundColor = "#FFFF00")),
@Condition(value = "已取消", style = @CellStyleDef(backgroundColor = "#808080", fontColor = "#FFFFFF")) })
private String status;

@ExcelProperty("销售额")
@ConditionalStyle(conditions = {
@Condition(value = ">=10000", style = @CellStyleDef(backgroundColor = "#FFD700", // 金色
bold = true)),
@Condition(value = ">=5000", style = @CellStyleDef(backgroundColor = "#87CEEB" // 天蓝色
)) })
private BigDecimal salesAmount;

@ExcelProperty("等级")
@ConditionalStyle(conditions = {
@Condition(value = "regex:^A.*", style = @CellStyleDef(backgroundColor = "#00FF00", bold = true)),
@Condition(value = "regex:^B.*", style = @CellStyleDef(backgroundColor = "#FFFF00")),
@Condition(value = "regex:^C.*", style = @CellStyleDef(backgroundColor = "#FFA500")) // 橙色
})
private String grade;

}

导出示例:

导出数据字典转换

实际业务开发过程中肯定无法避免字段以tinyint储存或者有枚举的情况,那么如何处理这种情况?
如下是直接塞入sexstatus字段为随机数值的情况,其他数据为随机生成数据,代码示例:

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
@GetMapping("/export")  
@ExportExcel(
name = "字典转换示例",
sheets = @Sheet(sheetName = "用户信息")
)
public List<DictExampleDTO> exportDictExample(@RequestParam(defaultValue = "10") int count) {
List<DictExampleDTO> list = new ArrayList<>();
Random random = new Random();

String[] departments = {"技术部", "市场部", "人事部", "财务部", "运营部"};
String[] usernames = {"张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"};

for (int i = 1; i <= count; i++) {
DictExampleDTO dto = new DictExampleDTO();
dto.setId((long) i);
dto.setUsername(usernames[random.nextInt(usernames.length)] + i);

// 性别: 数据库存储 0/1/2,导出时会转换为 女/男/未知
dto.setSex(String.valueOf(random.nextInt(3))); // 0, 1, 2

// 状态: 数据库存储 0/1/2,导出时会转换为 正常/禁用/锁定
dto.setStatus(String.valueOf(random.nextInt(3))); // 0, 1, 2

dto.setDepartment(departments[random.nextInt(departments.length)]);
dto.setCreateTime(LocalDateTime.now().minusDays(random.nextInt(365)));
dto.setRemark("这是测试数据 " + i);

list.add(dto);
}
return list;
}

处理方式为在ExcelProperty中添加属性converter = DictConverter.class并额外添加注解@ExcelDict(dictType = "sys_user_sex")标明用的是哪个字典。

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
@Data  
public class DictExampleDTO {

@ExcelProperty(value = "用户ID", index = 0)
private Long id;

@ExcelProperty(value = "用户名", index = 1)
private String username;

/**
* 性别字典转换示例
* 导出时: 0 → 女, 1 → 男, 2 → 未知
* 导入时: 女 → 0, 男 → 1, 未知 → 2
*/ @ExcelProperty(value = "性别", index = 2, converter = DictConverter.class)
@ExcelDict(dictType = "sys_user_sex")
private String sex;

/**
* 状态字典转换示例
* 导出时: 0 → 正常, 1 → 禁用, 2 → 锁定
* 导入时: 正常 → 0, 禁用 → 1, 锁定 → 2
*/ @ExcelProperty(value = "状态", index = 3, converter = DictConverter.class)
@ExcelDict(dictType = "sys_user_status")
private String status;

@ExcelProperty(value = "部门", index = 4)
private String department;

@ExcelProperty(value = "创建时间", index = 5)
private LocalDateTime createTime;

@ExcelProperty(value = "备注", index = 6)
private String remark;
}

这个DictConverter为封装的转换器,同时需要有对应的service实现。示例代码:
static中就是你所需实际项目中的字典值,需要将注解的dictType字段和下面key进行对应。

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
@Service  
public class DictServiceImpl implements DictService {

// 模拟字典数据
private static final Map<String, Map<String, String>> DICT_DATA = new HashMap<>();

static {
// 性别字典
Map<String, String> sexDict = new HashMap<>();
sexDict.put("0", "女");
sexDict.put("1", "男");
sexDict.put("2", "未知");
DICT_DATA.put("sys_user_sex", sexDict);

// 状态字典
Map<String, String> statusDict = new HashMap<>();
statusDict.put("0", "正常");
statusDict.put("1", "禁用");
statusDict.put("2", "锁定");
DICT_DATA.put("sys_user_status", statusDict);
}
@Override
public String getLabel(String dictType, String dictValue) {
Map<String, String> dict = DICT_DATA.get(dictType);
if (dict != null) {
return dict.get(dictValue);
}
return dictValue;
}
@Override
public String getValue(String dictType, String dictLabel) {
Map<String, String> dict = DICT_DATA.get(dictType);
if (dict != null) {
for (Map.Entry<String, String> entry : dict.entrySet()) {
if (entry.getValue().equals(dictLabel)) {
return entry.getKey();
}
}
}
return dictLabel;
}
}

带图片的导出

导出肯定会碰到有图片导出的情况,这种情况也是支持的。主要通过@ExcelImage注解实现。如下代码使用的是生成的base64图片,当然本地图片和图片链接也是支持的。支持给图片设置excel中的默认展示大小和对齐方式
图片列表也是支持的。
示例代码

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
@GetMapping("/export")  
@ExportExcel(name = "商品图片列表", sheets = @Sheet(sheetName = "商品信息", clazz = ProductImageDTO.class))
public List<ProductImageDTO> exportWithImages() {
List<ProductImageDTO> products = new ArrayList<>();

// 生成示例图片数据(彩色方块)
byte[] redImage = generateColorSquare(120, 120, 255, 0, 0); // 红色方块
byte[] greenImage = generateColorSquare(80, 80, 0, 255, 0); // 绿色方块
byte[] blueImage = generateColorSquare(100, 100, 0, 0, 255); // 蓝色方块

// 商品1:使用 Base64 图片
ProductImageDTO product1 = new ProductImageDTO();
product1.setId(1L);
product1.setName("iPhone 15 Pro");
product1.setPrice(new BigDecimal("7999.00"));
product1.setMainImage("data:image/png;base64," + Base64.getEncoder().encodeToString(redImage));
product1.setThumbnail(greenImage);
product1.setImageList(Arrays.asList("data:image/png;base64," + Base64.getEncoder().encodeToString(blueImage),
"data:image/png;base64," + Base64.getEncoder().encodeToString(redImage)));
product1.setStock(100);
product1.setDescription("最新款 iPhone 15 Pro,A17 Pro 芯片");
products.add(product1);

// 商品2:使用字节数组
ProductImageDTO product2 = new ProductImageDTO();
product2.setId(2L);
product2.setName("MacBook Pro 16");
product2.setPrice(new BigDecimal("19999.00"));
product2.setMainImage("data:image/png;base64," + Base64.getEncoder().encodeToString(blueImage));
product2.setThumbnail(redImage);
product2.setImageList(Arrays.asList("data:image/png;base64," + Base64.getEncoder().encodeToString(greenImage),
"data:image/png;base64," + Base64.getEncoder().encodeToString(blueImage)));
product2.setStock(50);
product2.setDescription("16 英寸 MacBook Pro,M3 Max 芯片");
products.add(product2);

// 商品3:使用不同尺寸的图片
byte[] largeImage = generateColorSquare(150, 150, 255, 165, 0); // 橙色方块
ProductImageDTO product3 = new ProductImageDTO();
product3.setId(3L);
product3.setName("iPad Air");
product3.setPrice(new BigDecimal("4799.00"));
product3.setMainImage("data:image/png;base64," + Base64.getEncoder().encodeToString(largeImage));
product3.setThumbnail(greenImage);
product3.setImageList(Collections.singletonList(
"data:image/png;base64," + Base64.getEncoder().encodeToString(redImage)));
product3.setStock(200);
product3.setDescription("11 英寸 iPad Air,M2 芯片");
products.add(product3);

log.info("Exporting {} products with images", products.size());
return products;
}

话不多说,都在代码里:

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
@Data  
@NoArgsConstructor
@AllArgsConstructor
public class ProductImageDTO {

@ExcelProperty("商品ID")
private Long id;

@ExcelProperty("商品名称")
private String name;

@ExcelProperty("商品价格")
private BigDecimal price;

/**
* 商品主图(单张图片)
* <p>
* 导出时:支持 URL、本地路径、Base64
* 导入时:读取为 Base64 字符串
* </p>
*/
@ExcelProperty("商品主图")
@ExcelImage(width = 120, height = 120)
private String mainImage;

/**
* 商品缩略图(字节数组)
* <p>
* 导出时:直接使用字节数组
* 导入时:读取为字节数组
* </p>
*/
@ExcelProperty("商品缩略图")
@ExcelImage(width = 80, height = 80, type = ExcelImage.ImageType.BYTES)
private byte[] thumbnail;

/**
* 商品图集(多张图片)
* <p>
* 导出时:支持多张图片水平排列
* 导入时:读取为 Base64 字符串列表
* </p>
*/
@ExcelProperty(value = "商品图集", converter = cn.allbs.excel.convert.ImageListConverter.class)
@ExcelImage(width = 100, height = 100)
private List<String> imageList;

@ExcelProperty("库存数量")
private Integer stock;

@ExcelProperty("商品描述")
private String description;

}

篇幅有限,有兴趣可以继续看下一篇中更多的应用方式,有些还是挺实用的。下下篇就没啥用处了,下下下篇可以看看。