2025-05-08🌱上海: ☀️ 🌡️+19°C 🌬️↖19km/h

# **Part008 通用 Excel 导出功能实战

# 1. 为什么(Why)

# 1.1 项目背景

part008 模块实现了一个基于 Java 的通用 Excel 导出框架,解决了企业应用中数据导出功能的常见需求。在实际业务系统中,数据导出是一个高频操作,如导出用户列表、订单记录、报表数据等。传统的 Excel 导出实现方式往往缺乏统一标准,不同模块的导出功能实现差异较大,代码复用性低,维护成本高。本模块设计了一套灵活、可扩展的 Excel 导出框架,通过统一的 API 和面向对象的设计,简化了 Excel 导出功能的开发难度,提高了代码的可复用性和可维护性。

# 1.2 解决的问题

  • 重复开发问题:传统方式每个 Excel 导出功能都需要编写类似的代码,导致大量重复工作。

  • 格式不统一:不同开发人员实现的导出功能,在 Excel 格式、样式、命名等方面缺乏统一标准。

  • 扩展性差:硬编码的导出功能难以适应需求变更,如增加导出字段、修改格式等。

  • 异常处理不完善:导出过程中的异常处理不统一,容易导致用户体验不佳。

  • 大数据量支持不足:缺乏对大数据量导出的优化处理,可能导致内存溢出或性能问题。

# 2. 如何实现(How)

# 2.1 项目结构

part008 模块的项目结构如下:

part008/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── muzi/
│   │   │           └── part8/
│   │   │               ├── aspect/                        # 切面拦截
│   │   │               │   └── ExcelExportAspect.java     # Excel导出切面
│   │   │               ├── controller/                    # 控制层
│   │   │               │   └── UserController.java        # 用户控制器
│   │   │               ├── dto/                           # 数据传输对象
│   │   │               │   ├── User.java                  # 用户对象
│   │   │               │   └── UserExportRequest.java     # 用户导出请求
│   │   │               ├── excel/                         # Excel导出核心包
│   │   │               │   ├── ExcelExportField.java      # 导出字段定义
│   │   │               │   ├── ExcelExportRequest.java    # 导出请求基类
│   │   │               │   ├── ExcelExportResponse.java   # 导出响应对象
│   │   │               │   ├── ExcelExportUtils.java      # 导出工具类
│   │   │               │   ├── ExcelHead.java             # Excel表头定义
│   │   │               │   └── ExcelSheet.java            # Excel工作表定义
│   │   │               ├── service/                       # 服务层
│   │   │               │   └── UserService.java           # 用户服务
│   │   │               └── utils/                         # 工具类
│   │   │                   └── CollUtils.java             # 集合工具类
│   │   └── resources/                              # 配置文件
│   └── test/                                       # 测试类
└── pom.xml                                         # Maven配置文件

# 2.2 关键技术点

# 2.2.1 案例分析:面向对象的 Excel 导出模型设计

技术实现: 本模块设计了一套完整的 Excel 导出领域模型,通过面向对象的设计实现灵活的导出功能:

  1. 核心领域模型
// Excel导出请求基类
public class ExcelExportRequest {
    private String excelName;     // Excel文件名
    private String sheetName;     // 工作表名称
    private List<ExcelExportField> fieldList; // 导出字段列表
    // getter/setter 略
}

// Excel导出字段定义
public class ExcelExportField {
    private String fieldName;     // 字段名称(对象的属性名)
    private String fieldDesc;     // 字段描述(表头显示名)
    // getter/setter 略
}

// Excel导出响应对象
public class ExcelExportResponse {
    private String excelName;     // Excel文件名
    private List<ExcelSheet> sheetList; // 工作表列表
    // getter/setter 略
}

// Excel工作表定义
public class ExcelSheet {
    private String sheetName;     // 工作表名称
    private List<ExcelHead> headList; // 表头列表
    private List<Map<String, String>> dataList; // 数据列表
    // getter/setter 略
}
  1. 业务导出请求实现
// 用户导出请求(继承通用导出请求)
public class UserExportRequest extends ExcelExportRequest {
    private List<Integer> userIdList; // 要导出的用户ID列表
    // getter/setter 略
}

