适配任何数据结构的异步Excel生成(企业级开发)

2023-11-12


前言

背景: 由于公司的excel生成过于缓慢,有时生成一个excel文件需要等待几十秒甚至几分钟,在等待的时候用户不能跳转其他页面,用户体验不好,可以先看一下,公司excel的下载流程图
在这里插入图片描述
可以发现问题所在,在查询数据的时候可能会有比较长的时间等待以及excel的生成都需要一定的时间,且数据结构单一,对不同的表单和报表等都需要独立出代码,代码的复用不高,所以进行如下优化。
先看优化后的流程图:
在这里插入图片描述

需求:用户选择需要下载的excel数据,会把选择的数据id发送给后台,后台有个解析数据的服务会把请求下载的数据,转化为sql,发送给异步下载excel,excel接收到sql并加入到线程池,返回通知给前端,正在下载的信息,用户便可在下载任务查看下载情况,在重新点击下载。下面便会分享的是异步下载excel的具体过程,至于服务转发就不在这里阐述了

源码会放在最后

一、Java操作Excel的基础知识

可以看这篇文章:https://blog.csdn.net/weixin_45537947/article/details/122521270

二、测试准备

模拟测试的数据:

CREATE TABLE `user`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `age` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `sex` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `creat_time` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

创建下载任务表:

