Spring Boot 实现动态配置导出,同时支持公式和动态下拉框渲染和性能优化案例示范

时间:2024-09-29 20:17:56

在业务系统中,数据导出是一个非常常见且重要的功能,本文将详细介绍如何在 Spring Boot 中实现这一功能,并结合 MySQL 数据库、MyBatis 作为数据访问层,EasyExcel 作为导出工具,展示如何在电商交易系统中搭建灵活、可扩展且高性能的导出系统。


1. 需求分析

在电商交易系统中,导出的数据类型可能包括订单数据、商品数据、用户数据等。用户希望通过界面配置导出的字段,按需生成不同格式的 Excel 文件。这一需求可以归纳为:

  • 动态参数配置:用户可以通过前端选择导出字段和导出条件。
  • 动态列导出:根据用户的选择,导出时生成不同列的 Excel 文件。
  • 高效性能:针对大规模数据导出,系统需要优化性能,防止内存溢出和执行超时。
  • 可扩展性:支持未来新模块、新业务数据的导出需求。

技术选型

  1. Spring Boot:轻量级 Java 框架,简化了 Spring 的配置和开发工作。
  2. MySQL:用于存储电商系统的交易数据。
  3. MyBatis:简化数据库访问,提供灵活的 SQL 语句管理。
  4. EasyExcel:高效的 Excel 导出工具,提供良好的性能和丰富的功能,适用于大数据量导出。
  5. Lombok:减少样板代码(Boilerplate code),简化实体类和 DTO(Data Transfer Object)的开发。

2. 项目结构

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── ecommerce/
│   │           ├── config/               # 配置类
│   │           ├── controller/           # 控制器层
│   │           ├── service/              # 服务层
│   │           ├── mapper/               # MyBatis 映射层
│   │           ├── model/                # 数据实体类
│   │           ├── dto/                  # 数据传输对象
│   │           └── utils/                # 工具类
│   └── resources/
│       ├── application.yml               # Spring Boot 配置文件
│       └── mapper/                       # MyBatis 映射文件
└── pom.xml                               # Maven 依赖配置

3. Maven 依赖

为了实现以上功能,我们需要引入以下 Maven 依赖:

<dependencies>
    <!-- Spring Boot Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MyBatis Starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>

    <!-- MySQL Connector -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- EasyExcel -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.0.5</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- Spring Boot DevTools (optional) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

4. 数据库表设计

在电商交易系统中,我们假设有一张订单表 orders,结构如下:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_number VARCHAR(50) NOT NULL,
    customer_name VARCHAR(100),
    total_amount DECIMAL(10, 2),
    order_status VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

此表记录订单的基础信息,实际场景中可能包含更多字段。在本文示例中,我们假设用户可以选择导出 order_numbercustomer_nametotal_amountorder_status 等列。


5. 实现动态参数配置

通过前端界面,用户可以选择希望导出的字段和条件。后端通过接受这些参数,生成对应的 SQL 查询,并将结果导出。

DTO 定义
@Data
public class ExportRequestDTO {
    private List<String> columns; // 用户选择的导出列
    private String orderStatus;   // 订单状态筛选条件
    private String startDate;     // 开始日期
    private String endDate;       // 结束日期
}
控制器层
@RestController
@RequestMapping("/api/export")
public class ExportController {

    @Autowired
    private ExportService exportService;

    @PostMapping("/orders")
    public void exportOrders(@RequestBody ExportRequestDTO exportRequest, HttpServletResponse response) throws IOException {
        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-Disposition", "attachment;filename=orders.xlsx");

        exportService.exportOrders(exportRequest, response.getOutputStream());
    }
}

6. 数据导出服务实现

ExportService 中,我们根据用户选择的字段和条件,构造 SQL 查询并通过 EasyExcel 导出结果。

服务层
@Service
public class ExportService {

    @Autowired
    private OrderMapper orderMapper;

