# easy-file
**Repository Path**: codesong/easy-file
## Basic Information
- **Project Name**: easy-file
- **Description**: EasyFile-一整套Web大文件导出解决方案。轻松导出千万以上数据
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 37
- **Created**: 2022-07-20
- **Last Updated**: 2023-06-08
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# EasyFile
### 介绍
EasyFile-是为了提供更加便捷的文件服务,可以提供统一的操作入口
目前主要支持导出功能
支持同步导出、异步导出、文件压缩、流式导出、分页导出、导出缓存复用等特性。
优化缓解导出文件时对服务的内存和CPU影响。针对文件服务可做更多的管理。
提供给开发者更加通用、快捷、统一的实现的API方案;
### 解决问题
1、瞬时加载数据过大导致内存飙高不够平滑机器宕机风险很大
2、生成较大文件容易出现HTTP 超时,造成导出失败
3、相同条件的导出结果无法做到复用,需要继续生成导出文件资源浪费
4、导出任务集中出现没有可监控机制
5、开发者不仅需要关心数据查询逻辑同时需要关心文件生成逻辑
### 框架对比
与 Alibaba 的EasyExcel 相比,两者侧重点不同。
Alibaba EasyExcel 是一个Excel文件生成导出、导入 解析工具。
EasyFile 是一个大文件导出的解决方案。用于解决大文件导出时遇到的,文件复用,文件导出超时,内存溢出,瞬时CPU 内存飙高等等问题的一整套解决方案。 同时EasyFile 不仅可以用于Excel
文件的导出,也可以用于csv,pdf,word 等文件导出的管理(暂时需要用户自己集成基础导出下载类BaseDownloadExecutor 实现文件生成逻辑)。
而且,EasyFile和Alibaba EasyExcel 并不冲突,依然可以结合EasyExcel 使用,文件生成逻辑使用Alibaba EasyExcel 做自行拓展使用。
### 软件架构
EasyFile 提供两种模式
local 模式: 需要提供本地的api 存储Mapper. 将数据存储到本地数据库中管理。
remote模式:需要部署easyfile-server 服务,并设置客户端调用远程EasyFile 的域名。
### 代码结构
- easyfile-common: 公共模块服务
- easyfile-storage: 存储服务
- easyfile-storage-api: 存储服务API
- easyfile-storage-remote: 远程调用存储
- easyfile-storage-local: 本地数据源存储
- easyfile-spring-boot-starter: easyfile starter 包
- easyfile-server: easyfile 远程存储服务端
- easyfile-example: 样例工程
- easyfile-example-local: 本地储存样样例工程
- easyfile-example-remote: 远程存储样例工程
### 使用教程
#### 一、引入maven依赖
如果使用本地模式 引入maven
```xml
org.svnee
easyfile-spring-boot-starter
1.0.0
org.svnee
easyfile-storage-local
1.0.0
```
如果使用远程模式引入maven 依赖
```xml
org.svnee
easyfile-spring-boot-starter
1.0.0
org.svnee
easyfile-storage-remote
1.0.0
```
#### 二、Client端需要提供文件上传服务进行实现接口
```java
package org.svnee.easyfile.storage.file;
import java.io.File;
import org.svnee.easyfile.common.bean.Pair;
/**
* 文件上传服务
*
* @author svnee
*/
public interface UploadService {
/**
* 文件上传
* 如果需要重试则需要抛出 org.svnee.easyfile.starter.exception.GenerateFileException
*
* @param file 文件
* @param fileName 自定义生成的文件名
* @param appId 服务ID
* @return key: 文件系统 --> value:返回文件URL/KEY标识符
*/
Pair upload(File file, String fileName, String appId);
}
```
将文件上传到自己的文件存储服务
#### 三、额外处理
如果是使用Local模式,需要提供Client配置
```properties
##### easyfile-local-datasource
easyfile.local.datasource.type=com.zaxxer.hikari.HikariDataSource
easyfile.local.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
easyfile.local.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8
easyfile.local.datasource.username=root
easyfile.local.datasource.password=123456
```
需要执行SQL:
```sql
CREATE TABLE ef_async_download_task
(
id BIGINT (20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
task_code VARCHAR(50) NOT NULL DEFAULT '' COMMENT '任务编码',
task_desc VARCHAR(50) NOT NULL DEFAULT '' COMMENT '任务描述',
app_id VARCHAR(50) NOT NULL DEFAULT '' COMMENT '归属系统 APP ID',
unified_app_id VARCHAR(50) NOT NULL DEFAULT '' COMMENT '统一APP ID',
enable_status TINYINT (3) NOT NULL DEFAULT 0 COMMENT '启用状态',
limiting_strategy VARCHAR(50) NOT NULL DEFAULT '' COMMENT '限流策略',
version INT (10) NOT NULL DEFAULT 0 COMMENT '版本号',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
create_by VARCHAR(50) NOT NULL DEFAULT '' COMMENT '创建人',
update_by VARCHAR(50) NOT NULL DEFAULT '' COMMENT '更新人',
is_deleted BIGINT (20) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (id),
UNIQUE KEY `uniq_app_id_task_code` (`task_code`,`app_id`) USING BTREE
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '异步下载任务';
CREATE TABLE ef_async_download_record
(
id BIGINT (20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
download_task_id BIGINT (20) NOT NULL DEFAULT 0 COMMENT '下载任务ID',
app_id VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'app ID',
download_code VARCHAR(50) NOT NULL DEFAULT '' COMMENT '下载code',
upload_status VARCHAR(50) NOT NULL DEFAULT '' COMMENT '上传状态',
file_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '文件路径',
file_system VARCHAR(50) NOT NULL DEFAULT '' COMMENT '文件所在系统',
download_operate_by VARCHAR(50) NOT NULL DEFAULT '' COMMENT '下载操作人',
download_operate_name VARCHAR(50) NOT NULL DEFAULT '' COMMENT '下载操作人',
remark VARCHAR(50) NOT NULL DEFAULT '' COMMENT '备注',
notify_enable_status TINYINT (3) NOT NULL DEFAULT 0 COMMENT '通知启用状态',
notify_email VARCHAR(50) NOT NULL DEFAULT '' COMMENT '通知有效',
max_server_retry INT (3) NOT NULL DEFAULT 0 COMMENT '最大服务重试',
current_retry INT (3) NOT NULL DEFAULT 0 COMMENT '当前重试次数',
execute_param TEXT NULL COMMENT '重试执行参数',
error_msg VARCHAR(256) NOT NULL DEFAULT '' COMMENT '异常信息',
last_execute_time DATETIME NULL COMMENT '最新执行时间',
invalid_time DATETIME NULL COMMENT '链接失效时间',
download_num INT (3) NOT NULL DEFAULT 0 COMMENT '下载次数',
version INT (10) NOT NULL DEFAULT 0 COMMENT '版本号',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
create_by VARCHAR(50) NOT NULL DEFAULT '' COMMENT '创建人',
update_by VARCHAR(50) NOT NULL DEFAULT '' COMMENT '更新人',
PRIMARY KEY (id),
KEY `idx_download_operate_by` (`download_operate_by`) USING BTREE,
KEY `idx_operator_record` (`download_operate_by`,`app_id`,`create_time`),
KEY `idx_upload_invalid` (`upload_status`,`invalid_time`,`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '异步下载记录';
```
如果是使用remote服务,需要部署easyfile-server 服务,Client提供配置
```properties
#### easyfile-storage-remote
easyfile.remote.username=example
easyfile.remote.password=example
easyfile.remote.server-addr=127.0.0.1:8080
easyfile.remote.namespace=remote-example
```
#### 四、客户端配置
如果使用默认异步文件处理器(`org.svnee.easyfile.starter.executor.impl.DefaultAsyncFileHandler`)
提供了一些配置
| 配置key | 描述 | 默认值 |
| ------------------------------------------------------------ | ----------------------------------- | ------ |
| easyfile.default.async.download.handler.thread-pool.core-pool-size | 默认下载线程池核心线程数 | 10 |
| easyfile.default.async.download.handler.thread-pool.maximum-pool-size | 默认下载线程池最大线程池数 | 20 |
| easyfile.default.async.download.handler.thread-pool.keep-alive-time | 默认下载线程池最大空闲时间 单位:秒 | 30 |
| easyfile.default.async.download.handler.thread-pool.max-blocking-queue-size | 默认下载线程池阻塞队列最大长度 | 2048 |
如果使用Job调度式的文件下载处理器(`org.svnee.easyfile.starter.executor.impl.ScheduleTriggerAsyncFileHandler`)
提供出了一些配置
```properties
easyfile.schedule.async.download.handler.enable=true
easyfile.schedule.async.download.handler.thread-pool-core-pool-size=2
easyfile.schedule.async.download.handler.thread-pool-thread-prefix=ScheduleAsyncHandler
easyfile.schedule.async.download.handler.max-execute-timeout=1600
easyfile.schedule.async.download.handler.max-trigger-count=5
easyfile.schedule.async.download.handler.schedule-period=10
easyfile.schedule.async.download.handler.trigger-offset=50
easyfile.schedule.async.download.handler.look-back-hours=2
easyfile.schedule.async.download.handler.max-archive-hours=24
```
| 配置key | 描述 | 默认值 |
| ------------------------------------------------------------ | ----------------------------------- | ------ |
| easyfile.schedule.async.download.handler.enable | 是否启用调度式异步处理器 | false |
| easyfile.schedule.async.download.handler.thread-pool-core-pool-size | 调度处理器单机核心线程数 | 2 |
| easyfile.schedule.async.download.handler.thread-pool-thread-prefix | 调度处理器线程前缀 | ScheduleAsyncHandler |
| easyfile.schedule.async.download.handler.max-execute-timeout | 调度处理一次最大超时 单位:秒 | 1600 |
| easyfile.schedule.async.download.handler.max-trigger-count | 最大调度重试次数 | 5 |
| easyfile.schedule.async.download.handler.schedule-period | 调度周期 单位:秒 | 10 |
| eeasyfile.schedule.async.download.handler.trigger-offset | 触发调度一次触发量 | 50 |
| easyfile.schedule.async.download.handler.look-back-hours | 一次回溯处理时间 单位:小时 | 2 |
| easyfile.schedule.async.download.handler.max-archive-hours | 已经执行完成的归档保持时间 单位:小时 | 24 |
Client 配置
| 配置key | 描述 | 默认值 |
| ----------------------------------------- | ------------------------------------------------------------ | ----------------- |
| easyfile.download.enabled | EasyFile是否启用 | true |
| easyfile.download.app-id | Client端 AppId | |
| easyfile.download.unified-app-id | Client端统一AppId | 默认是 用appId |
| easyfile.download.local-file-temp-path | Client端下载文件本地临时目录 | /tmp |
| easyfile.download.enable-auto-register | Client端自动注册下载任务开关 | false |
| easyfile.download.enable-compress-file | Client 是否开启文件压缩优化 | false |
| easyfile.download.min-enable-compress-mb-size | Client 启用文件压缩最小的大小,单位:MB 在启用文件压缩后生效 | 1 |
| easyfile.download.export-advisor-order | Client下载切面顺序 | Integer.MAX_VALUE |
| easyfile.download.excel-max-sheet-rows |Client 导出Excel 单Sheet最大行数|1000000|
| easyfile.download.excel-row-access-window-size |Client 导出Excel 缓存最大行数|1000|
| easyfile.download.clean-file-after-upload |Client 导出Excel 后是否删除源文件|true|
#### 五、实现下载器
实现接口:`org.svnee.easyfile.starter.executor.BaseDownloadExecutor`
并注入到Spring ApplicationContext中,并使用注解 `org.svnee.easyfile.common.annotations.FileExportExecutor`
如果需要支持同步导出,需要设置文件的HttpResponse 请求头,需要实现接口 `org.svnee.easyfile.starter.executor.BaseWrapperSyncResponseHeader`
例如:
```java
import org.springframework.stereotype.Component;
import org.svnee.easyfile.common.annotations.FileExportExecutor;
import org.svnee.easyfile.common.bean.DownloaderRequestContext;
import org.svnee.easyfile.starter.executor.BaseDownloadExecutor;
import org.svnee.easyfile.starter.executor.BaseWrapperSyncResponseHeader;
@Component
@FileExportExecutor("ExampleExcelExecutor")
public class ExampleExcelExecutor implements BaseDownloadExecutor,BaseWrapperSyncResponseHeader {
@Override
public boolean enableAsync(DownloaderRequestContext context) {
// 判断是否开启异步
return true;
}
@Override
public void export(DownloaderRequestContext context) {
// 生成文件下载逻辑
}
}
```
#### 拓展
类继承关系图

##### 下载器
1、分页下载支持
`org.svnee.easyfile.starter.executor.PageShardingDownloadExecutor`
提供更加方便的分页支持
`org.svnee.easyfile.starter.executor.impl.AbstractPageDownloadExcelExecutor`
需要配合使用(`org.svnee.easyfile.common.annotations.ExcelProperty`)
多Sheet组下载支持
`org.svnee.easyfile.starter.executor.impl.AbstractMultiSheetPageDownloadExcelExecutor`
2、流式下载支持
`org.svnee.easyfile.starter.executor.StreamDownloadExecutor`
提供更加方便的流式支持
`org.svnee.easyfile.starter.executor.impl.AbstractStreamDownloadExcelExecutor`
多Sheet组下载支持
`org.svnee.easyfile.starter.executor.impl.AbstractMultiSheetStreamDownloadExcelExecutor`
需要配合使用(`org.svnee.easyfile.common.annotations.ExcelProperty`)
##### 限流执行器
如需限流需要实现ExportLimitingExecutor
```java
package org.svnee.easyfile.storage.expand;
import org.svnee.easyfile.common.request.ExportLimitingRequest;
/**
* 限流服务
*
* @author svnee
*/
public interface ExportLimitingExecutor {
/**
* 策略
*
* @return 策略code码
*/
String strategy();
/**
* 限流
*
* @param request request
*/
void limit(ExportLimitingRequest request);
}
```
##### 缓存开启
导出结果缓存,主要是为了能够复用大文件的导出,减少不必要的相同数据的进行多次重复导出。以此尽可能的复用已经成功导出的结果。
导出的数据主要分成三种:\
1、静态数据(已经是过去数据,不在发生变化,相同条件多次导出结果一致) \
2、动态数据(正在发生的数据,数据一直在改变,相同条件多次导出结果不一致) \
3、静态数据+动态数据(部分数据已经不在改变、部分数据依旧在改变)
导出结果缓存 主要适用在第一种情况的场景
1、需要实现时,重写开启缓存方法
```java
/**
* 开启导出缓存
*
* @param context context
* @return 是否开启缓存
*/
default boolean enableExportCache(BaseDownloaderRequestContext context){
return false;
}
```
2、提供需要判定缓存的key的结果-用于比较是否一致.如果cache-key 为空时,则缓存为匹配所有
```java
/**
* 文件导出执行器
*
* @author svnee
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface FileExportExecutor {
/**
* 执行器code
*/
String value();
/**
* 执行器中文解释
* 默认是{@link #value()}
*/
String desc() default "";
/**
* 是否开启通知
*/
boolean enableNotify() default false;
/**
* 最大服务端重试次数
* 小于等于0时不在执行重试。
*/
int maxServerRetry() default 0;
/**
* cache-key
*
* @see BaseDownloaderRequestContext#getOtherMap() 中的key的对应的value值
* 如果有值则可以使用点使用指向最终的数据字段。例如:a.b.c
* 支持SpringEL表达式
*/
String[] cacheKey() default {};
}
// 例如
@FileExportExecutor(value = "studentStreamDownloadDemo", desc = "Student导出", cacheKey = {"#request.age"})
```
##### 子列单元格导出支持
目前针对 1:* 的映射导出 只支持及到两级,暂时不支持三级及以上(即:1:* :* ) \
excel的导出支持1:* 的数据单元行列的导出。例如:\

但是针对\
1、1:* 的导出数据时,不建议导出过多数据,由于需要merge 单元格的原因,导致导出生成excel时很慢,建议数量小于 2K行
2、针对特别大的数据时,建议使用1:1的单元格导出 \

##### 多Sheet分组导出支持
需要按照多个Sheet进行分组查询导出 导出数据形如,

EasyFile 提供两个执行器
- 流式-多Sheet组导出
`org.svnee.easyfile.starter.executor.impl.AbstractMultiSheetStreamDownloadExcelExecutor`
- 分页-多Sheet组导出
`org.svnee.easyfile.starter.executor.impl.AbstractMultiSheetPageDownloadExcelExecutor`
##### 优化建议
针对大文件导出功能目前easyfile 提供两种处理方式 分页导出/流式导出。\
1、但是针对大文件导出时建议将单sheet的最大行数设置的比较大,甚至设置成07版本excel的单sheet最大行数,避免频繁创建单Sheet导致内存无法回收OOM
配置为: `easyfile.download.excel-max-sheet-rows` \
2、针对大文件导出时可适量的根据内存本身的大小做设定excel缓存在内存中的行数。不建议设置特别大 配置为: `easyfile.download.excel-row-access-window-size` \
设置过大,会对内存有一定的压力。过小则会频繁的刷新数据到磁盘中,CPU容器上升。\
3、针对分页/流式导出 使用时设置一次查询行数,需要合理设置 \
分页导出时,需要注意分页的分页大小的设置 \
流式导出时,需要注意增强数据缓存的长度即方法`org.svnee.easyfile.starter.executor.impl.AbstractStreamDownloadExcelExecutor.enhanceLength`
##### 内存性能验证
使用本地存储模式 启动参数`-Xms512M -Xmx512M -Xmn512M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M` \
导出数据行数100w,生成文件大小30079kb excel(2007版本) \
设置分页/流式buf 一次长度设置100 \
使用配置:
```properties
easyfile.download.excel-max-sheet-rows=10000000
easyfile.download.excel-row-access-window-size=100
```
使用分页导出CPU/内存情况 \
 \
使用流式导出CPU/内存情况 \

#### easyfile-server 部署
1、执行存储DB SQL \
2、部署服务