数据转pdf(包含echarts图表)

需求描述:
公司有一些数据(查数据库、查接口),汇总后要体现到pdf上,方便转发查阅,数据的具体体现方式包含一些echarts渲染的柱状图以及图表,该pdf计划分为六个段落,每个段落有一部分根据数据汇总的文字展现,来纰漏需要注意什么和一些合理的建议。


实现方式:

准备工作:
    1、在服务器上安装Phantomjs软件
    2、公司前端同事,写好html模板
步骤:
    1、组装好数据,通过velocity,把数据填充到html模板上
    2、通过命令调用Phantomjs生成pdf
    3、打水印、上传文件服务器
需要注意的点:
    1、生成失败,重试策略
    2、服务器重启,还未生成的pdf,服务器启动重试
    3、如果多台server,重启后资源分配问题,例如:重复
    4、Phantomjs生成pdf时间较长,如何异步处理请求等

我的实现:
流程图:

总图:
graph TD
    A[后台接口开始] -->|放入数据队列| B(数据库队列)
    B --> C(单线程加锁从队列获取请求数据30毫秒一次)
    C --> |组建任务投递到执行线程池|D(主任务执行线程池50个线程)
    D --> E[task1]
    D --> F[task2]
    D --> H[task...]
    E --> I{执行是否成功}
    F--> I
    H-->I
    I -->|失败次数<3|B
    I -->|成功或者失败次数>3|J(更改数据库状态以及其他信息等)
    J-->K(流程结束)
task细节流程图:
graph TD
    A[开始] -->|多线程组建数据| B(获取数据线程池)
    B --> C[数据1]
    B --> D[数据2]
    B --> E[数据...]
    C -->F(数据汇总)
    D-->F
    E-->F
    F-->|velocity|G(生成html)
    G-->|Phantomjs,根据页面复杂程度,设置渲染时间|H(生成pdf)
    H-->I(添加水印)
    I-->J(上传文件系统)
    J-->K(删除系统上的文件,html\pdf等)
    K-->L(修改数据库状态以及文件地址)
    L-->M(流程结束)

细节描述:
由于数据分为月维度和周维度,将来可能会增加年维度,所以采用策略模式来获取数据,数据又有好多个获取渠道,又都为io操作,为了提高性能,所以创建单独的数据获取线程池,线程数目为100(根据具体的使用情况调整),不同的数据源通过future的方式去获取,最后汇总为报告主体数据,报告主体数据需要进行一系列的处理(例如:统一处理小数的点位、要根据数据计算消耗率、每个段落都要根据该段落的数据进行建议文本的填充),在这里我抽象出了两个接口fillTips、dataHandle,多个段落通过实现fillTips来进行填充建议文本,其他的数据处理通过实现dataHandle,来实现整体数据的处理,在这里利用责任链模式,每一个处理类只负责自己责任的那一部分,来解耦,如果之后需要扩展,直接增加实现类就可以,完成后调用velocity获取填充后的html文本,把文本输出到文件上以便于进行后续的操作。


