【项目亮点】基于EasyExcel + 线程池解决POI文件导出时的内存溢出及超时问题

目录

一、背景

二、技术选型

三、具体实现


一、背景

在一个后台管理功能中,需要导出 Excel,但是当处理大数据量的 Excel 文件导出时,常用的 Apache POI 库可能因其内存占用较高而导致内存溢出问题。同时,数据处理过程可能非常耗时,导致用户等待时间过长或请求超时。为解决这些问题,采用了基于 EasyExcel 和线程池的解决方案。

二、技术选型

Excel 的导出有很多种方案,包括了 POI、EasyExcel 还有 Hutool 中也有类似的功能。在市面上,用得最多的还是 POI 和 EasyExcel,而在处理大文件这方面,EasyExcel 更加适合一些。

在文件导出过程中,用异步的方式进行,用户不需要在页面一直等待。异步文件生成之后,把文件上传到云存储中,再通知用户去下载即可。

这里云存储选择阿里云的 OSS,线程池异步处理采用 @Async

用户通知这里就是用 Spring Mail 进行邮件发送即可。

三、具体实现

入口是一个 Controller,主要接收用户的文件导出请求。

这里做了一些简化,比如筛选条件、以及具体的获取数据部分我都省略了,大家可以根据自己的业务情况来实现。

java 复制代码
@RestController
@RequestMapping("/export")
public class DataExportController {

    @Autowired
    private ExcelExportService exportService;

    @GetMapping("/data")
    public ResponseEntity<String> exportData() {
        List<DataModel> data = fetchData();
        String fileUrl = exportService.exportDataAsync(data);

        return ResponseEntity.ok("导出任务开始,文件生成后会通知您下载链接");
    }

    private List<DataModel> fetchData() {
        // 获取需要导出的数据
        return null; // 省略具体实现
    }
}

下面是导出服务的具体实现:

java 复制代码
@Service
public class ExcelExportService {

    @Async("exportExecutor")
    public String exportDataAsync(List<DataModel> data) {
        // 生成 Excel 文件并获取 InputStream
        InputStream fileContent = generateExcelFile(data);
        String fileName = "data_" + System.currentTimeMillis() + ".xlsx";

        // 上传到 OSS
        String fileUrl = ossService.uploadFile(fileName, fileContent);

        // 发送邮件通知
        emailService.sendEmail(data.getUserEmail(), "文件导出通知", "您的文件已导出,下载链接:");

        return fileUrl;
    }

    private InputStream generateExcelFile(List<DataModel> data) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            // 使用 EasyExcel 写入数据到输出流
            ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream, DataModel.class);
            writerBuilder.sheet("Data").doWrite(data);
        } catch (Exception e) {
            // 处理异常(如日志记录、抛出自定义异常等)
            e.printStackTrace(); // 实际项目中建议使用 logger
        }
        // 将字节数组转换为 InputStream 返回
        return new ByteArrayInputStream(outputStream.toByteArray());
    }

    // DataModel 类定义
    public static class DataModel {
        // 省略参数及 setter/getter 方法
        // 示例:
        // private String name;
        // private String email;
        //
        // getter 和 setter 方法...
    }
}

这里面用到了 @Async 来实现一个异步处理,这里主要干了三件事:

  • 使用 EasyExcel 生成文件
  • OSS 上传生成后的文件
  • 给用户发邮件通知下载地址

这里为了用到真正的线程池,制定了一个自定义的 exportExecutor,实现如下:

java 复制代码
@Configuration
@EnableAsync
public class AsyncExecutorConfig {

    @Bean("exportExecutor")
    public Executor exportExecutor() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("registerSuccessExecutor-%d").build();

        ExecutorService executorService = new ThreadPoolExecutor(
            10, // corePoolSize:核心线程数
            20, // maximumPoolSize:最大线程数
            0L, // keepAliveTime:空闲线程存活时间(毫秒)
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(1024), // 队列容量
            namedThreadFactory,
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程执行任务
        );

        return executorService;
    }
}

OSS上传服务部分代码实现如下,依赖阿里云OSS的API进行文件上传:

java 复制代码
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.PutObjectRequest;

import java.io.InputStream;
import java.net.URL;
import java.util.Date;

public class OssService {

    private String endpoint = "<OSS_ENDPOINT>";
    private String accessKeyId = "<ACCESS_KEY_ID>";
    private String accessKeySecret = "<ACCESS_KEY_SECRET>";
    private String bucketName = "<BUCKET_NAME>";

    public String uploadFile(String fileName, InputStream fileContent) {
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        try {
            // 上传文件到 OSS
            ossClient.putObject(new PutObjectRequest(bucketName, fileName, fileContent));

            // 设置预签名 URL 过期时间为 1 小时
            Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
            URL url = ossClient.generatePresignedUrl(bucketName, fileName, expiration);

            return url.toString();
        } finally {
            if (ossClient != null) {
                ossClient.shutdown(); // 关闭客户端,释放资源
            }
        }
    }
}

邮件发送部分实现:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Service;

@Service
public class EmailNotificationService {

    @Autowired
    private JavaMailSender mailSender;

    public void sendEmail(String toAddress, String subject, String body) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom("noreply@example.com");
        message.setTo(toAddress);
        message.setSubject(subject);
        message.setText(body);

        mailSender.send(message);
    }
}

还需要一些额外的 Spring Mail 的配置,配置到 application.properties

java 复制代码
spring.mail.host=smtp.example.com
spring.mail.port=587
spring.mail.username=user@example.com
spring.mail.password=yourpassword
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
相关推荐
Lisonseekpan2 小时前
IntelliJ IDEA 快捷键全解析与高效使用指南
java·ide·后端·intellij-idea
Fantasydg2 小时前
外卖项目 day01
java
SeaTunnel2 小时前
结项报告完整版:Apache SeaTunnel 支持 Flink 引擎 Schema Evolution 功能
java·大数据·flink·开源·seatunnel
q***71852 小时前
常见的 Spring 项目目录结构
java·后端·spring
元亓亓亓2 小时前
考研408--操作系统--day4--进程同步&互斥&信息量机制
java·数据库·考研·操作系统·408
武子康2 小时前
Java-169 Neo4j CQL 实战速查:字符串/聚合/关系与多跳查询
java·开发语言·数据库·python·sql·nosql·neo4j
q***23572 小时前
记录 idea 启动 tomcat 控制台输出乱码问题解决
java·tomcat·intellij-idea
一只小灿灿2 小时前
深入解析 Maven 与 Gradle:Java 项目构建工具的安装、使用
java·开发语言·maven
深色風信子2 小时前
Java Maven Log4j 项目日志打印
java·log4j·maven·java maven