    public void exportOrders(ExportRequestDTO exportRequest, OutputStream outputStream) {
        // 根据用户选择的列动态生成 SQL 查询
        List<OrderDTO> orders = orderMapper.selectOrders(exportRequest);

        // 使用 EasyExcel 导出
        ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class).build();
        WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();
        excelWriter.write(orders, writeSheet);
        excelWriter.finish();
    }
}
MyBatis Mapper
@Mapper
public interface OrderMapper {
    List<OrderDTO> selectOrders(@Param("exportRequest") ExportRequestDTO exportRequest);
}
动态 SQL 生成(MyBatis XML)
<select id="selectOrders" resultType="com.ecommerce.dto.OrderDTO">
    SELECT
        <foreach collection="exportRequest.columns" item="column" separator=",">
            ${column}
        </foreach>
    FROM orders
    WHERE 1 = 1
    <if test="exportRequest.orderStatus != null">
        AND order_status = #{exportRequest.orderStatus}
    </if>
    <if test="exportRequest.startDate != null">
        AND created_at &gt;= #{exportRequest.startDate}
    </if>
    <if test="exportRequest.endDate != null">
        AND created_at &lt;= #{exportRequest.endDate}
    </if>
</select>

7. 使用 EasyExcel 实现动态列导出

EasyExcel 允许根据动态字段生成对应的 Excel 文件。在本例中,OrderDTO 是我们导出数据的载体。用户选择的列会自动映射到 DTO 中,生成 Excel 文件。

OrderDTO 类
@Data
public class OrderDTO {
    private String orderNumber;
    private String customerName;
    private BigDecimal totalAmount;
    private String orderStatus;
    private Timestamp createdAt;
}

8. 性能优化

8.1 分页查询性能优化:轮询分页代替传统分页
8.1.1 传统分页查询的性能问题

通常在大数据导出时,我们会采用分页查询的方式,如每次查询1000条记录并逐页导出,避免一次性加载过多数据。然而,当处理数百万条记录时,传统分页方式会越来越慢,尤其在 OFFSET 较大的情况下,查询效率会显著下降,因为数据库在分页时仍需扫描前面的数据并跳过。

public void exportOrdersPaged(ExportRequestDTO exportRequest, OutputStream outputStream) {
    ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class).build();
    WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();

    int pageNum = 1;
    int pageSize = 1000; // 每次查询 1000 条
    List<OrderDTO> orders;

    do {
        orders = orderMapper.selectOrdersPaged(exportRequest, pageNum, pageSize);
        excelWriter.write(orders, writeSheet);
        pageNum++;
    } while (orders.size() == pageSize);

    excelWriter.finish();
}

这种方式虽然表面上避免了内存占用,但随着 pageNum 的增大,分页查询的 OFFSET 也会增大,导致查询效率下降。

8.1.2 轮询分页:通过主键过滤提升效率

为了避免大偏移量的分页查询,我们可以采用 轮询分页(Keyset Pagination)。轮询分页的核心思想是通过上一次查询结果的主键值作为下一次查询的起点,从而避免 OFFSET 的性能问题。

public void exportOrdersKeyset(ExportRequestDTO exportRequest, OutputStream outputStream) {
    ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class).build();
    WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();

    Long lastId = null; // 用于记录上次分页的最后一个订单ID
    int pageSize = 1000;
    List<OrderDTO> orders;

    do {
        orders = orderMapper.selectOrdersKeyset(exportRequest, lastId, pageSize);
        if (!orders.isEmpty()) {
            lastId = orders.get(orders.size() - 1).getId(); // 记录最后一个ID
            excelWriter.write(orders, writeSheet);
        }
    } while (orders.size() == pageSize);

    excelWriter.finish();
}
8.1.3 轮询分页的 SQL 实现
SELECT * FROM orders 
WHERE id > #{lastId} 
ORDER BY id ASC 
LIMIT #{pageSize}

通过上述 SQL,每次查询从上一次获取的最大 id 之后开始,避免了 OFFSET 的计算。这样,即使数据量再大,查询性能也能保持稳定。


8.2 流式处理:减少内存占用
8.2.1 问题分析

即使使用分页查询,在大数据导出时依然可能会遇到内存压力问题,尤其是在导出上百万条记录的场景下。如果我们将所有查询结果加载到内存中,再一次性写入 Excel,会导致 JVM 堆内存消耗过大,可能引发 OutOfMemoryError

8.2.2 流式处理优化