具体代码展示:

  • 线程池定义以及任务提交
   public static final String RECORD_NAME_PREFIX = "%s_%s_%s_%s.pdf";
    private static ThreadPoolExecutor GEN_THREAD_POOL = new ThreadPoolExecutor(50, 50, 0L,
            TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(50000), new ThreadFactoryBuilder().setNameFormat("task-genPdf-%d").build(),
            new ThreadPoolExecutor.CallerRunsPolicy());

    /**
     * 生成任务
     *
     * @param req
     * @param user
     */
    public void submitGenPdfTask(PdfReportDTO req, User user) throws Exception {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        logger.info("生成pdf任务,traceId:[{}],param:[{}],user:[{}]", traceId, JSONObject.toJSONString(req), JSONObject.toJSONString(user));
        String fileName = String.format(RECORD_NAME_PREFIX, req.getCustId(), req.getCustName(), WEEK_EXPORT_TYPE.equals(req.getExportType()) ? "周报" : "月报", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
        Long recordId = reportsExportDao.saveExportRecord(Long.valueOf(req.getCustId()), fileName, (long) user.getEmpId());
        GEN_THREAD_POOL.submit(new GenWorker(traceId, recordId, req, 0, fileName, user.getLoginName()));
    }
  • 任务worker
    /**
     * 生成pdf的worker
     */
    class GenWorker implements Runnable {

        /**
         * traceId
         **/
        private String traceId;

        /**
         * 入库的id
         */
        private Long recordId;

        /**
         * 请求实体
         */
        private PdfReportDTO reportDTO;

        /**
         * 已经执行次数
         */
        private Integer frequency;

        /**
         * 销售id
         */
        private String newFileName;

        /**
         * 登录名
         **/
        private String loginName;

        public GenWorker(String traceId, Long recordId, PdfReportDTO reportDTO, Integer frequency, String newFileName, String loginName) {
            this.traceId = traceId;
            this.recordId = recordId;
            this.reportDTO = reportDTO;
            this.frequency = frequency;
            this.newFileName = newFileName;
            this.loginName = loginName;
        }

        @Override
        public void run() {
            // 3次后默认执行失败,更新执行结果
            if (frequency >= 3) {
                reportsExportDao.updateStatus(ExportStatus.DATA_ERROR, recordId);
                logger.error("生成pdf,多次尝试执行后失败,traceId:[{}],recordId:[{}]", traceId, recordId);
                return;
            }
            String htmlFileName = null, pdfFileName = null, afterMarkFile = null;
            try {
                MDC.put("X-B3-TraceId", traceId);
                PdfReportVO reportVO = genPdfReportStrategy.genPdfReport(reportDTO);
                logger.info("生成pdf任务,reportVO:[{}]", JSONObject.toJSONString(reportVO));
                String staticHtml = velocityEnGineUtil.generatorStaticHtml(VelocityPathConstants.REPORT_VM_ADDRESS, ImmutableMap.of("pdfReportVO", reportVO));
                htmlFileName = FileUtil.textOutput2File(staticHtml, FileUtil.genFileName(), genFilePath);
                pdfFileName = PhantomTools.printHtml2Pdf(htmlFileName, FileUtil.genFileName(), genFilePath);
                TimeUnit.SECONDS.sleep(10);
                afterMarkFile = watermarkUtil.watermark(pdfFileName, loginName, genFilePath, newFileName);
                PutFileDTO fileDTO = uploadUtil.putFile(afterMarkFile);
                reportsExportDao.updateStatusAndFileUrl(fileDTO.getFileUrl(), ExportStatus.DATA_CREATE, recordId);
            } catch (Exception e) {
                logger.error("生成pdf,执行失败", traceId, e);
                // 执行失败重试
                GEN_THREAD_POOL.submit(this);
            } finally {
                FileUtil.delFile(htmlFileName);
                FileUtil.delFile(pdfFileName);
                FileUtil.delFile(afterMarkFile);
                MDC.clear();
                frequency++;
            }
        }
    }
  • 组建tips(各段落提示文案)实现
@Component("fillTipsStrategy")
public class FillTipsStrategy {

    @Autowired
    private List fillTipsList = new ArrayList<>(6);

    /**
     * 填充tips
     *
     * @param pdfReportVO
     */
    public void fillTips(PdfReportVO pdfReportVO) {
        if (Objects.isNull(pdfReportVO)) {
            return;
        }
        fillTipsList.forEach(x -> x.fillTips(pdfReportVO));
    }
}
  • 数据各种处理实现
@Component("reportHandlerStrategy")
public class ReportHandlerStrategy {

    @Autowired
    private List reportHandlerServices = new ArrayList<>(5);

    @PostConstruct
    public void init() {
        Collections.sort(reportHandlerServices, Comparator.comparingInt(Ordered::getOrder));
    }

    /**
     * 处理数据
     *
     * @param pdfReportVO
     */
    public void dataHandle(PdfReportVO pdfReportVO) {
        reportHandlerServices.forEach(x -> x.dataHandle(pdfReportVO));
    }
}

最后总结:

  1. 服务重启过程中未完成的任务没有在重启后继续执行
  2. 通过定时线程定时去数据库拿数据,因为有分布式锁,所以可能导致多个server之间分配不均,极端情况下可能有些server就一直拿不到锁,所以一直也没有进行任务
  3. 重试策略应该放到单独的一个异常队列进行5分钟重试、10分钟重试、30分钟重试...,把主任务处理的资源让出来。

你可能感兴趣的:(javaspring)