文章目录
- 积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)
- 优化结果
- 需求背景和解决方案的思考
- 解决方案
- 流程描述:
- 关键代码
- 引入easy excel
- 新建数据库表
- 识别所需注解、基类
- 下载中心服务(报表下载的导出任务)
- 数据跨库获取工具
- 拦截切面
- 报表下载任务处理事件
- 透传信息
- 给前端暴露接口
积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)
优化结果
原积木导出有两种导出,直接导出和大数据导出(大数据导出是做了优化去掉了一些样式之类的,性能更好)
实测中发现
原积木大数据导出性能:1万条数据导出耗时30秒,1.5万条耗时1.5分钟导出失败,数据超过一万条后经常导出失败,还会导致容器实例探活失败/内存撑爆重启
异步+自实现导出功能优化方案导出性能:34.6万条数据一次导出耗时2.5分钟,导出时快速返回空数据文件,避免页面死等不可用,导出后的表格是在线OSS的文件URL直接每个人都可以快速下载,可随时多次下载。
需求背景和解决方案的思考
详细讲了需求来由、解决历程的思考心路历程,有点长,想学习解决思路的可认真阅读,只需要解决方案的可以不看。我这正是因为当前项目组还没有专门做数据仓库的人,先用的积木这样的报表来做的。数据量大之后性能看着很心累,数据量大建议招专业的做数仓的。
项目组的数据报表、导出最开始的技术人员选型的是开源的积木报表(积木报表GitHub地址(5.4K star)),后续积木闭源了。在后续数据量增加后,积木报表经常性导出失败,运营同学反馈多次,于是优化报表的重担交给了我。
一期我先优化了SQL、涉及表的索引,取消走SQL join 一二十张表的方案,改为API代码分页实现查询。上线后效果仍旧不行,于是继续深入分析性能瓶颈,观察链路跟踪后发现,在导出时内存和CPU都飙升,甚至内存会被拉爆。
我看了下实际SQL执行时间其实相对于整个导出过程并没有很长,在500ms左右一个分页(5000条一个页,减少页到1000,2000未发现明显性能提升),在整个导出操作流程中,我通过抓包和链路跟踪查看,链路是导出按钮调用了积木的一个exportAllExcelStream接口,然后积木内部代码又去调用我配置的表数据源接口地址,将分页和查询条件拿去调我们项目接口获取数据,并导出为excel写入导出请求的响应流里,那既然我们获取数据的接口耗时占整个导出操作的占比低,其实性能瓶颈在积木框架内部。
于是我打开积木的代码一看,好家伙,都是abcdefg这样的命名,做过代码混淆加密的就一眼看得出,变量名被重命名混淆过了,甚至连积木导出接口的字符串都搜索不到没法找到入口,看来接口字符串都做了隐藏,于是我怀疑是excel导出大量数据有性能问题,我看了下积木依赖的pom文件里引入的是org.apache.poi,我去百度搜索了相关文章后,看到了其他博主说Apache POI导出有性能问题,性能低下,占用过多的内存,数据量大的时候特别明显,推荐使用阿里巴巴的EasyExcel来导出表格,只有十分之一的消耗。
于是收集完情况后,目前摆在眼前的两个痛点,第一个是导出慢会慢到网关直接504返回超时(一个接口调用几分钟以上),第二个是积木报表自身用了POI导出很慢,和领导讨论了一下,超时问题可以走异步实现解决,写一个导出下载中心,导出操作接口快速返回一个空的(注解+AOP切面实现拦截),然后异步去再次调用积木导出接口,并设置上请求头,方便拦截接口导出的切面识别到这个是异步执行,就返回实际数据不要返回空数据,这样就不会接口超时了,这个方案我先实现了下确实可行。
第二个问题就是导出慢的解决,由于闭源还有代码混淆加密操作,无法二开代码,因此要想解决POI这个问题你就需要自己实现导出,第一个想到的操作是直接在积木报表的页面加个快速导出按钮,可页面代码是积木自己生成的,不方便植入,第二个思路是,识别积木的导出操作,因为积木导出的时候还是要来我们接口这获取数据的,获取的时候带了分页信息,我们根据每页数据和配置的积木导出时每页数据条数这个配置对比就能识别到是不是导出操作,平时查看是10条每页,导出的时候会变5000条一页,很好区分,但区分完了,怎么实现导出操作呢?报表都是在积木的报表设计里用鼠标点点点的UI操作设计的,你自己接口怎么知道表头、数据源、表名、哪个账户导出的?
带着这些问题我深入查看了积木的数据库表,你积木能读取我就能读取,我看看你到底把这些关键信息存储在哪里了,翻开数据表里可以看到的,确实把表格的字段、宽度、样式、表格ID之类的信息都在Mysql里存着呢,那我只需要写一个解析这些数据的服务就可以像积木一样导出了。
方案思路可行性得到确认,那我就开始将复杂任务流程关键问题拆解并解决:
第一个是积木点击导出时的参数信息要完整拿到并保存到我们自己设计的导出任务里
(走积木自定义header预留的钩子实现透传请求数据)
第二个是要能正确读取我们人工在积木报表里配置的数据源API、表头名、表头字段等信息
(自行配置一个示范表并看数据库数据怎么存的,写代码解析这个数据结构并返回我们标准化的解析方法)
第三个是要自行实现调用数据源AP时积木的那次导出操作被拦截返回空数据,我们自己的导出正常导出数据
(新加注解,拦截注解的API,识别是否为积木导出操作,是则拦截,生成导出任务,返回空数据集合,否则直接执行原接口逻辑返回正常数据)
第四个是正确根据积木里的表头字段等信息使用EasyExcel生成表格并将表格上传到OSS,中间不建议产生机器上的物理文件(使用二进制流保存)
(写一个指定接口分页导出工具,按分页不断调用数据获取接口获得元数据,利用前面获得的表头字段进行解析生成EasyExcel导出数据时需要的结构体,最后使用EasyExcel将数据导出为excel文件的二进制流)
解决方案
流程描述:
识别并拦截积木导出操作,透传导出时参数生成自己设计的导出任务,异步执行导出任务(自行实现积木导出的逻辑),并使用高性能的阿里巴巴开源的EasyExcel工具,最后将EasyExcel导出的表格文件上传到OSS变为表格URL(记得做安全防护,避免表格泄漏)
积木的逻辑
积木前端导出按钮->积木导出接口->分页调取你的数据接口拉数据->生成excel返回
关键代码
大部分代码是不需要你改的,你可以根据自己项目结合使用,例如OSS上传的地方改为你自己的。后面的代码有点长~需要的人依次把代码弄进自己项目。
引入easy excel
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel-core</artifactId><version>3.1.1</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.1</version></dependency>
新建数据库表
CREATE TABLE `t_download_task` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',`account` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提交任务的账号',`title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '下载任务标题',`icon` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标',`url` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件URL',`file_size` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件大小',`percent` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '进度(例如50%)',`state` tinyint(4) DEFAULT '0' COMMENT '任务状态(0 等待执行,1执行中,2执行成功,3执行失败)',`error` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '执行报错信息(有则填)',`json` varchar(4096) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '{}' COMMENT '预留的json扩展字段',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_account_create_time` (`account`,`create_time`),KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB AUTO_INCREMENT=78 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='下载中心的任务';
下载任务
/*** <p>* 下载任务* </p>** @author humorchen* @since 2024-01-05*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_download_task")
public class DownloadTask implements Serializable {private static final long serialVersionUID = 1L;/*** ID*/@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 创建下载任务的账号*/@TableField("account")private String account;/*** 下载任务标题*/@TableField("title")private String title;/*** 图标*/@TableField("icon")private String icon;/*** 文件URL*/@TableField("url")private String url;/*** 文件大小*/@TableField("file_size")private String fileSize;/*** 进度(例如50%)*/@TableField("percent")private String percent;/*** 任务状态(0 等待执行,1执行中,2执行成功,3执行失败)*/@TableField("state")private Integer state;/*** 执行报错信息(有则填)*/@TableField("error")private String error;/*** 预留的json扩展字段*/@TableField("json")private String json;/*** 创建时间*/@TableField("create_time")private Date createTime;/*** 更新时间*/@TableField("update_time")private Date updateTime;public static final String ID = "id";public static final String ACCOUNT = "account";public static final String TITLE = "title";public static final String ICON = "icon";public static final String URL = "url";public static final String FILE_SIZE = "file_size";public static final String PERCENT = "percent";public static final String STATE = "state";public static final String ERROR = "error";public static final String JSON = "json";public static final String CREATE_TIME = "create_time";public static final String UPDATE_TIME = "update_time";}
使用mabatis plus code generator生成service、mapper等文件
识别所需注解、基类
数据获取api所调用服务上方法上标注的注解
/*** @author: humorchen* date: 2024/1/15* description: 该报表接口使用下载任务中心代理掉,完成下载任务* 使用要求:* 参数中需要有一个参数是DownloadCenterBaseParam的子类,方法返回值类型需要是支持泛型的JimuPageDto类,方法上加注@UseDownloadTaskCenter注解* 参考cn.sffix.recovery.report.service.impl.ReportServiceImpl#dashboardNewVersion(cn.sffix.recovery.report.entity.dto.DashBoardQueryDto)**/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface UseDownloadTaskCenter {}
示范给数据接口调用的服务加注解
@Override@UseDownloadTaskCenterpublic JimuPageDto<DashBoardDataDto> dashboardNewVersion(DashBoardQueryDto dashBoardQueryDto) {// 你的数据接口逻辑}
参数基类
/*** @author: humorchen* date: 2024/1/15* description:**/
@Data
public class DownloadCenterBaseParam {/*** 分页数据页号和页大小*/private Integer pageNo;/*** 分页数据页号和页大小*/private Integer pageSize;
}
结果基类
/*** @author: humorchen* date: 2023/12/19* description:**/
@Data
@FieldNameConstants
@Accessors(chain = true)
public class JimuPageDto<T> {/*** 数据*/private List<T> data;/*** 积木的count是总数据条数,不是当前页多少条!!!*/private long count;/*** 积木的total是总页数,不是总数据条数!!!*/private long total;public static final JimuPageDto EMPTY = new JimuPageDto().setData(Collections.emptyList()).setTotal(0).setCount(0);
}
/*** @author: humorchen* date: 2024/1/5* description: 下载任务状态**/
@Getter
public enum DownloadTaskStateEnum {WAIT(0, "等待执行"),RUNNING(1, "执行中"),SUCCESS(2, "执行成功"),FAILED(3, "执行失败"),;private final int state;private final String title;DownloadTaskStateEnum(int state, String title) {this.state = state;this.title = title;}/*** 根据状态获取枚举** @param state* @return*/public static DownloadTaskStateEnum of(int state) {for (DownloadTaskStateEnum value : values()) {if (value.state == state) {return value;}}return null;}
}
下载中心服务(报表下载的导出任务)
/*** <p>* 下载任务 服务类* </p>** @author humorchen* @since 2024-01-05*/
public interface IDownloadTaskService extends IService<DownloadTask> {/*** 注册任务** @param downloadTask* @return*/DownloadTask registerTask(@NonNull DownloadTask downloadTask);/*** 10秒内是否有相同任务未完成,不给再次注册下载任务** @param account* @param requestBody* @return*/boolean setSameTaskLock(String account, String requestBody);/*** 更新任务** @param downloadTask* @return*/int updateTaskById(@NonNull DownloadTask downloadTask);/*** 更新任务进度** @param id* @param percent* @return*/int changeTaskPercent(int id, @NonNull String percent);/*** 更新任务状态** @param id* @param state* @return*/int changeTaskState(int id, @NonNull DownloadTaskStateEnum state);/*** 更新任务状态** @param id* @param expectState* @param targetState* @return*/int compareAndSwapTaskState(int id, @NonNull DownloadTaskStateEnum expectState, @NonNull DownloadTaskStateEnum targetState);/*** 根据任务ID获取任务** @param id* @return*/DownloadTask getDownloadTaskById(int id);/*** 分页查下载任务** @param pageListDownloadTaskDto* @return*/IPage<DownloadTask> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto);/*** 重新执行下载任务** @param taskId* @return*/@RequestMapping("/rerunTask")Result<String> rerunTask(Integer taskId);/*** 根据报表ID获取报表名称** @param reportId* @return*/String getReportNameByReportId(String reportId);/*** 从请求体中获取报表ID** @param requestBody* @return*/String getReportIdFromRequestBody(String requestBody);/*** 根据报表ID获取报表API地址或者SQL** @param reportId* @return*/JimuReportDataSourceDTO getReportApiOrSqlByReportId(String reportId);/*** 获取积木报表的头** @param reportId* @return*/List<JimuReportDataColumnDTO> getReportHead(String reportId);/*** 从积木请求体中获取请求参数** @param json* @return*/String getRequestParamFromJson(String json);
}
实现类
/*** <p>* 下载任务 服务实现类* </p>** @author humorchen* @since 2024-01-05*/
@Service
@Slf4j
public class DownloadTaskServiceImpl extends ServiceImpl<DownloadTaskMapper, DownloadTask> implements IDownloadTaskService {@Autowiredprivate DownloadTaskMapper downloadTaskMapper;@Autowiredprivate IReportDataGetService reportDataGetService;@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 注入spring 事件发布器*/@Autowiredprivate ApplicationEventPublisher eventPublisher;/*** 注册任务** @param downloadTask* @return*/@Overridepublic DownloadTask registerTask(@NonNull DownloadTask downloadTask) {downloadTaskMapper.insert(downloadTask);return downloadTask;}/*** 10秒内是否有相同任务未完成,不给再次注册下载任务** @param account* @param requestBody* @return*/@Overridepublic boolean setSameTaskLock(String account, String requestBody) {DownloadTaskSubmitLimitCacheKey limitCacheKey = new DownloadTaskSubmitLimitCacheKey(account, MD5.create().digestHex(requestBody));Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(limitCacheKey.getKey(), DateUtil.now(), limitCacheKey.getExpire(), limitCacheKey.getTimeUnit());return Boolean.TRUE.equals(setIfAbsent);}/*** 更新任务** @param downloadTask* @return*/@Overridepublic int updateTaskById(@NonNull DownloadTask downloadTask) {return downloadTaskMapper.updateById(downloadTask);}/*** 更新任务进度** @param id* @param percent* @return*/@Overridepublic int changeTaskPercent(int id, @NonNull String percent) {UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();updateWrapper.eq(DownloadTask.ID, id);updateWrapper.set(DownloadTask.PERCENT, percent);log.info("【下载中心】更新任务进度 id:{} percent:{}", id, percent);return downloadTaskMapper.update(null, updateWrapper);}/*** 更新任务状态** @param id* @param state* @return*/@Overridepublic int changeTaskState(int id, @NonNull DownloadTaskStateEnum state) {UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();updateWrapper.eq(DownloadTask.ID, id);updateWrapper.set(DownloadTask.STATE, state.getState());return downloadTaskMapper.update(null, updateWrapper);}/*** 更新任务状态** @param id* @param expectState* @param targetState* @return*/@Overridepublic int compareAndSwapTaskState(int id, @NonNull DownloadTaskStateEnum expectState, @NonNull DownloadTaskStateEnum targetState) {UpdateWrapper<DownloadTask> updateWrapper = new UpdateWrapper<>();updateWrapper.eq(DownloadTask.ID, id);updateWrapper.eq(DownloadTask.STATE, expectState.getState());updateWrapper.set(DownloadTask.STATE, targetState.getState());return downloadTaskMapper.update(null, updateWrapper);}/*** 根据任务ID获取任务** @param id* @return*/@Overridepublic DownloadTask getDownloadTaskById(int id) {return downloadTaskMapper.selectById(id);}/*** 查下载任务** @param pageListDownloadTaskDto* @return*/@Overridepublic IPage<DownloadTask> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {Integer id = pageListDownloadTaskDto.getId();String startTime = pageListDownloadTaskDto.getStartTime();String endTime = pageListDownloadTaskDto.getEndTime();String fileName = pageListDownloadTaskDto.getFileName();Integer taskState = pageListDownloadTaskDto.getTaskState();UserInfo userInfo = UserInfoHolder.get();String account = userInfo.getAccount();int pageNo = Optional.ofNullable(pageListDownloadTaskDto.getPageNo()).orElse(1);int pageSize = Optional.ofNullable(pageListDownloadTaskDto.getPageSize()).orElse(10);QueryWrapper<DownloadTask> queryWrapper = new QueryWrapper<>();queryWrapper.eq(DownloadTask.ACCOUNT, account);queryWrapper.eq(id != null, DownloadTask.ID, id);queryWrapper.between(startTime != null && endTime != null, DownloadTask.CREATE_TIME, startTime, endTime);queryWrapper.like(StrUtil.isNotBlank(fileName), DownloadTask.TITLE, "%" + fileName + "%");queryWrapper.eq(taskState != null, DownloadTask.STATE, taskState);// 最新的在前queryWrapper.orderByDesc(DownloadTask.CREATE_TIME);return page(new Page<>(pageNo, pageSize), queryWrapper);}/*** 重新执行下载任务** @param taskId* @return*/@Overridepublic Result<String> rerunTask(Integer taskId) {DownloadTask downloadTask = getDownloadTaskById(taskId);if (downloadTask == null) {return Result.fail("未找到该任务,请刷新后重试");}if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {return Result.fail("任务正在执行中,请稍后重试");}eventPublisher.publishEvent(new DownloadTaskPublishEvent(taskId));return Result.ok("重新执行中");}/*** 根据报表ID获取报表名称** @param reportId* @return*/@Overridepublic String getReportNameByReportId(String reportId) {if (StrUtil.isBlank(reportId)) {return "";}String sql = "select name from report.jimu_report where id = '" + reportId + "'";JSONObject jsonObject = reportDataGetService.getOne(sql);return Optional.ofNullable(jsonObject.getString("name")).orElse("");}/*** 从请求体中获取报表ID** @param requestBody* @return*/@Overridepublic String getReportIdFromRequestBody(String requestBody) {if (StrUtil.isNotBlank(requestBody)) {JSONObject jsonObject = JSONObject.parseObject(requestBody);return jsonObject.getString("excelConfigId");}return null;}/*** 根据报表ID获取报表API地址或者SQL** @param reportId* @return*/@Overridepublic JimuReportDataSourceDTO getReportApiOrSqlByReportId(String reportId) {JimuReportDataSourceDTO jimuReportDataSourceDTO = new JimuReportDataSourceDTO();if (StrUtil.isNotBlank(reportId)) {String sql = "select db_dyn_sql,api_url from report.jimu_report_db where jimu_report_id = '" + reportId + "'";JSONObject jsonObject = reportDataGetService.getOne(sql);jimuReportDataSourceDTO.setSql(jsonObject.getString("db_dyn_sql"));jimuReportDataSourceDTO.setApiUrl(jsonObject.getString("api_url"));}List<List<String>> head = new ArrayList<>();EasyExcel.write(new OutputStream() {@Overridepublic void write(int b) throws IOException {}}).head(head).sheet("sheet").doWrite(new ArrayList<>());return jimuReportDataSourceDTO;}/*** 获取积木报表的头** @param reportId* @return*/@Overridepublic List<JimuReportDataColumnDTO> getReportHead(String reportId) {if (StrUtil.isBlank(reportId)) {return Collections.emptyList();}String sql = "select json_str from report.jimu_report where id = '" + reportId + "'";JSONObject jsonObject = reportDataGetService.getOne(sql);String jsonStr = jsonObject.getString("json_str");JSONObject json = JSONObject.parseObject(jsonStr);JSONObject rows = json.getJSONObject("rows");JSONObject rows0Cells = rows.getJSONObject("0").getJSONObject("cells");JSONObject rows1Cells = rows.getJSONObject("1").getJSONObject("cells");Set<String> rows0KeySets = rows0Cells.keySet();List<JimuReportDataColumnDTO> heads = rows0KeySets.stream().map(key -> {JSONObject keyObject = rows0Cells.getJSONObject(key);JSONObject columnObject = rows1Cells.getJSONObject(key);if (keyObject == null || columnObject == null) {return null;}String name = keyObject.getString("text");String column = columnObject.getString("text");if (StrUtil.isBlank(name) || StrUtil.isBlank(column)) {return null;}// 处理 #{vpjcgifyua.orderId}int indexOf = column.lastIndexOf(".");int indexOf2 = column.lastIndexOf("}");if (column.startsWith("#") && indexOf >= 0 && indexOf2 >= 0) {column = column.substring(indexOf + 1, indexOf2);if (StrUtil.isBlank(column)) {return null;}}JimuReportDataColumnDTO jimuReportDataColumnDTO = new JimuReportDataColumnDTO();jimuReportDataColumnDTO.setName(name);jimuReportDataColumnDTO.setColumn(column);jimuReportDataColumnDTO.setIndex(Integer.parseInt(key));return jimuReportDataColumnDTO;}).filter(Objects::nonNull).sorted(Comparator.comparing(JimuReportDataColumnDTO::getIndex)).collect(Collectors.toList());log.info("【下载中心】获取积木报表的头 reportId:{},heads:{}", reportId, heads);return heads;}/*** 从积木请求体中获取请求参数** @param json* @return*/@Overridepublic String getRequestParamFromJson(String json) {if (StrUtil.isNotBlank(json)) {JSONObject jsonObject = JSONObject.parseObject(json);if (jsonObject.containsKey("param")) {return jsonObject.getJSONObject("param").toJSONString();}return "{}";}return "{}";}}
数据跨库获取工具
服务接口
/*** @author: humorchen* date: 2023/12/19* description: 获取数据服务,直接SQL跨库拿数据**/
@DS("slave_1")
public interface IReportDataGetService<T> {/*** 执行SQL返回数据** @param sql* @return*/JSONObject getOne(String sql);/*** 执行SQL返回数据,数据封装到类cls对象里** @param sql* @param cls* @return*/T getOne(String sql, Class<T> cls);/*** 执行SQL返回数据** @param sql* @return*/JSONArray getList(String sql);/*** 执行SQL返回数据,数据封装到类cls对象里** @param sql* @param cls* @return*/List<T> getList(String sql, Class<T> cls);/*** 分页查询** @param sql* @param page* @param pageSize* @return*/JSONArray pageGetList(String sql, int page, int pageSize);/*** 分页查询** @param sql* @param page* @param pageSize* @return*/JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize);/*** 分页查询** @param sql* @param page* @param pageSize* @param cls* @return*/JimuPageDto<T> pageGetListForJimu(String sql, int page, int pageSize, Class<T> cls);/*** 计数** @param sql* @return*/long count(String sql);/*** 生成in语句** @param columnName* @param elements* @return string*/default String getColumnInSql(String columnName, List<String> elements) {StringBuilder sqlBuilder = new StringBuilder();sqlBuilder.append(" ");sqlBuilder.append(columnName);sqlBuilder.append(" in (");for (int i = 0; i < elements.size(); i++) {String id = elements.get(i);if (i > 0) {sqlBuilder.append(",");}sqlBuilder.append("'");sqlBuilder.append(id);sqlBuilder.append("'");}sqlBuilder.append(")");return sqlBuilder.toString();}}
服务实现
/*** @author: chenfuxing* date: 2023/12/19* description:**/
@Service
@Slf4j
public class ReportDataGetServiceImpl implements IReportDataGetService {@Autowiredprivate DataSource dataSource;/*** 执行SQL返回数据** @param sql* @return*/@Overridepublic JSONObject getOne(String sql) {JSONObject ret = null;Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = dataSource.getConnection();statement = connection.createStatement();logSql(sql);resultSet = statement.executeQuery(sql);if (resultSet != null) {while (resultSet.next()) {if (ret != null) {throw new RuntimeException("查询结果不止一条数据");}ret = new JSONObject();ResultSetMetaData metaData = resultSet.getMetaData();int columnCount = metaData.getColumnCount();for (int i = 1; i <= columnCount; i++) {String columnName = metaData.getColumnLabel(i);ret.put(columnName, resultSet.getObject(columnName));}}}} catch (Exception e) {log.error("获取数据报错", e);} finally {// 释放资源IoUtil.close(resultSet);IoUtil.close(statement);IoUtil.close(connection);}return ret;}/*** 执行SQL返回数据** @param sql* @return*/@Overridepublic JSONArray getList(String sql) {JSONArray ret = new JSONArray();Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = dataSource.getConnection();statement = connection.createStatement();logSql(sql);resultSet = statement.executeQuery(sql);if (resultSet != null) {while (resultSet.next()) {// 组装数据为json 对象JSONObject data = new JSONObject();ResultSetMetaData metaData = resultSet.getMetaData();int columnCount = metaData.getColumnCount();for (int i = 1; i <= columnCount; i++) {String columnName = metaData.getColumnLabel(i);data.put(columnName, resultSet.getObject(columnName));}ret.add(data);}}} catch (Exception e) {log.error("获取数据报错", e);} finally {// 释放资源IoUtil.close(resultSet);IoUtil.close(statement);IoUtil.close(connection);}return ret;}private void logSql(String sql) {int len = 5000;// 执行SQLlog.info("执行的SQL:{}", StrUtil.isNotBlank(sql) && sql.length() > len ? sql.substring(0, len) : sql);}/*** 计数** @param sql* @return*/@Overridepublic long count(String sql) {String countSQL = getCountSqlFromQuerySql(sql);if (StrUtil.isBlank(countSQL)) {throw new RuntimeException("计数语句不得为空,SQL为:" + sql);}long ret = 0;Connection connection = null;Statement statement = null;ResultSet resultSet = null;try {connection = dataSource.getConnection();statement = connection.createStatement();logSql(sql);resultSet = statement.executeQuery(countSQL);if (resultSet != null) {while (resultSet.next()) {ret = resultSet.getLong(1);}}} catch (Exception e) {log.error("获取数据报错", e);} finally {// 释放资源if (resultSet != null) {try {resultSet.close();} catch (Exception ignored) {}}if (statement != null) {try {statement.close();} catch (Exception ignored) {}}if (connection != null) {try {connection.close();} catch (Exception ignored) {}}}return ret;}/*** 从查询语句变计数语句** @param sql* @return*/public String getCountSqlFromQuerySql(String sql) {String selectStr = "select";int selectIndex = sql.indexOf(selectStr);int fromIndex = sql.indexOf("from");return sql.replace(sql.substring(selectIndex + selectStr.length(), fromIndex), " count(*) as c ");}/*** 分页查询** @param sql* @param page* @param pageSize* @return*/@Overridepublic JSONArray pageGetList(String sql, int page, int pageSize) {String querySql = getPageSqlFromQuerySql(sql, page, pageSize);if (StrUtil.isBlank(querySql)) {throw new RuntimeException("分页查询解析失败,SQL:" + sql + " 页号: " + page + " 每页数量:" + pageSize);}return getList(querySql);}/*** 分页查询** @param sql* @param page* @param pageSize* @return*/@Overridepublic JimuPageDto<JSONObject> pageGetListForJimu(String sql, int page, int pageSize) {JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();// 查countlong count = count(sql);long total = count / pageSize + (count % pageSize > 0 ? 1 : 0);log.info("数据总条数:{} 条,每页:{} 条,总页数:{} 页", count, pageSize, total);jimuPageDto.setTotal(total);// 查分页数据JSONArray data = pageGetList(sql, page, pageSize);List<JSONObject> dataList = new ArrayList<>(data.size());for (int i = 0; i < data.size(); i++) {JSONObject jsonObject = data.getJSONObject(i);dataList.add(jsonObject);}jimuPageDto.setData(dataList);jimuPageDto.setCount(count);return jimuPageDto;}/*** 分页查询** @param sql* @param page* @param pageSize* @return*/public String getPageSqlFromQuerySql(String sql, int page, int pageSize) {Assert.isTrue(page >= 1, () -> new IllegalArgumentException("page不得小于1"));Assert.isTrue(pageSize >= 1, () -> new IllegalArgumentException("pageSize不得小于1"));int skip = (page - 1) * pageSize;StringBuilder builder = new StringBuilder(sql);builder.append(" limit ");if (skip > 0) {builder.append(skip);builder.append(",");}builder.append(pageSize);String querySql = builder.toString();log.info("分页查询原SQL:{}\n分页SQL处理后:{}", sql, querySql);return querySql;}/*** 执行SQL返回数据,数据封装到类cls对象里** @param sql* @param cls* @return*/@Overridepublic Object getOne(String sql, Class cls) {return getOne(sql).toJavaObject(cls);}/*** 执行SQL返回数据,数据封装到类cls对象里** @param sql* @param cls* @return*/@Overridepublic List getList(String sql, Class cls) {return getList(sql).toJavaList(cls);}/*** 分页查询** @param sql* @param page* @param pageSize* @param cls* @return*/@Overridepublic JimuPageDto pageGetListForJimu(String sql, int page, int pageSize, Class cls) {JimuPageDto<JSONObject> jimuPageDto = pageGetListForJimu(sql, page, pageSize);JimuPageDto ret = new JimuPageDto<>();List list = new ArrayList(jimuPageDto.getData().size());for (int i = 0; i < jimuPageDto.getData().size(); i++) {list.add(jimuPageDto.getData().get(i).toJavaObject(cls));}ret.setData(list);ret.setTotal(jimuPageDto.getTotal());ret.setCount(jimuPageDto.getCount());return ret;}}
拦截切面
/*** @author: humorchen* date: 2024/1/15* description: 下载任务切面* 对加上了@UseDownloadTaskCenter注解的方法进行切面,使用下载任务中心代理掉,完成下载任务**/
@Aspect
@Component
@Slf4j
public class DownloadTaskAspect {@Autowiredprivate IDownloadTaskService downloadTaskService;/*** 注入spring 事件发布器*/@Autowiredprivate ApplicationEventPublisher eventPublisher;/*** 环绕通知** @return*/@Around("@annotation(cn.sffix.recovery.report.annotations.UseDownloadTaskCenter))")@Order(50)public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("【下载中心】进入下载中心切面");// 是下载中心发的请求则直接执行分页数据if (DownloadCenterUtil.isDownloadCenterRequest()) {log.info("【下载中心】下载中心发的请求,直接执行分页数据");return joinPoint.proceed();}// 识别下载请求int pageNo = 1;int pageSize = 20;Object[] args = joinPoint.getArgs();if (args != null && args.length > 0) {DownloadCenterBaseParam downloadCenterBaseParam = null;// 找到参数for (Object arg : args) {if (arg instanceof DownloadCenterBaseParam) {downloadCenterBaseParam = (DownloadCenterBaseParam) arg;break;}}// 检查参数if (downloadCenterBaseParam != null) {pageNo = Optional.ofNullable(downloadCenterBaseParam.getPageNo()).orElse(pageNo);pageSize = Optional.ofNullable(downloadCenterBaseParam.getPageSize()).orElse(pageSize);}log.info("【下载中心】下载中心切面,downloadCenterBaseParam:{}", downloadCenterBaseParam);if (downloadCenterBaseParam != null) {Object target = joinPoint.getTarget();Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();Class<?> returnType = method.getReturnType();// 返回值类型检查if (returnType.equals(JimuPageDto.class)) {// 如果是导出请求,则使用下载任务中心代理掉if (isExportFirstPageRequest(pageNo, pageSize)) {// 如果是导出第一页请求,则使用下载任务中心代理掉DownloadTask downloadTask = registerTask(downloadCenterBaseParam, target, method, args);if (downloadTask == null || downloadTask.getId() == null) {log.error("【下载中心】注册下载任务失败,任务信息:{}", downloadTask);return joinPoint.proceed();}log.info("【下载中心】注册下载任务成功,任务信息:{}", downloadTask);// 返回积木所需要的数据JimuPageDto<JSONObject> jimuPageDto = new JimuPageDto<>();jimuPageDto.setTotal(0);jimuPageDto.setCount(0);JSONObject jsonObject = new JSONObject();String downloadTaskJsonStr = downloadTask.getJson();DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTaskJsonStr, DownloadTaskJson.class);String requestBody = downloadTaskJson.getRequestBody();String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);log.info("【下载中心】reportHead:{}", reportHead);if (CollectionUtil.isNotEmpty(reportHead) && reportHead.size() > 1) {String column = reportHead.get(1).getColumn();jsonObject.put(column, "请前往报表中台-下载中心查看(任务ID " + downloadTask.getId() + ")");log.info("【下载中心】返回数据:{}", jsonObject);} else {log.info("【下载中心】返回数据为空");}List<JSONObject> list = Collections.singletonList(jsonObject);jimuPageDto.setData(list);eventPublisher.publishEvent(new DownloadTaskPublishEvent(downloadTask.getId()));return jimuPageDto;} else {log.info("【下载中心】不是导出请求,直接执行分页数据");}} else {log.error("【下载中心】返回值类型不是JimuPageDto,无法使用下载任务中心代理掉");}}}return joinPoint.proceed();}/*** 生成下载任务** @param downloadTaskParam* @return*/private DownloadTask registerTask(DownloadCenterBaseParam downloadTaskParam, Object proxyTarget, Method method, Object[] args) {UserInfo loginUser = UserInfoHolder.get();String account = loginUser.getAccount();HttpServletRequest currentRequest = RequestUtil.getCurrentRequest();String requestBody = DownloadCenterUtil.getRequestBodyFromHeader(currentRequest);// 防止10秒内重复点击if (!downloadTaskService.setSameTaskLock(account, requestBody)) {log.error("【下载中心】10秒内重复点击,不给再次注册下载任务");return null;}String title = "导出-" + DateUtil.now().replace(" ", "_") + ".xlsx";try {title = downloadTaskService.getReportNameByReportId(downloadTaskService.getReportIdFromRequestBody(requestBody)) + title;} catch (Exception e) {log.error("【下载中心】获取报表名称失败", e);}String url = generateFileUrl();DownloadTask downloadTask = new DownloadTask();downloadTask.setAccount(account);downloadTask.setTitle(title);downloadTask.setIcon("");downloadTask.setUrl(url);downloadTask.setFileSize("");downloadTask.setPercent("0%");downloadTask.setState(DownloadTaskStateEnum.WAIT.getState());DownloadTaskJson downloadTaskJson = new DownloadTaskJson();// 拷贝最开始请求积木的token和requestBody,执行下载任务时需要downloadTaskJson.setRequestToken(DownloadCenterUtil.getRequestTokenFromHeader(currentRequest));downloadTaskJson.setRequestBody(requestBody);downloadTaskJson.setProxyMethod(method.getName());if (args != null) {for (Object arg : args) {if (arg instanceof DownloadCenterBaseParam) {downloadTaskJson.setParam((DownloadCenterBaseParam) arg);break;}}}downloadTask.setJson(JSONObject.toJSONString(downloadTaskJson));downloadTask = downloadTaskService.registerTask(downloadTask);return downloadTask;}/*** 生成文件url地址** @return*/private String generateFileUrl() {// todo 生成文件url地址return "";}/*** 注入jm report分页大小*/@Value("${jeecg.jmreport.page-size-number:5000}")private int jmReportPageSizeNumber;/*** 判断是否为导出请求** @param pageNo* @param pageSize* @return*/private boolean isExportPageRequest(int pageNo, int pageSize) {return pageSize == jmReportPageSizeNumber;}/*** 判断是否为导出请求** @param pageNo* @param pageSize* @return*/private boolean isExportFirstPageRequest(int pageNo, int pageSize) {log.info("【下载中心】判断是否为导出请求 isExportFirstPageRequest pageNo:{},pageSize:{},积木报表导出size:{}", pageNo, pageSize, jmReportPageSizeNumber);return pageNo == 1 && isExportPageRequest(pageNo, pageSize);}
}
报表下载任务处理事件
暂时由于导出项目只部署一个实例,导出任务使用的是spring 本地event来异步处理的,你可以使用MQ、调度来执行掉任务,可以参考我这个
上传华为云OSS的部分你替换为你的OSS上传操作,这个地方我是复用项目里已有的存储上传服务
里面的异步线程池、环境工具类、积木导出每页大小要设置自己的
/*** @author: humorchen* date: 2024/1/16* description: 下载任务发布事件**/
@Getter
public class DownloadTaskPublishEvent extends ApplicationEvent {/*** 任务ID*/private final Integer taskId;public DownloadTaskPublishEvent(Integer taskId) {super(taskId);this.taskId = taskId;}}
处理器
/*** @author: humorchen* date: 2024/1/16* description: 处理下载任务发布事件**/
@Component
@Slf4j
public class DownloadTaskPublishEventHandler implements ApplicationListener<DownloadTaskPublishEvent> {@Autowiredprivate IDownloadTaskService downloadTaskService;@Autowiredprivate IReportService reportService;@Autowired@Qualifier(AsyncConfig.ASYNC_THREAD_POOL)private Executor executor;@Autowiredprivate HwProperties hwProperties;@Autowiredprivate EnvUtil envUtil;/*** 注入jm report分页大小*/@Value("${jeecg.jmreport.page-size-number:5000}")private int jmReportPageSizeNumber;@Autowiredprivate RedissonClient redissonClient;/*** 超时时间*/private static final int TIMEOUT_MILLS = 1000 * 60 * 20;@Autowiredprivate HwObjectStorageService hwObjectStorageService;/*** 上传文件到OSS的路径** @param account* @param fileName* @return*/private final StoragePath getStoragePath(String account, String fileName) {return StoragePath.path().addFolder("downloadTask").addFolder(account).fileName(fileName);}/*** Handle an application event.** @param event the event to respond to*/@Override@Async(AsyncConfig.ASYNC_THREAD_POOL)public void onApplicationEvent(DownloadTaskPublishEvent event) {Integer taskId = event.getTaskId();log.info("【下载中心】执行下载任务 taskId:{}", taskId);DownloadTask downloadTask = downloadTaskService.getById(taskId);if (downloadTask == null) {log.error("【下载中心】下载任务不存在,taskId:{}", taskId);return;}if (downloadTask.getState() == DownloadTaskStateEnum.RUNNING.getState()) {log.error("【下载中心】下载任务正在执行中,taskId:{}", taskId);return;}try {log.info("【下载中心】下载任务开始执行,taskId:{}", taskId);// 改状态到执行中DownloadTaskStateEnum downloadTaskStateEnum = Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT);int compareAndSwapTaskState = downloadTaskService.compareAndSwapTaskState(taskId, downloadTaskStateEnum, DownloadTaskStateEnum.RUNNING);if (compareAndSwapTaskState < 1) {log.info("【下载中心】下载任务状态不对,taskId:{}, state:{}", taskId, downloadTaskStateEnum);return;}DownloadTaskJson downloadTaskJson = JSONObject.parseObject(downloadTask.getJson(), DownloadTaskJson.class);// 获取数据String requestBody = downloadTaskJson.getRequestBody();String requestToken = downloadTaskJson.getRequestToken();String reportId = downloadTaskService.getReportIdFromRequestBody(requestBody);String reportName = downloadTaskService.getReportNameByReportId(reportId);String requestParam = downloadTaskService.getRequestParamFromJson(downloadTask.getJson());JimuReportDataSourceDTO dataSourceDTO = downloadTaskService.getReportApiOrSqlByReportId(reportId);List<JimuReportDataColumnDTO> reportHead = downloadTaskService.getReportHead(reportId);// 打印上面拿到的数据log.info("reportId :{} \n reportName:{} \n requestParam:{} \n requestBody:{} \n dataSourceDTO:{} \n reportHead:{}", reportId, reportName, requestParam, requestBody, dataSourceDTO, reportHead);JimuReportDynamicEasyExcelImpl jimuReportDynamicEasyExcel = new JimuReportDynamicEasyExcelImpl(reportId, reportName, taskId, downloadTaskService, requestParam, requestToken, dataSourceDTO, reportHead);// 生成excel文件List<List<String>> head = reportHead.stream().map(d -> Collections.singletonList(d.getName())).collect(Collectors.toList());// 分页写数据InputStream inputStream = DynamicColumnEasyExcelUtil.writePageData(head, jimuReportDynamicEasyExcel, jmReportPageSizeNumber);// 上传excel文件到ossStoragePath storagePath = getStoragePath(downloadTask.getAccount(), downloadTask.getTitle());downloadTask.setPercent("100%");StopWatch stopWatch = new StopWatch();stopWatch.start();URI uri = hwObjectStorageService.savePublicFile(inputStream, storagePath);stopWatch.stop();log.info("【下载中心】上传文件到OSS,耗时:{} ms,uri:{}", stopWatch.getLastTaskTimeMillis(), uri);// 更新任务信息String url = getUrlPrefix() + uri.getPath();downloadTask.setUrl(url);downloadTask.setState(DownloadTaskStateEnum.SUCCESS.getState());log.info("【下载中心】下载任务成功,taskId:{},task:{}", taskId, downloadTask);boolean updated = downloadTaskService.updateById(downloadTask);log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);} catch (Exception e) {log.error("【下载中心】下载任务执行失败", e);// 更新任务信息downloadTask.setState(DownloadTaskStateEnum.FAILED.getState());downloadTask.setError("【下载中心】执行失败(" + e.getMessage() + ")");boolean updated = downloadTaskService.updateById(downloadTask);log.info("【下载中心】下载任务更新结果,taskId:{}, updated:{}", taskId, updated);} finally {log.info("【下载中心】下载任务 {} 执行完毕", taskId);}}/*** 根据环境获取文件url前缀** @return*/private String getUrlPrefix() {String envCode = envUtil.isPreOrProdEnv() ? "pro" : "test";String prefix = "https://test-obs.xxxxx.com";for (String key : hwProperties.getUrlMap().keySet()) {if (key.contains(envCode)) {prefix = hwProperties.getUrlMap().get(key);}}return prefix;}}
透传信息
主要是在这个customApiHeader() 方法里DownloadCenterUtil.copyDownloadCenterHeader(request, header);自定义获取数据请求的header这,读取这次积木导出请求的参数信息,写到获取数据请求的header里去
/*** 自定义积木报表鉴权(如果不进行自定义,则所有请求不做权限控制)* 1.自定义获取登录token* 2.自定义获取登录用户*/
@Slf4j
@Component
public class JimuReportTokenService implements JmReportTokenServiceI {@Autowired@Lazyprivate UserAutenticationFeign userAutenticationFeign;@Autowired@Lazyprivate UserInfoFeign userInfoFeign;/*** 通过请求获取Token* @param request* @return*/@Overridepublic String getToken(HttpServletRequest request) {String token = request.getParameter("token");if (token == null) {token = request.getHeader("X-Access-Token");}if (token == null) {token = request.getHeader("token");}if (token == null) {token = request.getHeader("Token");}return token;}/*** 通过Token获取登录人用户名* @param token* @return*/@Overridepublic String getUsername(String token) {UserTokenDTO tokenInfo = new UserTokenDTO();tokenInfo.setToken(token);Result<CustomerInfoDTO> customerInfoDTOResult = userInfoFeign.customerInfo(tokenInfo);CustomerInfoDTO data = customerInfoDTOResult.getData();if(data != null){FxUserInfoDTO userInfo = JSONObject.parseObject(JSONObject.toJSONString(data.getBase()), FxUserInfoDTO.class);if(userInfo == null){throw new RuntimeException("找不到相应平台用户信息");} else {// 写到上下文UserInfo user = new UserInfo();BeanUtils.copyProperties(userInfo, user);UserInfoHolder.set(user);log.info("成功将用户信息写入上下文");}if(userInfo.getClientType() != PlatformTypeEnum.fx.name()){return userInfo.getName();}else{throw new RuntimeException("平台类型不支持");}}else {throw new RuntimeException("用户不存在");}}/*** 自定义用户拥有的角色** @param token* @return*/@Overridepublic String[] getRoles(String token) {return new String[]{"admin"};}/*** Token校验* @param token* @return*/@Overridepublic Boolean verifyToken(String token) {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = requestAttributes.getRequest();request.setAttribute(UserAccountConstant.TOKEN,token);try {UserTokenDTO tokenInfo = new UserTokenDTO();tokenInfo.setToken(token);Result<CustomerInfoDTO> customerInfoDTOResult = userInfoFeign.customerInfo(tokenInfo);return customerInfoDTOResult.getData() != null;}catch (Exception e){log.error("校验Token异常:" + e.getMessage());return false;}}/*** 自定义请求头* @return*/@Overridepublic HttpHeaders customApiHeader() {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpHeaders header = new HttpHeaders();header.add("custom-header1", "Please set a custom value 1");if (requestAttributes != null) {HttpServletRequest request = requestAttributes.getRequest();header.set("authorization", getToken(request));header.set("token", getToken(request));// 拷贝请求过去DownloadCenterUtil.copyDownloadCenterHeader(request, header);// 如果是下载中心发起的请求,设置请求头if (DownloadCenterUtil.isDownloadCenterRequest(requestAttributes.getRequest())) {DownloadCenterUtil.setDownloadCenterHeaderRequest(header);}}return header;}
}
下载中心工具类
/*** @author: humorchen* date: 2024/1/18* description: 下载中心工具类**/
@Slf4j
public class DownloadCenterUtil {private static final String DOWNLOAD_CENTER_HEADER_REQUEST = "download-center-request";/*** 是否为下载中心发起的请求** @return*/public static boolean isDownloadCenterRequest() {ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes == null) {log.debug("DownloadCenterUtil#isDownloadCenterRequest requestAttributes is null");return false;}return isDownloadCenterRequest(requestAttributes.getRequest());}/*** 是否为下载中心发起的请求** @param servletRequest* @return*/public static boolean isDownloadCenterRequest(HttpServletRequest servletRequest) {return servletRequest != null && StrUtil.isNotBlank(servletRequest.getHeader(DOWNLOAD_CENTER_HEADER_REQUEST));}/*** 设置下载中心请求头** @param headers*/public static void setDownloadCenterHeaderRequest(HttpHeaders headers) {if (headers != null) {headers.set(DOWNLOAD_CENTER_HEADER_REQUEST, "true");}}/*** 复制下载中心请求头** @param request* @param headers*/public static void copyDownloadCenterHeader(HttpServletRequest request, HttpHeaders headers) {if (request == null || headers == null) {return;}// 复制request请求里的Token请求头String token = request.getHeader("Token");if (StrUtil.isNotBlank(token)) {headers.set(DownloadTaskJson.Fields.requestToken, token);}// 复制request请求里的请求体headers.set(DownloadTaskJson.Fields.requestBody, RequestUtil.getRequestBody(request));}/*** 获取下载请求头token** @param request* @return*/public static String getRequestTokenFromHeader(HttpServletRequest request) {if (request == null) {return null;}return request.getHeader(DownloadTaskJson.Fields.requestToken);}/*** 获取下载请求头** @param request* @return*/public static String getRequestBodyFromHeader(HttpServletRequest request) {if (request == null) {return null;}return request.getHeader(DownloadTaskJson.Fields.requestBody);}/*** 设置下载中心请求头** @param request*/public static void setDownloadCenterHeaderRequest(HttpRequest request) {if (request != null) {request.header(DOWNLOAD_CENTER_HEADER_REQUEST, "true");}}/*** 获取带参数的url** @param url* @param params* @return*/public static String getUrlWithParams(String url, JSONObject params) {if (StrUtil.isBlank(url) || params == null) {return url;}StringBuilder sb = new StringBuilder(url);if (url.contains("?")) {sb.append("&");} else {sb.append("?");}for (String key : params.keySet()) {sb.append(key).append("=").append(params.getString(key)).append("&");}return sb.substring(0, sb.length() - 1);}}
异步线程池
/*** @author: humorchen* date: 2024/1/16* description: 异步配置**/
@Slf4j
@Configuration
public class AsyncConfig {public static final String ASYNC_THREAD_POOL = "asyncThreadPool";/*** 异步线程池*/@Bean(name = ASYNC_THREAD_POOL)public Executor asyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(8);executor.setMaxPoolSize(64);executor.setQueueCapacity(128);executor.setThreadNamePrefix("asyncThreadPool-");executor.initialize();return executor;}
}
给前端暴露接口
/*** @author humorchen* date: 2024/2/28* description: 下载中心API**/@RequestMapping("/report/form/downloadCenter")
public interface DownloadCenterApi {/*** 分页查下载任务** @param pageListDownloadTaskDto* @return*/@RequestMapping("/pageListDownloadTask")Result<IPage<DownloadTaskVo>> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto);/*** 删除下载任务** @param taskId* @return*/@RequestMapping("/deleteTask")Result<String> deleteTask(Integer taskId);/*** 重新执行下载任务** @param taskId* @return*/@RequestMapping("/rerunTask")Result<String> rerunTask(Integer taskId);
}
/*** @author humorchen* date: 2024/2/28* description: 下载中心**/
@RestController
public class DownloadCenterController implements DownloadCenterApi {@Autowiredprivate IDownloadTaskService downloadTaskService;/*** 分页查下载任务** @param pageListDownloadTaskDto* @return*/@Overridepublic Result<IPage<DownloadTaskVo>> pageListDownloadTask(PageListDownloadTaskDto pageListDownloadTaskDto) {IPage<DownloadTask> downloadTaskPages = downloadTaskService.pageListDownloadTask(pageListDownloadTaskDto);Page<DownloadTaskVo> downloadTaskVoPage = new Page<>();downloadTaskVoPage.setCurrent(downloadTaskPages.getCurrent());downloadTaskVoPage.setPages(downloadTaskPages.getPages());downloadTaskVoPage.setSize(downloadTaskPages.getSize());downloadTaskVoPage.setTotal(downloadTaskPages.getTotal());List<DownloadTaskVo> downloadTaskVos = downloadTaskPages.getRecords().stream().map(downloadTask -> {DownloadTaskVo downloadTaskVo = BeanUtils.convert(downloadTask, DownloadTaskVo.class);downloadTaskVo.setStateStr(Optional.ofNullable(DownloadTaskStateEnum.of(downloadTask.getState())).orElse(DownloadTaskStateEnum.WAIT).getTitle());return downloadTaskVo;}).collect(Collectors.toList());downloadTaskVoPage.setRecords(downloadTaskVos);return Result.ok(downloadTaskVoPage);}/*** 删除下载任务** @param taskId* @return*/@Overridepublic Result<String> deleteTask(Integer taskId) {boolean removeById = downloadTaskService.removeById(taskId);return removeById ? Result.ok("删除成功") : Result.fail("未找到该任务,请刷新后重试)");}/*** 重新执行下载任务** @param taskId* @return*/@Overridepublic Result<String> rerunTask(Integer taskId) {return downloadTaskService.rerunTask(taskId);}
}
DynamicColumnEasyExcelUtil 动态字段EasyExcel工具
/*** @author humorchen* date: 2024/3/5* description: 动态列easyexcel工具类**/
@Slf4j
public class DynamicColumnEasyExcelUtil {public static final String ROW = "=row()";public static interface DynamicColumnEasyExcelInterface<T> {/*** 分页获取数据** @param page* @param size* @return*/JimuPageDto<T> pageGetData(int page, int size);/*** 数据对象转换为字符串** @param t* @return*/List<String> mapDataToStringList(T t);/*** 分页获取数据加载第i页时触发函数,用于实现进度变更** @param pageNo* @param pageSize*/void onLoadedPage(int pageNo, int pageSize, int pages);}/*** 从数据库分页读数据并写入成Excel文件,把文件内容写到输出流** @param head* @param dynamicColumnEasyExcelInterface* @param pageSize* @param <T>* @return*/public static <T> ByteArrayInputStream writePageData(List<List<String>> head, DynamicColumnEasyExcelInterface<T> dynamicColumnEasyExcelInterface, int pageSize) {ByteArrayOutputStream outputStream = new ByteArrayOutputStream();ExcelWriter excelWriter = EasyExcel.write(outputStream).head(head).build();int currentPage = 1;long pages = 1;int index = 1;StopWatch stopWatch = new StopWatch("报表分页写入Excel");WriteSheet sheet = EasyExcel.writerSheet("sheet" + currentPage).build();do {// 加载数据stopWatch.start("加载第" + currentPage + "页数据");JimuPageDto<T> jimuPageDto = dynamicColumnEasyExcelInterface.pageGetData(currentPage, pageSize);stopWatch.stop();// 数据判空List<T> records = jimuPageDto.getData();if (CollectionUtil.isEmpty(records)) {break;}// 转换数据stopWatch.start("转换第" + currentPage + "页数据");List<List<String>> data = records.stream().map(dynamicColumnEasyExcelInterface::mapDataToStringList).collect(Collectors.toList());stopWatch.stop();// 处理序号 row()if (CollectionUtil.isNotEmpty(data) && CollectionUtil.isNotEmpty(data.get(0)) && ROW.equals(data.get(0).get(0))) {for (List<String> stringList : data) {if (CollectionUtil.isNotEmpty(stringList) && ROW.equals(stringList.get(0))) {stringList.set(0, String.valueOf(index));++index;}}}// 写入数据stopWatch.start("写入第" + currentPage + "页数据");excelWriter.write(data, sheet);stopWatch.stop();pages = jimuPageDto.getTotal();// 更新进度dynamicColumnEasyExcelInterface.onLoadedPage(currentPage, pageSize, (int) pages);log.info("【下载中心】 分页获取数据,第{}页,总页数:{} 第一行数据是:{}", currentPage, pages, data.get(0));// 自增currentPage++;} while (currentPage <= pages);log.info("【下载中心】 耗时打印");for (StopWatch.TaskInfo taskInfo : stopWatch.getTaskInfo()) {log.info("【下载中心】 {} 耗时:{} ms", taskInfo.getTaskName(), taskInfo.getTimeMillis());}excelWriter.finish();return new ByteArrayInputStream(outputStream.toByteArray());}/*** 获取字段宽度策略** @return*/private AbstractHeadColumnWidthStyleStrategy getAbstractColumnWidthStyleStrategy() {return new AbstractHeadColumnWidthStyleStrategy() {/*** Returns the column width corresponding to each column head.** <p>* if return null, ignore** @param head Nullable.* @param columnIndex Not null.* @return*/@Overrideprotected Integer columnWidth(Head head, Integer columnIndex) {return null;}};}}