原理分析

  1. 分层设计

    1. 将 Excel 导出功能拆分为多个层次的对象,每个对象负责特定职责

    2. 使用继承关系建立通用导出基类和业务导出请求之间的关系

    3. 通过组合关系构建 Excel 文档的结构 (工作簿、工作表、表头、数据)

  2. 灵活性与扩展性

    1. 业务模块只需继承通用基类,定义业务特定属性即可实现定制化导出

    2. 支持动态指定导出字段,实现按需导出

    3. 工作表定义支持多 Sheet 导出,满足复杂场景需求

  3. 映射关系

    1. 字段定义 (ExcelExportField) 建立了 Java 对象属性与 Excel 表头的映射关系

    2. 数据列表 (dataList) 使用 Map<String, String > 存储,键为字段名称,值为单元格内容

    3. 使用反射机制自动将 Java 对象属性值映射到 Excel 单元格

# 2.2.2 案例分析:导出工具类实现

技术实现: 本模块通过 ExcelExportUtils 工具类实现 Excel 导出的核心逻辑:

public class ExcelExportUtils {
    // 构建Excel导出响应对象
    public static ExcelExportResponse build(List<?> dataList, ExcelExportRequest request) {
        // 创建响应对象
        ExcelExportResponse result = new ExcelExportResponse();
        result.setExcelName(request.getExcelName());
        
        // 组装工作表列表
        List<ExcelSheet> sheetList = new ArrayList<>();
        result.setSheetList(sheetList);
        
        // 创建工作表
        ExcelSheet excelSheet = new ExcelSheet();
        excelSheet.setSheetName(request.getSheetName());
        // 设置表头
        excelSheet.setHeadList(buildSheetHeadList(request));
        // 设置数据
        excelSheet.setDataList(buildSheetDataList(dataList, request));
        sheetList.add(excelSheet);
        
        return result;
    }
    
    // 根据导出请求构建表头列表
    public static List<ExcelHead> buildSheetHeadList(ExcelExportRequest request) {
        List<ExcelExportField> fieldList = request.getFieldList();
        List<ExcelHead> excelHeadList = new ArrayList<>(fieldList.size());
        for (ExcelExportField field : fieldList) {
            ExcelHead head = new ExcelHead();
            head.setFieldName(field.getFieldName());
            head.setFieldDesc(field.getFieldDesc());
            excelHeadList.add(head);
        }
        return excelHeadList;
    }
    
    // 根据数据列表和请求构建数据列表
    public static List<Map<String, String>> buildSheetDataList(
            List<?> dataList, ExcelExportRequest request) {
        if (CollUtils.isEmpty(dataList)) {
            return CollUtils.emptyArrayList();
        }
        
        List<Map<String, String>> sheetDataList = new ArrayList<>(dataList.size());
        List<ExcelExportField> fieldList = request.getFieldList();
        List<String> exportFieldNameList = 
            CollUtils.convertList(fieldList, ExcelExportField::getFieldName);
        
        for (Object data : dataList) {
            Map<String, String> dataMap = new HashMap<>();
            for (String fieldName : exportFieldNameList) {
                // 通过反射获取字段值
                Object fieldValue = ReflectUtil.getFieldValue(data, fieldName);
                dataMap.put(fieldName, convertToString(fieldValue));
            }
            sheetDataList.add(dataMap);
        }
        return sheetDataList;
    }
    
    // 将Excel写入HTTP响应
    public static void writeExcelToResponse(ExcelExportResponse excelExportResult) 
            throws IOException {
        HttpServletResponse response = 
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
            .getResponse();
        OutputStream outputStream = response.getOutputStream();
        
        // 设置响应头
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setHeader("Content-Disposition", "attachment;filename=" + 
            URLEncodeUtil.encode(excelExportResult.getExcelName() + ".xlsx"));
        
        // 写入Excel
        write(excelExportResult, outputStream);
    }
    
    // 将Excel写入输出流
    public static void write(ExcelExportResponse result, OutputStream outputStream) {
        List<ExcelSheet> sheetList = result.getSheetList();
        try (ExcelWriter writer = EasyExcel.write(outputStream).build()) {
            for (int sheetNo = 0; sheetNo < sheetList.size(); sheetNo++) {
                ExcelSheet excelSheet = sheetList.get(sheetNo);
                // 构建EasyExcel需要的头和数据格式
                List<List<String>> head = buildEasyExcelHead(excelSheet);
                List<List<String>> dataList = buildEasyExcelDataList(excelSheet);
                
                // 创建工作表并写入数据
                WriteSheet writeSheet = EasyExcel
                    .writerSheet(sheetNo, excelSheet.getSheetName())
                    .head(head).build();
                writer.write(dataList, writeSheet);
            }
        }
    }
}

