java 异步表格导出

以前我们项目汇总的导出:直接查询数据库,生成Excel然后返回给前端。

简单的做以下优化项(实际上都没使用):

1:如果共用界面查询的分页接口,可以去掉分页的总条数查询,pageSize传大点

2:Spring MVC  的 Controller 返回 Callable,实现异步请求,防止长时间的文件导出任务一直占用tomcat线程

3:如果没有共用分页查询,可以使用mysql的流式查询,在回调接口中一条条的返回数据,可以用一条丢一条,及时释放内存减小内存压力

4:前端自己做分页请求,生成文件,这样后端改动最小(项目中真遇到过这样的界面,性能不太好)

改造之前,我们项目中用的最多的就是第一种方式,带来的问题就是,数据量过大,内存飙升,也发生过几次oom,用户体验方面,导出任务比较慢的情况,用户不能离开当前界面需要保持连接,还很可能超时。至于慢sql这种问题,需要开发者自己优化了

现在我们的微服务项目改成了统一的导出模式可以避免上面说的那些问题。整体流程如下图:

整体概述就是所有的请求全部请求同一个微服务的Controller,然后用户立即收到请求成功信息,并在统一的任务界面查看导出进度。

再解析url找到对应的微服务应用,由Ribbon负责负载均衡分页请求,然后处理返回数据,解析后的数据写入临时文件(我这里使用的是 EasyExcel,api简单一点)。最后上传到 OSS文件服务器,更新下载url到数据库,展示给用户,并且重复下载直接从文件服务器下载。

图中标记1处

        1:可以用线程池,也可以用MQ,此处可以统计当前进行中的任务数量,比如限制50个任务同时导出,超出最大任务量直接提示 “系统繁忙,请稍后尝试”

        2:这里我还加了个规则。根据返回的总条数计算,如果发现超过了5页,就同时启动两个线程并发查询。

        3:异步操作,别忘了事务问题(数据库的事务还没提交,线程池中的任务已经开始执行,查不到数据,然后报错)

图中标记2处:

       1: 前端传入查询数据的链接和参数(导出列表数据的情况下,url和参数一般和界面的列表查询接口相同,这样可以减少一个专门的导出接口,在返回对象中添加对应的Excel注解用于生成表头和数据转换),构建请求信息去调用其他服务的查询接口,一定要带上用户信息,因为大部分系统都有数据权限,防止用户看到无权查看的内容。

        构建分页查询参数时,根据自己的内存使用情况,我这里是默认每次查询400条,最多50000条数据,当然这些参数也可以由前端提供

        2:尽量全部查询接口用分页查询,当时我做这个的时候为了兼容老接口,支持不分页(囧),然后有个接口一次返回了几万条,线上观察到内存突增(大量的Excel cell对象和json解析相关的对象 char[] 等。具体不记得了,,,,)

        3:查询分页接口,如果要做进度展示,可以优化一下分页查询的count()查询。sql 的优化不多说了,我们java代码层面可以控制只请求一次count就行(如果不需要查看进度,完全去掉count查询也挺好),可以自定义mybatisplus的分页拦截器处理,测试了发现PageHelper 和 mybatisplus自带的IPage<T>都能正常拦截



//先创建拦截器
public class TestPaginationInnerInterceptor extends PaginationInnerInterceptor {

    public TestPaginationInnerInterceptor(DbType dbType) {
        super(dbType);
    }

    @Override
    public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 这里根据请求方发送的默认参数判断,如果是第一次请求就分页
        //SysExcelUtils.EXPORT_SEARCH_COUNT  是一个ThreadLocal 不详细解释了
        if (Boolean.FALSE.equals(SysExcelUtils.EXPORT_SEARCH_COUNT.get())) {
            return true;
        }
        return super.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    }
}



//然后在配置类中注册
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
     MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
     interceptor.addInnerInterceptor(new TestPaginationInnerInterceptor(DbType.MYSQL));
     return interceptor;
    }