CREATE TABLE `record`  (
  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `account` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `status` int(0) NULL DEFAULT NULL COMMENT '0下载中,1下载完成',
  `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `creation_time` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `file_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

三、实现源码

AsyncExcelMapper.java:

/**
 * 异步下载excel业务
 *
 * @author : 一个爱运动的程序员
 */
@Mapper
@Repository
public interface AsyncExcelMapper extends BaseMapper<Record> {

    @Select(" ${querySql} ")
    List<Map<String, Object>> queryList(@Param("querySql") String querySQL);
}

IAsyncExcelService.java:

/**
 * 异步下载excel业务
 *
 * @author : 一个爱运动的程序员
 */
public interface IAsyncExcelService  extends IService<Record> {


    /**
     * 查询下载成功的任务
     * @param account
     * @return
     */
    List<Record> findDownloadTask(String account);

    /**
     * 通过文件码下载文件
     * @param response
     * @param id
     */
    void streamDownloadFile(HttpServletResponse response, String id);

    /**
     * 添加下载任务-SQL
     * @param username
     * @param fileName
     * @param querySQL
     */
    String addSQLDownloadTask(String username, String fileName, String querySQL);
}

AsyncExcelServiceImpl.java:

/**
 * 异步下载excel业务
 *
 * @author : 一个爱运动的程序员
 */
@Service
public class AsyncExcelServiceImpl extends ServiceImpl<AsyncExcelMapper, Record> implements IAsyncExcelService {

    private final static Logger logger = LoggerFactory.getLogger(AsyncExcelServiceImpl.class);

    @Autowired
    AsyncExcelMapper asyncExcelMapper;


    /**
     * 创建等待队列
     */
    BlockingQueue<Runnable> bq = new ArrayBlockingQueue<Runnable>(20);

    /**
     * 创建线程池,池中保存的线程数为3,允许的最大线程数为5
     * keepAliveTime:当线程数大于核心数时,该参数为所有的任务终止前,多余的空闲线程等待新任务的最长时间
     * unit:等待时间的单位
     * workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务
     */
    ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 50, TimeUnit.MILLISECONDS, bq);


    /**
     * 查询下载成功的任务
     *
     * @param account
     * @return
     */
    @Override
    public List<Record> findDownloadTask(String account) {
        return asyncExcelMapper.selectList(new QueryWrapper<Record>().eq("status", 1).eq("account", account).orderByDesc("creation_time"));
    }

    /**
     * 通过文件码下载文件
     *
     * @param response
     * @param id
     */
    @Override
    public void streamDownloadFile(HttpServletResponse response, String id) {
        try {
            Record record = asyncExcelMapper.selectById(id);
            File file = new File(record.getUrl());
            String filename = file.getName();
            // 以流的形式下载文件。
            InputStream fis = new BufferedInputStream(new FileInputStream(record.getUrl()));
            byte[] buffer = new byte[fis.available()];
            fis.read(buffer);
            fis.close();
            // 清空response
            response.reset();
            response.setContentType("application/octet-stream;charset=UTF-8");
            String fileName = new String(filename.getBytes("gb2312"), "iso8859-1");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName);
            OutputStream ouputStream = response.getOutputStream();
            ouputStream.write(buffer);
            ouputStream.flush();
            ouputStream.close();
        } catch (Exception e) {
            throw new RuntimeException(String.format("文件下载出现异常: %s", e.getMessage()));
        }
    }

    /**
     * 添加下载任务-SQL
     *
     * @param username
     * @param fileName
     * @param querySQL
     */
    @Override
    public String addSQLDownloadTask(String username, String fileName, String querySQL) {
        // 在任务表中新建下载任务
        Record record = new Record();
        String taskId = username + System.currentTimeMillis();
        record.setId(taskId);
        record.setAccount(username);
        record.setStatus(0);
        record.setCreationTime(String.valueOf(new Date()));
        record.setFileName(fileName);
        asyncExcelMapper.insert(record);
        try {
            pool.execute(() -> {
                try {
                    Thread.sleep(5000);
                    List<Map<String, Object>> mapList = asyncExcelMapper.queryList(querySQL);
                    // 未查询到数据,删除任务表中正在下载的任务记录
                    if (mapList.isEmpty() || mapList == null) {
                        asyncExcelMapper.deleteById(taskId);
                        logger.info(taskId + " 未查询到数据,无法下载");
                    } else {    // 查询到数据生成Excel表
                        String property = System.getProperty("user.dir") + "\\src\\main\\resources\\excel\\";
                        String fileUrl = property + taskId + fileName + ".xlsx";
                        List<List<Object>> lists = new ArrayList<List<Object>>();
                        for (Map<String, Object> m : mapList) {
                            List<Object> data = new ArrayList<Object>();
                            for (Map.Entry<String, Object> entry : m.entrySet()) {
                                data.add(entry.getValue());
                            }
                            lists.add(data);
                        }
                        EasyExcel.write(fileUrl)
                                // 这里放入动态头
                                .head(head(mapList)).sheet(fileName)
                                // 当然这里数据也可以用 List<List<String>> 去传入
                                .doWrite(lists);
                        Record r = new Record();
                        r.setId(taskId);
                        r.setStatus(1);
                        r.setUrl(fileUrl);
                        asyncExcelMapper.updateById(r);
                        logger.info(taskId + fileName + "下载完成");
                    }
                } catch (Exception e) {
                    asyncExcelMapper.deleteById(taskId);
                    throw new RuntimeException(String.format("线程池下载任务出现异常: %s", e.getMessage()));
                }
            });
        } catch (Exception e) {
            asyncExcelMapper.deleteById(taskId);
            throw new RuntimeException(String.format("线程池等待队列已满,无法添加新的下载任务: %s", e.getMessage()));
        }
        return "已加入下载任务";
    }

    /**
     * 获取行信息
     * @param list
     * @return
     */
    private List<List<String>> head(List<Map<String, Object>> list) {
        List<List<String>> lists = new ArrayList<List<String>>();
        for (Map<String, Object> m : list) {
            for (Map.Entry<String, Object> entry : m.entrySet()) {
                List<String> l = new ArrayList<>();
                String mapKey = entry.getKey();
                l.add(mapKey);
                lists.add(l);
            }
            break;
        }
        return lists;
    }
}

因为需要被全局调用,所以调用采用了单例的设计模式:AsyncExcelSingleton.java:

/**
 * 异步下载excel的全局单例调用类
 *
 * @author : 一个爱运动的程序员
 */
@Slf4j
public class AsyncExcelSingleton {

    private static volatile AsyncExcelSingleton INSTANCE;

    /**
     * 加上 ForTool 后缀来和之前两种方式创建的对象作区分。
     */
    private IAsyncExcelService iAsyncExcelService;

    private AsyncExcelSingleton() {
        iAsyncExcelService = SpringContextUtils.getBean(IAsyncExcelService.class);
    }


    public static AsyncExcelSingleton getInstance() {
        if (null == INSTANCE) {
            synchronized (AsyncExcelSingleton.class) {
                if (null == INSTANCE) {
                    INSTANCE = new AsyncExcelSingleton();
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 使用 SpringContextUtils 获取的 UserService 对象,并从 UserDao 中获取数据
     */
    /**
     * 查询下载成功的任务
     * @param account
     * @return
     */
    public List<Record> findDownloadTask(String account) {
        if (null == iAsyncExcelService) {
            log.debug("AsyncExcelSingleton iAsyncExcelService findDownloadTask is null");
            throw new RuntimeException(String.format("AsyncExcelSingleton iAsyncExcelService findDownloadTask is null"));
        }
        return iAsyncExcelService.findDownloadTask(account);
    }

    /**
     * 通过文件码下载文件
     * @param response
     * @param id
     */
    public void streamDownloadFile(HttpServletResponse response, String id) {
        if (null == iAsyncExcelService) {
            log.debug("AsyncExcelSingleton iAsyncExcelService streamDownloadFile is null");
            throw new RuntimeException(String.format("AsyncExcelSingleton iAsyncExcelService streamDownloadFile is null"));
        }
        iAsyncExcelService.streamDownloadFile(response, id);
    }

    /**
     * 添加下载任务-SQL
     *
     * @param username
     * @param fileName
     * @param querySQL
     */
    public String addSQLDownloadTask(String username, String fileName, String querySQL) {
        if (null == iAsyncExcelService) {
            log.debug("AsyncExcelSingleton iAsyncExcelService addSQLDownloadTask is null");
            throw new RuntimeException(String.format("AsyncExcelSingleton iAsyncExcelService addSQLDownloadTask is null"));
        }
        return iAsyncExcelService.addSQLDownloadTask(username, fileName, querySQL);
    }
}

AsyncExcelController.java:
注意: 由于我是模拟测试,一般除登录外需要传输用户名外,其他时候我们一般不会去传输用户的信息,要获取用户信息直接cookit、session、token中获取就可以了

/**
 * 异步下载excel的控制层
 *
 * @author : 一个爱运动的程序员
 */

@RestController
@RequestMapping("/async/excel")
public class AsyncExcelController {

    @GetMapping("/findDownloadTask")
    public R<List<Record>> findDownloadTask(String username) {
        List<Record> list = AsyncExcelSingleton.getInstance().findDownloadTask(username);
        return R.ok(list);
    }

    @GetMapping("/streamDownloadFile")
    public void streamDownloadFile(HttpServletResponse response, String id) {
        AsyncExcelSingleton.getInstance().streamDownloadFile(response, id);
    }

    @GetMapping("/addSQLDownloadTask")
    public R<String> addSQLDownloadTask(String username, String fileName, String querySQL) {
        String s = AsyncExcelSingleton.getInstance().addSQLDownloadTask(username, fileName, querySQL);
        return R.ok(s);
    }
}

四、功能测试

1、添加下载任务
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2、查询下载任务
在这里插入图片描述
3、下载到本地
在这里插入图片描述
在这里插入图片描述

总结

以上的内容便是此处分享的内容,希望对你有帮助,当然如果你有更好的实现点子或者我的实现有不好的地方欢迎评论区指出,谢谢*✧⁺˚⁺ପ(๑・ω・)੭ु⁾⁾ 好好学习天天向上

源码

GitHub: https://github.com/XIN007-C/async-excel

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

适配任何数据结构的异步Excel生成(企业级开发) 的相关文章

  • Guice 忽略注入构造函数参数上的 @Nullable

    我正在使用 Guice v 3 0 并且有一个值被注入到构造函数中 该值可以为 null 因此我在构造函数中使用 Nullable 来自 javax annotations 注释了该参数 public MyClass Parameter1
  • 获取文件的锁

    我想在对特定文件开始 threo read 时获取文件上的锁定 以便其他应用程序无法读取已锁定的文件并希望在线程终止时释放锁定文件 您可以获得一个FileLock https docs oracle com javase 8 docs ap
  • 带有 Android 支持库 v7 的 Maven Android 插件

    我使用 maven android plugin 构建我的 android 应用程序 它依赖于 android 支持库 v4 和 v7 由于我没有找到如何从developer android com下载整个sdk 因此我无法使用maven
  • 如何使用 JAVA 代码以编程方式捕获线程转储?

    我想通过 java 代码生成线程转储 我尝试使用 ThreadMXBean 为此 但我没有以正确的格式获得线程转储 因为我们正在使用jstack命令 请任何人提供一些帮助 他们是否有其他方式获取线程转储 使用任何其他 API 我想要的线程转
  • HAProxy SSL终止+客户端证书验证+curl/java客户端

    我希望使用我自己的自签名证书在 HAProxy 上进行 SSL 终止 并使用我创建的客户端证书验证客户端访问 我通过以下方式创建服务器 也是 CA 证书 openssl genrsa out ca key 1024 openssl req
  • 在 Struts 2 中传递 URL 参数而不使用查询字符串

    我想使用类似的 URL host ActionName 123 abc 而不是像这样传递查询字符串 host ActionName parm1 123 parm2 abc 我怎样才能在 Struts 2 中做到这一点 我按照下面的方法做了
  • 是否可以从 servlet 内部以编程方式设置请求上下文路径?

    这是一个特殊情况 我陷入了处理 企业 网络应用程序的困境 企业应用程序正在调用request getContext 并将其与另一个字符串进行比较 我发现我可以使用 getServletContext getContextPath 获取 se
  • 在 Java 中通过 XSLT 分解 XML

    我需要转换具有嵌套 分层 表单结构的大型 XML 文件
  • 寻找局部最小值

    下面的代码正确地找到了数组的局部最大值 但未能找到局部最小值 我已经进行了网络搜索 以找到找到最小值的最佳方法 并且根据这些搜索 我认为我正在使用下面的正确方法 但是 在几天的时间里多次检查每一行之后 下面的代码中有一些我仍然没有看到的错误
  • 在 Clojure 中解压缩 zlib 流

    我有一个二进制文件 其内容由zlib compress在Python上 有没有一种简单的方法可以在Clojure中打开和解压缩它 import zlib import json with open data json zlib wb as
  • 无法在 Java/Apache HttpClient 中处理带有垂直/管道栏的 url

    例如 如果我想处理这个网址 post new HttpPost http testurl com lists lprocess action LoadList 401814 1 Java Apache 不允许我这么做 因为它说竖线 是非法的
  • Java - 从 XML 文件读取注释

    我必须从 XML 文件中提取注释 我找不到使用 JDOM 或其他东西来让它们使用的方法 目前我使用 Regex 和 FileReader 但我不认为这是正确的方法 您可以使用 JDOM 之类的东西从 XML 文件中获取注释吗 或者它仅限于元
  • 无需登录即可直接从 Alfresco 访问文件/内容

    我的场景是这样的 我有一个使用 ALFRESCO CMS 来显示文件或图像的 Web 应用程序 我正在做的是在 Java servlet 中使用用户名和密码登录 alfresco 并且我可以获得该登录的票证 但我无法使用该票证直接从浏览器访
  • 禁用 Android 菜单组

    我尝试使用以下代码禁用菜单组 但它不起作用 菜单项仍然启用 你能告诉我出了什么问题吗 资源 菜单 menu xml menu menu
  • JMS 中的 MessageListener 和 Consumer 有什么区别?

    我是新来的JMS 据我了解Consumers能够从队列 主题中挑选消息 那么为什么你需要一个MessageListener因为Consumers会知道他们什么时候收到消息吗 这样的实际用途是什么MessageListener 编辑 来自Me
  • 源值 1.5 的错误已过时,将在未来版本中删除

    我使用 scala maven plugin 来编译包含 scala 和 java 代码的项目 我已经将源和目标设置为1 7 但不知道为什么maven仍然使用1 5 这是我在 pom xml 中的插件
  • 使用 Java https 上传到 Imgur v3 错误

    我目前正在尝试使用他们当前的 API v3 上传到 imgur 但是我不断收到错误 错误 javax net ssl SSLException 证书中的主机名不匹配 api imgur com imgur com OR imgur com
  • 检查应用程序是否在 Android Market 上可用

    给定 Android 应用程序 ID 包名称 如何以编程方式检查该应用程序是否在 Android Market 上可用 例如 com rovio angrybirds 可用 而 com random app ibuilt 不可用 我计划从
  • 将对象从手机共享到 Android Wear

    我创建了一个应用程序 在此应用程序中 您拥有包含 2 个字符串 姓名和年龄 和一个位图 头像 的对象 所有内容都保存到 sqlite 数据库中 现在我希望可以在我的智能手表上访问这些对象 所以我想实现的是你可以去启动 启动应用程序并向左和向
  • try-with-resources 中出现死代码警告,但翻译后的 try-catch-finally 中没有出现死代码警告

    以下代码使用try 有资源 https docs oracle com javase specs jls se7 html jls 14 html jls 14 20 3Java 8 中引入的构造 偶尔抛出 方法被声明为抛出一个偶尔的异常

随机推荐

  • Java jackson配置类,Java jackson工具类,SpringBoot Jackson类配置

    Java jackson配置类 Java jackson工具类 SpringBoot Jackson类配置 Copyright 蕃薯耀 2021 04 27 https blog csdn net w995223851 一 SpringBo
  • 第十八章 Post-Processing

    第十八章 Post Processing Post processing是指在场景渲染之后 使用一些图形技术对场景进行处理 比如 把整个场景转换为grayscale 灰度 样式或使场景中明亮的区域发光 本章将编写一些post process
  • 项目开发的前期准备(二)

    项目开发的前期准备 二 时序图用于描述对象之间的传递消息的时间顺序 即用例中的行为顺序 当执行一个用例时 时序图中的每条消息对应了一个类操作或者引起转换的触发事件 在 UML 中 时序图表示为一个二维的关系图 其中 纵轴是时间轴 时间延竖线
  • Selenium clear()方法无法清掉数据

    今天遇到了Selenium clear 方法无法清掉数据的问题 问题描述 页面看着清空了数据 保存时候还是存在 解决方法 WebElement ballast browser getWebDriver findElement By xpat
  • Qt之界面样式

    1 窗口 最小化 最大化 关闭按钮 显示状态自定义 setWindowFlags Qt CustomizeWindowHint setWindowFlags Qt WindowCloseButtonHint 只要关闭按钮 setWindow
  • NeuralNLP-NeuralClassifier的使用记录(一),训练预测自己的【英文文本多分类】

    NeuralNLP NeuralClassifier的使用记录 训练预测自己的英文文本多分类 NeuralNLP NeuralClassifier是腾讯开发的一个多层多分类应用工具 支持的任务包括 文本分类中的二分类 多分类 多标签 以及层
  • 存档&改造【01】模板导入&租户登录

    app101改造之前 想搞一个设备导入功能 想象中 实际上 再改改样式 关于数据的导入导出 可见博客 APEX数据源加载实现Excel表数据导入及自定义存储过程 王小小鸭的博客 CSDN博客https blog csdn net clove
  • swagger免token鉴权

    使用swagger很简单 但是在引入项目里面是由于 项目使用了Spring Security OAuth实现鉴权体系 所以浏览器访问swagger的时候一直报401 说没权限 网上的很多方案主要是两种方法 1 鉴权时过滤指定请求 但是我没弄
  • JUC(2): 阻塞队列+线程池(重点)+新时代程序员必会

    一 阻塞队列 ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列 LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列 PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列 D
  • 数据库设计--三大范式

    1 第一范式 确保每列保持原子性 第一范式是最基本的范式 如果数据库表中的所有字段值都是不可分解的原子值 就说明该数据库表满足了第一范式 第一范式的合理遵循需要根据系统的实际需求来定 比如某些数据库系统中需要用到 地址 这个属性 本来直接将
  • Anaconda简单理解

    1 定义 Ancconda是一个开源的软件包管理系统和环境管理系统 用于安装多个版本的软件包及其依赖关系 并在它们之间轻松切换 2 最重要的作用 创建虚拟环境 方法 conda create n 虚拟环境名字 python 版本 可以在一台
  • 小程序报错:Invalid attempt to destructure non-iterable instance

    TypeError Invalid attempt to destructure non iterable instance In order to be iterable non array objects must have a Sym
  • Distributed Database System —— Mysql Binlog不止是主从同步

    文章目录 引入 Binlog应用场景 读写分离 数据恢复 保证数据最终一致性 异地多活 引入 Mysql 5 0以后 支持通过binary log 二进制日志 以支持主从复制 复制允许将来自一个MySQL数据库服务器 master 的数据复
  • Spring Boot 大型线上商城项目实战

    你会学到什么 Spring Boot 技术栈的基础使用和开发技巧 掌握 Spring Boot 项目实践 如果你在发愁毕业设计 这个项目也可以给你很多思路 Thymeleaf 模板引擎整合及运用 AdminLTE3 Bootstrap 4
  • windows10 搜索不起作用,搜索框按了没反应

    第一步在win10系统上按win R键打开运行 输入regedit 如下图所示 2 第二步打开注册表之后 点击HKEY LOCAL MACHINE 如下图所示 3 第三步然后依次点击 HKEY LOCAL MACHINE gt SOFTWA
  • 大数据——Java面向对象知识点总结

    面向对象 what when where why whom how 类与对象的关系 类是对象的抽象 对象是类实现 类是由属性和方法构成 它的每一个对象都有对应的属性和方法 方法 方法的重载 目的 针对同一个行为的多种表现 对应相同方法名的多
  • Linux发布Spring Boot项目

    文章目录 Linux发布Spring Boot项目 一 CentOS7虚拟机环境 1 清理后打包项目 2 上传jar到Linux中 3 查看IP 4 关闭防火墙 5 运行jar包 6 浏览器访问 二 真实服务器环境 4 需要开启端口 三 后
  • linux sed -i replace text/sed 跟expression替换文本

    1 生成测试文本 peng peng cat gt aa txt aa bb cc dd ee C 2 原本的方案 用vi替换文本 aa替换成abc s aa abc 3 用sed命令替换文本 replace aa with abc at
  • 交换两个变量的值(包括字符串的交换)

    例 交换两个变量的值 输入两个整型变量a和b 设计一个交换函数将其交换后再输出 注意 不能直接输出b和a 错误代码 include
  • 适配任何数据结构的异步Excel生成(企业级开发)

    文章目录 前言 一 Java操作Excel的基础知识 二 测试准备 三 实现源码 四 功能测试 总结 源码 前言 背景 由于公司的excel生成过于缓慢 有时生成一个excel文件需要等待几十秒甚至几分钟 在等待的时候用户不能跳转其他页面