为了解决内存占用问题,可以使用 EasyExcel 提供的流式写入功能。在流式处理中,数据可以逐条查询并写入 Excel,避免将所有数据加载到内存中。以下是基于 EasyExcel 的流式处理示例:

public void exportOrdersStream(ExportRequestDTO exportRequest, OutputStream outputStream) {
    ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class).build();
    WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();

    Long lastId = null;
    int pageSize = 1000;
    List<OrderDTO> orders;

    do {
        orders = orderMapper.selectOrdersKeyset(exportRequest, lastId, pageSize);
        if (!orders.isEmpty()) {
            lastId = orders.get(orders.size() - 1).getId();
            // 使用流式写入
            excelWriter.write(orders, writeSheet);
        }
    } while (orders.size() == pageSize);

    excelWriter.finish();
}

流式处理的核心是每次查询的数据在处理后立即释放,不会全部加载到内存,从而极大降低了内存的使用压力。


8.3 多线程并行导出:提高导出速度
8.3.1 问题分析

在单线程模式下,尽管我们可以优化查询效率并减少内存占用,但对于超大数据集的导出,单线程处理仍然可能耗费大量时间。为了进一步提升导出性能,可以采用 多线程并行导出 的方式,将数据按块分批处理。

8.3.2 多线程导出实现

通过线程池并行处理多个分页数据,可以显著提高导出效率。每个线程负责查询并处理一部分数据,最终汇总成一个完整的 Excel 文件。