图中标记3处:

我们是在线程中分页循环请求数据,返回的数据需要进行转换,这里的转换我只写了三种,

数据字典注解   比如@DicMap("test")(1:线上配置的数据字典,最好缓存起来    2:枚举    3:自定义key,value)

具体实现:略

数字格式化注解  解析 com.alibaba.excel.annotation.format.NumberFormat

  直接找到EasyExcel的源码抄过来就行

GlobalConfiguration config = new GlobalConfiguration();

NumberDataFormatterUtils.format(new BigDecimal(String.valueOf(o)), (short) 0, headDTO.getNumberFormat(), config );

日期格式化注解   解析 com.alibaba.excel.annotation.format.DateTimeFormat

把以上对应的数据解析到新的结果集,再写入临时文件

我们可以做个小优化,数据转换后,原来的几百行数据就没用了,这时手动把引用设置为null,原因如下图中JDK源码注释:

如果写入临时文件后,没有文件服务器上传怎么办?那就老老实实的用文章开头的哪几种办法吧,再加个文件压缩什么的,或者直接配置springMVC 的静态目录,然后文件生成到静态目录。 

图中标记4处:

        用户查看进度,这里有条件的花,使用webSocket挺好,我们项目商量了下,没必要为导出专门加上websocket,所以用的就是最原始的定时任务请求,

具体实现:进入首页会请求一次,发现没有进行中的任务就不再请求。返回数据时顺便检测进行中的任务是否长时间没有更新,有时服务发版或者崩溃,会导致进行中的任务中断,所以这里判断长时间没有更新状态,就把这个任务设置为“失败”,防止前端一直请求

除了前面两种方案,我们也可以借鉴其他框架的长轮询方案,大概为:后端Controller 为异步请求,前端发起请求后,后端不会立即返回;因为是异步请求,所以tomcat线程也不会一直被占用,10秒内,导出任务有状态变化就返回给前端,没有变化就返回无变化即可,前端接着开启下次长轮询

界面上可以设置失败重试和重新下载按钮。失败重试:根据数据库中保存的url,请求参数还原请求,直接开始导出任务。 重新下载则直接把OSS上的url地址给前端,直接下载

最后:

        现有代码改造,需要前后端一同修改,前端把调用接口全部统一格式发送到固定接口,然后用户等待异步返回结果就行。后端改造需要把原来的同步生成Excel文件接口改成统一的JSON返回格式用于数据解析,并且把批量查询改成分页查询。

查询接口返回的统一结果对象 Result(可能是其他名字,这里只是举例)对象中,添加一个方法 public Result<T> addExcelHeaderClass(Class<?>  clazz),    clazz参数就表示需要解析的excel头对象,判断只有导出任务才需要解析,只需要解析一次,然后缓存起来。

动态列表的导出可能需要单独把header头信息传过去,具体实现就不写了。

最后效果是后端的分页查询接口可以直接作为导出接口使用。

我们当前的项目有70多个导出接口,,,,写完代码,加每个导出接口挨个修改,大概8个左右工作日就没了