原理分析

  1. 数据转换过程

    1. 将业务对象列表转换为 Excel 导出的标准模型

    2. 通过反射机制动态获取对象属性值,实现灵活映射

    3. 将 Java 对象的复杂结构转换为 EasyExcel 需要的二维表格结构

  2. 分步骤组装

    1. 先构建 Excel 导出响应对象 (ExcelExportResponse)

    2. 再构建工作表 (ExcelSheet),包括表头和数据

    3. 最后将数据转换为 EasyExcel 需要的格式并写入

  3. 输出处理

    1. 支持将 Excel 写入 HTTP 响应,实现浏览器下载

    2. 支持将 Excel 写入指定输出流,适应不同场景需求

    3. 设置适当的响应头,确保浏览器正确处理下载文件

# 2.2.3 案例分析:AOP 实现自动导出

技术实现: 本模块使用 Spring AOP 实现了自动 Excel 导出功能:

@Component
@Aspect
public class ExcelExportAspect {
    @Around(value = "execution(* com.muzi.*Controller.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 执行原方法
        Object result = proceedingJoinPoint.proceed();
        
        // 判断返回值类型,如果是Excel导出响应,则自动处理导出
        if (result instanceof ExcelExportResponse) {
            // 下载Excel
            ExcelExportUtils.writeExcelToResponse((ExcelExportResponse) result);
            return null;
        } else {
            // 其他类型返回值正常返回
            return result;
        }
    }
}

原理分析

  1. AOP 拦截

    1. 使用切面拦截所有 Controller 方法的返回值

    2. 对于 Excel 导出响应类型,自动处理导出逻辑

    3. 对于其他类型返回值,不影响正常处理流程

  2. 简化导出流程

    1. 业务代码只需关注数据准备和导出配置

    2. 无需编写导出响应处理代码,统一由切面处理

    3. 避免重复编写相似的导出代码

  3. 关注点分离

    1. 导出配置和数据准备由业务层负责

    2. 导出实现细节由框架层负责

    3. 业务代码和导出框架代码解耦

# 2.2.4 案例分析:用户导出实现

技术实现: 本模块实现了一个用户列表导出的示例:

// 控制器方法
@PostMapping("/userExport")
@ResponseBody
public ExcelExportResponse userExport(@RequestBody UserExportRequest userExportRequest) throws IOException {
    LOGGER.info("userExportRequest:{}", JSONUtil.toJsonPrettyStr(userExportRequest));
    return this.userService.userExport(userExportRequest);
}

// 服务实现
public ExcelExportResponse userExport(UserExportRequest request) {
    List<Integer> userIdList = request.getUserIdList();
    // 获取用户列表
    List<User> userList;
    if (CollectionUtil.isEmpty(userIdList)) {
        userList = this.getUserList();
    } else {
        userList = this.getUserList(request.getUserIdList());
    }
    // 调用工具类构建导出响应
    return ExcelExportUtils.build(userList, request);
}

原理分析

  1. 业务流程

    1. 控制器接收客户端传来的导出请求 (包含文件名、字段配置等)

    2. 服务层根据请求参数查询需要导出的数据

    3. 调用导出工具构建导出响应对象,由 AOP 切面自动处理导出

  2. 动态字段导出

    1. 客户端可以指定需要导出的字段列表,实现按需导出

    2. 支持自定义表头名称,提高用户体验

    3. 支持按条件筛选导出数据 (示例中支持按用户 ID 列表导出)

  3. 简洁实现

    1. 完整的用户导出功能仅需几行代码

    2. 导出逻辑与业务逻辑分离,易于维护

    3. 通过继承复用通用导出框架,减少重复代码

# 3. 技术点详解(Detail)

# 3.1 EasyExcel 原理与优势