public void exportOrdersParallel(ExportRequestDTO exportRequest, OutputStream outputStream) {
    ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class).build();
    WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();

    int pageSize = 1000;
    Long lastId = null;

    // 创建线程池
    ExecutorService executor = Executors.newFixedThreadPool(10); 
    List<Future<List<OrderDTO>>> futures = new ArrayList<>();

    // 分块查询并行处理
    for (int i = 0; i < 10; i++) {
        Long finalLastId = lastId;
        Future<List<OrderDTO>> future = executor.submit(() -> {
            return orderMapper.selectOrdersKeyset(exportRequest, finalLastId, pageSize);
        });
        futures.add(future);
    }

    // 汇总每个线程的结果
    for (Future<List<OrderDTO>> future : futures) {
        try {
            List<OrderDTO> orders = future.get();
            excelWriter.write(orders, writeSheet);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    // 关闭线程池
    executor.shutdown();

    excelWriter.finish();
}
8.3.3 注意事项
  • 线程安全性:在并行写入 Excel 时,EasyExcelwrite 方法是线程安全的,因此可以直接使用。
  • 合理设置线程数:线程数不宜过多,通常根据服务器的 CPU 核心数进行设置,以避免过多线程反而导致性能瓶颈。
  • 分页大小控制:每个线程负责的分页大小应适当调整,避免单个分页数据量过大或过小影响性能。

9. 扩展性设计

在复杂的电商系统中,可能涉及多个业务模块,如订单、商品、用户等。为了让这些模块都能灵活实现数据导出功能,并且保证代码的高复用性和可维护性,我们可以将导出逻辑抽象为通用接口。这样每个模块只需要实现这个接口即可完成相应的数据导出功能。

通过这样的设计,可以大幅减少代码冗余,提升系统的可扩展性。我们将从接口的定义开始,逐步实现针对不同业务模块的导出逻辑。


9.1 通用导出接口定义

首先定义一个通用的导出接口 ExportService。这个接口规定了所有导出服务的基本结构,每个模块都需要实现数据查询逻辑以及将数据导出为 Excel 的具体实现。

public interface ExportService<T> {

    /**
     * 导出数据
     *
     * @param exportRequest 导出的参数配置
     * @param outputStream  输出流,用于生成 Excel 文件
     */
    void exportData(ExportRequestDTO exportRequest, OutputStream outputStream);

    /**
     * 根据用户请求的参数动态查询数据
     *
     * @param exportRequest 导出请求参数
     * @return 查询到的业务数据列表
     */
    List<T> queryData(ExportRequestDTO exportRequest);
}
  • exportData:定义了数据导出的方法,接收导出请求和输出流,所有具体导出逻辑都在这个方法中实现。
  • queryData:用于动态查询数据,每个业务模块会根据自身的需求来实现。
9.2 通用导出逻辑实现

我们可以实现一个通用的抽象类 AbstractExportService 来减少重复代码。该抽象类实现了通用的 exportData 方法,并将数据查询交给具体业务模块去实现。

public abstract class AbstractExportService<T> implements ExportService<T> {

    @Override
    public void exportData(ExportRequestDTO exportRequest, OutputStream outputStream) {
        // 查询数据
        List<T> data = queryData(exportRequest);

        // 使用 EasyExcel 导出数据
        ExcelWriter excelWriter = EasyExcel.write(outputStream, getModelClass()).build();
        WriteSheet writeSheet = EasyExcel.writerSheet("导出数据").build();
        excelWriter.write(data, writeSheet);
        excelWriter.finish();
    }

    /**
     * 获取导出实体类的类型
     *
     * @return 实体类的类型
     */
    protected abstract Class<T> getModelClass();
}
  • AbstractExportService 实现了通用的数据导出逻辑,通过 queryData 查询数据,并通过 EasyExcel 将数据导出为 Excel 文件。
  • getModelClass 方法让每个子类提供其具体的业务数据类型,例如订单、商品或用户。

9.3 针对订单模块的导出实现

在电商系统中,订单模块是非常常见的业务场景之一。我们可以通过实现 OrderExportService 来完成订单数据的导出。

@Service
public class OrderExportService extends AbstractExportService<OrderDTO> {

    @Autowired
    private OrderMapper orderMapper;

    @Override
    public List<OrderDTO> queryData(ExportRequestDTO exportRequest) {
        return orderMapper.selectOrders(exportRequest);
    }

    @Override
    protected Class<OrderDTO> getModelClass() {
        return OrderDTO.class;
    }
}
  • OrderExportService 继承了 AbstractExportService,并实现了 queryData 方法,调用 OrderMapper 来动态查询订单数据。
  • getModelClass 返回订单的 DTO 类型 OrderDTO,以便 EasyExcel 知道如何映射列和数据。

9.4 针对商品模块的导出实现

同样,我们可以实现商品模块的导出逻辑。假设商品的基本信息存储在 products 表中,我们实现 ProductExportService

@Service
public class ProductExportService extends AbstractExportService<ProductDTO> {

    @Autowired
    private ProductMapper productMapper;

    @Override
    public List<ProductDTO> queryData(ExportRequestDTO exportRequest) {
        return productMapper.selectProducts(exportRequest);
    }

    @Override
    protected Class<ProductDTO> getModelClass() {
        return ProductDTO.class;
    }
}
  • ProductExportService 负责商品数据的查询和导出,通过 ProductMapper 进行数据库查询。
  • 同样地,getModelClass 返回商品的 DTO 类型 ProductDTO

9.5 统一的控制器设计

通过对不同模块导出逻辑的封装,我们可以设计一个统一的控制器,根据请求动态调用不同的导出服务。

@RestController
@RequestMapping("/api/export")
public class ExportController {

    @Autowired
    private Map<String, ExportService<?>> exportServices;

    @PostMapping("/{type}")
    public void exportData(
            @PathVariable String type, 
            @RequestBody ExportRequestDTO exportRequest, 
            HttpServletResponse response) throws IOException {

        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-Disposition", "attachment;filename=" + type + ".xlsx");

        ExportService<?> exportService = exportServices.get(type);
        if (exportService == null) {
            throw new IllegalArgumentException("Unsupported export type: " + type);
        }

        exportService.exportData(exportRequest, response.getOutputStream());
    }
}
  • exportServices 是一个包含所有导出服务的 Map,我们可以根据 URL 中的 type 参数动态选择对应的导出服务。
  • @PostMapping("/{type}") 中,type 是用户请求的导出类型(例如 ordersproductsusers),通过 exportServices.get(type) 获取对应的导出服务。

9.6 服务注册

为了实现不同模块导出的动态调用,我们可以在 Spring Boot 启动时通过 @Bean 自动注册所有导出服务。

@Configuration
public class ExportServiceConfig {

    @Bean
    public Map<String, ExportService<?>> exportServices(
            OrderExportService orderExportService, 
            ProductExportService productExportService) {
        Map<String, ExportService<?>> map = new HashMap<>();
        map.put("orders", orderExportService);
        map.put("products", productExportService);
        return map;
    }
}
  • ExportServiceConfig 中,将所有导出服务注册到 Map> 中,供控制器调用。
  • 这样,订单导出请求可以通过 type = orders 调用 OrderExportService,商品导出请求可以通过 type = products 调用 ProductExportService

9.7 扩展性优势

通过以上设计,我们将导出功能抽象为接口,并使用一个统一的控制器处理不同模块的数据导出需求。以下是该设计的几个扩展性优势:

  1. 模块解耦:每个模块的导出逻辑都是独立的,互不干扰。新增一个业务模块只需实现 ExportService 接口,并在配置类中注册即可。
  2. 代码复用:通过 AbstractExportService 实现通用的导出逻辑,减少了重复代码,提高了代码的可维护性。
  3. 灵活扩展:未来如果增加新业务模块,比如用户数据导出,只需新增 UserExportService 并实现对应的 queryData 方法,无需改动控制器或其他业务代码。
  4. 动态选择导出类型:控制器层的设计允许通过 URL 动态选择导出类型,方便扩展和管理不同类型的数据导出。

10. Excel 导出实现公式和样式

在实际的业务场景中,Excel 不仅仅用于展示数据,还需要具备一定的格式化和交互功能。通过 EasyExcel,我们可以为导出的 Excel 文件设置多种样式和功能,比如设置字体颜色、插入批注、添加动态下拉框等。这些功能能够提升导出文件的专业性和可读性,同时帮助用户更直观地分析数据。

接下来,我们将详细讲解如何使用 EasyExcel 实现以下几种功能:

  1. 设置列的字体颜色,例如红色字体
  2. 为单元格添加批注
  3. 为列添加动态下拉框

10.1 设置某列字体颜色

为了实现特定列的字体颜色更改,我们可以自定义 WriteHandler 来控制单元格的样式。EasyExcel 提供了 CellWriteHandler 接口来实现这个功能。

我们可以通过实现这个接口来动态控制某列的字体颜色,例如订单金额小于0时,显示为红色字体。

public class CustomCellWriteHandler implements CellWriteHandler {

    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        // 获取当前行和列索引
        Integer rowIndex = context.getRowIndex();
        Integer columnIndex = context.getColumnIndex();

        // 判断列号,比如订单金额在第5列
        if (columnIndex == 4) {
            Cell cell = context.getCell();
            Double cellValue = Double.valueOf(cell.getStringCellValue());

            // 如果金额小于 0,设置字体颜色为红色
            if (cellValue < 0) {
                Workbook workbook = context.getWriteSheetHolder().getSheet().getWorkbook();
                Font font = workbook.createFont();
                font.setColor(Font.COLOR_RED);

                CellStyle cellStyle = workbook.createCellStyle();
                cellStyle.setFont(font);
                cell.setCellStyle(cellStyle);
            }
        }
    }
}