<think>好的,我现在需要帮助用户理解如何使用Java中的CompletableFuture实现异步导出。根据用户提供的引用内容,特别是引用[4]提到的使用CompletableFuture处理耗时任务,比如数据表格导出,结合其他引用中的方法,我需要整理出一个清晰的步骤和示例。 首先,用户的需求是导出数据时,可能涉及多个耗时查询,这些查询可以并行执行,然后等待所有结果再进行后续处理。这正好可以用CompletableFuture的异步特性来实现。引用[1]和[2]提到thenApply和链式调用,而引用[3]提到了组合多个任务的方法,如allOf和anyOf。引用[4]则具体提到了在导出场景中的应用。 接下来,我需要确定实现步骤。通常,导出流程可能包括:用户触发导出,创建异步任务,并行执行数据查询,组装数据,生成文件,通知用户下载。根据引用[5],可能还需要结合WebSocket通知前端。 然后,考虑代码示例。需要展示如何创建CompletableFuture,使用supplyAsync执行异步任务,使用thenApply处理结果,以及使用allOf等待所有任务完成。例如,用户可能需要查询订单、用户和商品数据,这些可以并行处理,然后合并数据生成Excel。 需要注意异常处理,比如使用exceptionally方法。同时,要展示如何获取所有结果,使用join或get。引用[3]中的acceptEither可能不适用,因为需要所有任务完成,所以应该用allOf。 最后,生成相关问题,帮助用户深入理解,如性能优化、异常处理、与其他工具结合等。</think>### 如何使用Java CompletableFuture实现异步导出 #### 一、实现思路 1. **用户触发导出**:前端发起导出请求,后端创建异步任务 2. **并行执行耗时查询**:使用`CompletableFuture.supplyAsync()`并行执行多个数据查询 3. **同步等待结果**:通过`CompletableFuture.allOf()`等待所有任务完成 4. **数据组装处理**:使用`.thenApply()`进行结果合并与数据处理 5. **文件生成与通知**:生成文件后通过WebSocket通知前端下载[^5] #### 二、完整代码示例 ```java public class ExportService { // 模拟数据查询服务 private OrderService orderService; private UserService userService; private ProductService productService; public CompletableFuture<String> asyncExport(Long taskId) { // 1. 创建异步任务 CompletableFuture<List<Order>> futureOrders = CompletableFuture.supplyAsync( () -> orderService.queryOrders(), Executors.newCachedThreadPool() ).exceptionally(ex -> { System.err.println("订单查询失败: " + ex.getMessage()); return Collections.emptyList(); }); CompletableFuture<List<User>> futureUsers = CompletableFuture.supplyAsync( () -> userService.queryUsers() ); CompletableFuture<List<Product>> futureProducts = CompletableFuture.supplyAsync( () -> productService.queryProducts() ); // 2. 等待所有任务完成并处理结果 return CompletableFuture.allOf(futureOrders, futureUsers, futureProducts) .thenApply(v -> { try { List<Order> orders = futureOrders.get(); List<User> users = futureUsers.get(); List<Product> products = futureProducts.get(); // 3. 数据组装 ExportData exportData = assembleData(orders, users, products); // 4. 生成Excel文件 String filePath = generateExcel(exportData); return filePath; } catch (InterruptedException | ExecutionException e) { throw new RuntimeException("数据组装失败", e); } }); } private String generateExcel(ExportData data) { // 实现Excel生成逻辑 return "/export/files/report_2023.xlsx"; } } ``` #### 三、关键API说明 1. **`supplyAsync()`**:创建异步任务并返回`CompletableFuture` ```java CompletableFuture.supplyAsync(() -> {...}, executor) ``` 2. **`allOf()`**:等待所有任务完成 ```java CompletableFuture.allOf(future1, future2) .thenApply(v -> {...}) ``` 3. **`thenApply()`**:链式处理结果 ```java future.thenApply(data -> transform(data)) ``` 4. **`exceptionally()`**:异常处理 ```java future.exceptionally(ex -> handleError(ex)) ``` #### 四、WebSocket通知集成示例 ```java @Async public void handleExportTask(Long taskId) { CompletableFuture<String> exportFuture = asyncExport(taskId); exportFuture.thenAccept(filePath -> { // 通过WebSocket通知前端 websocketSender.send(taskId, "EXPORT_SUCCESS", filePath); }).exceptionally(ex -> { websocketSender.send(taskId, "EXPORT_FAILED", ex.getMessage()); return null; }); } ``` #### 五、最佳实践建议 1. **线程池配置**:建议使用自定义线程池控制并发量 ```java ExecutorService executor = Executors.newFixedThreadPool(10); ``` 2. **超时控制**:添加超时机制避免长时间阻塞 ```java future.get(30, TimeUnit.SECONDS); ``` 3. **结果缓存**:对已完成的导出任务进行缓存[^4]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值