本模块基于阿里巴巴开源的 EasyExcel 库实现 Excel 导出功能:

  1. EasyExcel 特点

    1. 基于 POI 实现,但大幅降低内存占用

    2. 采用 SAX 模式逐行读取,避免将整个 Excel 加载到内存

    3. 使用注解方式定义映射关系,简化开发

  2. 性能优势

    1. 内存占用低,适合大数据量导出

    2. 支持流式处理,避免 OOM 问题

    3. 写入性能高,支持批量写入优化

  3. 扩展能力

    1. 支持自定义样式、合并单元格等高级功能

    2. 支持 Excel 模板填充,适合复杂报表生成

    3. 提供丰富的事件监听接口,可以实现各种定制需求

# 3.2 面向对象设计与设计模式应用

本模块的设计应用了多种设计模式:

  1. 模板方法模式

    1. ExcelExportUtils 类中的 build 方法定义了 Excel 构建的骨架算法

    2. 将构建表头、构建数据等步骤抽象为独立方法,便于扩展和复用

  2. 装饰器模式

    1. 使用 AOP 切面装饰控制器方法,增强其导出功能

    2. 不改变原有业务逻辑的情况下,添加 Excel 导出响应处理能力

  3. 策略模式

    1. ExcelExportRequest 可视为导出策略的抽象

    2. 具体业务请求类 (如 UserExportRequest) 提供具体的导出策略实现

    3. 客户端可以灵活配置导出字段,实现不同的导出策略

  4. 建造者模式

    1. ExcelExportUtils 的 build 方法实现了建造者模式

    2. 分步骤构建复杂的 Excel 导出响应对象

    3. 将构建过程与表示分离,使同样的构建过程可以创建不同的表示

# 3.3 反射与动态数据处理

本模块使用反射机制实现动态数据处理:

  1. 动态字段获取

    1. 使用 Hutool 工具库的 ReflectUtil 获取对象属性值

    2. 支持任意类型对象的属性读取,无需编写特定的 getter 调用

  2. 反射优化考量

    1. 反射操作性能较低,但在导出场景下影响相对有限

    2. 可考虑使用缓存或 Map 预处理减少反射调用次数

    3. 在大数据量场景下,应权衡反射便利性与性能影响

  3. 类型转换处理

    1. 将各种类型的属性值统一转换为字符串,便于 Excel 处理

    2. 处理 null 值情况,避免 NullPointerException

    3. 可扩展支持自定义类型转换逻辑,如日期格式化等

# 3.4 HTTP 响应与文件下载

本模块实现了将 Excel 文件通过 HTTP 响应下载的功能:

  1. 响应头设置

    1. 设置 Content-Type 为 "application/vnd.ms-excel",指示浏览器处理 Excel 文件

    2. 设置 Content-Disposition 头,指定文件名和下载行为

    3. 设置字符编码和跨域头,确保跨域场景下的正确处理

  2. 文件名编码

    1. 使用 URLEncodeUtil 对文件名进行 URL 编码,处理中文文件名问题

    2. 避免不同浏览器下文件名乱码的问题

  3. 流处理

    1. 直接将 Excel 写入响应输出流,避免临时文件

    2. 使用 try-with-resources 确保流正确关闭,防止资源泄漏

    3. 异常处理确保在出错情况下仍能给客户端正确的响应

# 4. 使用示例(Usage)

# 4.1 基本使用

// 控制器方法
@PostMapping("/userExport")
@ResponseBody
public ExcelExportResponse userExport(@RequestBody UserExportRequest request) {
    // 设置导出配置
    request.setExcelName("用户列表");
    request.setSheetName("用户信息");
    
    // 设置导出字段
    List<ExcelExportField> fieldList = new ArrayList<>();
    fieldList.add(new ExcelExportField("userId", "用户ID"));
    fieldList.add(new ExcelExportField("userName", "用户名"));
    fieldList.add(new ExcelExportField("age", "年龄"));
    fieldList.add(new ExcelExportField("address", "地址"));
    request.setFieldList(fieldList);
    
    // 获取数据并构建导出响应
    List<User> userList = userService.getUserList(request.getUserIdList());
    return ExcelExportUtils.build(userList, request);
}

# 4.2 客户端调用示例