在数据导出时,我们只需将这个 CustomCellWriteHandler 添加到 EasyExcelExcelWriter 中即可。

ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class)
    .registerWriteHandler(new CustomCellWriteHandler())
    .build();
WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();
excelWriter.write(orderData, writeSheet);
excelWriter.finish();
  • 逻辑说明:代码通过 CellWriteHandler 实现自定义样式设置。对于某列的数据,如果符合特定条件(例如小于0),将该列的字体颜色设置为红色。

10.2 为单元格添加批注

有时,我们需要在导出的 Excel 文件中为某些数据添加批注(Comment),以便解释某些数据或提供额外的注释信息。可以使用 WriteHandler 实现为指定单元格或指定列添加批注。

public class CommentWriteHandler implements CellWriteHandler {

    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        Integer rowIndex = context.getRowIndex();
        Integer columnIndex = context.getColumnIndex();

        // 判断需要在哪一列添加批注,比如在第3列
        if (columnIndex == 2) {
            Workbook workbook = context.getWriteSheetHolder().getSheet().getWorkbook();
            Sheet sheet = context.getWriteSheetHolder().getSheet();

            CreationHelper factory = workbook.getCreationHelper();
            ClientAnchor anchor = factory.createClientAnchor();
            Drawing<?> drawing = sheet.createDrawingPatriarch();

            // 定位批注的位置(列和行)
            anchor.setCol1(columnIndex);
            anchor.setCol2(columnIndex + 1);
            anchor.setRow1(rowIndex);
            anchor.setRow2(rowIndex + 2);

            // 创建批注并设置内容
            Comment comment = drawing.createCellComment(anchor);
            comment.setString(factory.createRichTextString("此数据为系统自动生成"));
            Cell cell = context.getCell();
            cell.setCellComment(comment);
        }
    }
}

