数据转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(流程结束)

【数据转pdf(包含echarts图表)】细节描述:
由于数据分为月维度和周维度,将来可能会增加年维度,所以采用策略模式来获取数据,数据又有好多个获取渠道,又都为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分钟重试...,把主任务处理的资源让出来。

    推荐阅读