// 前端发起导出请求
async function exportUsers() {
  const response = await fetch('/userExport', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      excelName: '用户列表',
      sheetName: '用户信息',
      fieldList: [
        { fieldName: 'userId', fieldDesc: '用户ID' },
        { fieldName: 'userName', fieldDesc: '用户名' },
        { fieldName: 'age', fieldDesc: '年龄' },
        { fieldName: 'address', fieldDesc: '地址' }
      ],
      userIdList: [1, 2, 3] // 只导出ID为1,2,3的用户
    })
  });
  
  // 处理文件下载
  const blob = await response.blob();
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = '用户列表.xlsx';
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
}

# 4.3 多 Sheet 导出示例

public ExcelExportResponse exportMultiSheet(MultiSheetRequest request) {
    // 创建响应对象
    ExcelExportResponse response = new ExcelExportResponse();
    response.setExcelName(request.getExcelName());
    
    // 创建工作表列表
    List<ExcelSheet> sheetList = new ArrayList<>();
    response.setSheetList(sheetList);
    
    // 添加第一个工作表(用户信息)
    ExcelSheet userSheet = new ExcelSheet();
    userSheet.setSheetName("用户信息");
    userSheet.setHeadList(buildUserHeadList());
    userSheet.setDataList(buildUserDataList(userService.getUserList()));
    sheetList.add(userSheet);
    
    // 添加第二个工作表(订单信息)
    ExcelSheet orderSheet = new ExcelSheet();
    orderSheet.setSheetName("订单信息");
    orderSheet.setHeadList(buildOrderHeadList());
    orderSheet.setDataList(buildOrderDataList(orderService.getOrderList()));
    sheetList.add(orderSheet);
    
    return response;
}

# 4.4 自定义样式示例

// 扩展ExcelExportUtils,添加样式支持
public static void writeWithStyle(ExcelExportResponse result, OutputStream outputStream) {
    List<ExcelSheet> sheetList = result.getSheetList();
    // 创建样式
    WriteCellStyle headWriteCellStyle = new WriteCellStyle();
    headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
    WriteFont headWriteFont = new WriteFont();
    headWriteFont.setFontHeightInPoints((short) 12);
    headWriteFont.setBold(true);
    headWriteCellStyle.setWriteFont(headWriteFont);
    
    // 设置内容样式
    WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
    WriteFont contentWriteFont = new WriteFont();
    contentWriteFont.setFontHeightInPoints((short) 11);
    contentWriteCellStyle.setWriteFont(contentWriteFont);
    
    // 应用样式
    try (ExcelWriter writer = EasyExcel.write(outputStream)
            .registerWriteHandler(new HorizontalCellStyleStrategy(
                    headWriteCellStyle, contentWriteCellStyle))
            .build()) {
        // 写入数据,同原方法
        for (int sheetNo = 0; sheetNo < sheetList.size(); sheetNo++) {
            // …省略数据写入代码
        }
    }
}

# 5. 总结与优化方向(Summary)

# 5.1 技术总结

本模块实现了一个灵活、可扩展的 Excel 导出框架:

  1. 设计了完整的 Excel 导出领域模型,实现了面向对象的导出功能

  2. 使用 EasyExcel 库作为底层实现,保证了大数据量下的导出性能

  3. 通过 AOP 切面实现了自动导出处理,简化了业务代码

  4. 支持动态字段配置、多 Sheet 导出等高级功能

# 5.2 优化方向

  1. 异步导出支持

    1. 对于大数据量导出,可以实现异步导出机制

    2. 先返回任务 ID,后台异步生成 Excel 文件

    3. 提供接口查询导出进度,完成后提供下载链接

  2. 导出模板支持

    1. 增加模板导出功能,支持复杂的预定义格式

    2. 通过模板文件定义样式、合并单元格等复杂布局

    3. 实现只需填充数据的模板导出功能

  3. 分批次导出优化

    1. 针对超大数据量导出,实现分批次查询数据

    2. 避免一次性加载全部数据导致内存压力

    3. 使用 EasyExcel 的分批写入功能,优化性能

  4. 导出权限控制

    1. 集成权限系统,控制用户可导出的字段和数据范围

    2. 根据用户角色动态生成导出字段列表

    3. 确保敏感数据安全,避免数据泄露风险

  5. 导出监控与统计

    1. 记录导出操作日志,包括导出人、导出内容、耗时等

    2. 实现导出性能监控,识别慢导出操作

    3. 提供导出次数、数据量等统计功能,优化系统资源分配