在 Excel 导出时,类似设置字体颜色,我们需要将 CommentWriteHandler 注册到 EasyExcel 中。

ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class)
    .registerWriteHandler(new CommentWriteHandler())
    .build();
WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();
excelWriter.write(orderData, writeSheet);
excelWriter.finish();
  • 逻辑说明:通过在某列上创建批注,提供额外说明或注释。例如,可以在订单编号列中添加批注,说明订单来源或某个数据字段的特别含义。

10.3 为列添加动态下拉框

动态下拉框可以帮助用户在 Excel 文件中快速选择预定义的值。我们可以在 Excel 导出时为特定的列添加下拉框,限制用户输入的内容,确保数据的有效性和一致性。

public class DropdownWriteHandler implements SheetWriteHandler {

    @Override
    public void beforeSheetCreate(SheetWriteHandlerContext context) {
    }

    @Override
    public void afterSheetCreate(SheetWriteHandlerContext context) {
        Sheet sheet = context.getSheet();

        // 假设需要为第2列(商品类别)添加下拉框
        String[] options = {"电子产品", "生活用品", "服装", "书籍"};

        // 定义范围和下拉框内容
        DataValidationHelper helper = sheet.getDataValidationHelper();
        CellRangeAddressList addressList = new CellRangeAddressList(1, 100, 1, 1); // 行范围
        DataValidationConstraint constraint = helper.createExplicitListConstraint(options);
        DataValidation validation = helper.createValidation(constraint, addressList);

        // 设置下拉框约束
        sheet.addValidationData(validation);
    }
}

在导出时将 DropdownWriteHandler 注册到 EasyExcel 中,动态为某列添加下拉框。

ExcelWriter excelWriter = EasyExcel.write(outputStream, ProductDTO.class)
    .registerWriteHandler(new DropdownWriteHandler())
    .build();
WriteSheet writeSheet = EasyExcel.writerSheet("商品数据").build();
excelWriter.write(productData, writeSheet);
excelWriter.finish();
  • 逻辑说明:通过 DropdownWriteHandler,我们可以为指定的列(如商品类别)添加下拉框。这样用户在填写或修改该列时,只能选择预设的选项,确保数据输入的正确性。

11. 实现动态公式

在某些业务场景中,数据导出不仅需要展示原始数据,还需要在 Excel 文件中实现动态计算。比如在订单系统中,我们可以在导出时自动为某列设置公式,计算订单的总价。

public class FormulaWriteHandler implements CellWriteHandler {

    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        Integer rowIndex = context.getRowIndex();
        Integer columnIndex = context.getColumnIndex();

        // 假设需要为第6列(总价)设置公式:总价 = 数量 * 单价
        if (columnIndex == 5) {
            Cell cell = context.getCell();
            String formula = String.format("C%d*D%d", rowIndex + 1, rowIndex + 1); // C列是数量,D列是单价
            cell.setCellFormula(formula);
        }
    }
}

在导出时,使用 FormulaWriteHandler 注册到 EasyExcel 中,实现公式的动态设置。

ExcelWriter excelWriter = EasyExcel.write(outputStream, OrderDTO.class)
    .registerWriteHandler(new FormulaWriteHandler())
    .build();
WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();
excelWriter.write(orderData, writeSheet);
excelWriter.finish();
  • 逻辑说明:通过公式,可以在 Excel 导出时动态计算出订单总价。在 Excel 文件中,用户打开后,系统会自动进行计算并显示结果。