spring boot项目excel导出功能封装——1.简单导出

117 阅读11分钟

说在前面

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

这是封装的依赖库源码:github.com/chenqi92/al…

这是这个依赖库的使用示例:github.com/chenqi92/al…

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

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

使用说明

添加maven依赖

<dependency>  
    <groupId>cn.allbs</groupId>  
    <artifactId>allbs-excel</artifactId>  
    <version>3.0.0</version>  
</dependency>

基本导出

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

@GetMapping("/simple")  
public List<UserDTO> simpleExport(@RequestParam(defaultValue = "10") int count) {  
    return testDataService.generateUsers(count);  
}
@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就可以变成一个导出接口。 上面的代码就变成了:

@GetMapping("/simple") 
// 导出的excel文件名为用户列表,sheet为用户信息 
@ExportExcel(  
    name = "用户列表",  
    sheets = @Sheet(sheetName = "用户信息")  
)  
public List<UserDTO> simpleExport(@RequestParam(defaultValue = "10") int count) {  
    return testDataService.generateUsers(count);  
}
@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注解为

@GetMapping("/empty")  
@ExportExcel(  
    name = "空用户列表",  
    sheets = @Sheet(  
        sheetName = "用户信息", 
        // 指定数据类型用于生成表头   
        clazz = UserDTO.class  
    )  
)  
public List<UserDTO> emptyExport() {  
    return Collections.emptyList();  
}

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

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

@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的名称,并返回对于数量的列表。

@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实现 代码示例:

@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实现 代码示例:

@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;  
    }}

数据验证功能

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

@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行会有验证,其他行不会):

@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);  
}
@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的用法。 代码示例:

@GetMapping("/conditional-style")  
@ExportExcel(  
        name = "条件样式示例",  
        sheets = @Sheet(sheetName = "条件样式示例"),  
        writeHandler = {ConditionalStyleWriteHandler.class}  
)  
public List<ConditionalStyleDTO> conditionalStyleExport(  
        @RequestParam(defaultValue = "20") int count  
) {  
    return testDataService.generateConditionalStyleData(count);  
}
@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字段为随机数值的情况,其他数据为随机生成数据,代码示例:

@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")标明用的是哪个字典。

@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进行对应。

@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中的默认展示大小和对齐方式。 图片列表也是支持的。 示例代码

@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;  
}

话不多说,都在代码里:

@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;  
  
}

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