当前位置: 首页 > 后端技术 > Java

数据转pdf(包括echarts图表)

时间:2023-04-01 16:31:56 Java

需求说明:公司有一些数据(查数据库,查接口),汇总后要体现在pdf中,方便转发和查看。数据的具体展示方式包括echarts渲染的一些直方图以及图表,pdf方案分为六段,每段都有一部分根据数据总结的文字来展示需要注意的地方和一些合理的建议.实现方法:准备工作:1、在服务器上安装Phantomjs软件。2、公司前端同事编写html模板。3、加水印和上传文件需要注意的点:1、如果生成失败,重试策略2、如果服务器重启,如果pdf还没有生成,服务器会开始重试3、如果有多个服务器,resource重启后的分配问题,例如:重复4.Phantomjs生成pdf需要很长时间。如何异步处理请求?->C(单线程每30毫秒从队列中加锁并获取一次请求数据)C-->|构建任务并下发至执行线程池|D(主任务执行线程池50个线程)D-->E[task1]D-->F[task2]D-->H[task...]E-->I{执行成功了吗?}F-->IH-->II-->|失败次数<3|BI-->|成功或失败次数>3|J(更改数据库状态等信息等)J-->K(流程结束)任务详情流程图:图TDA[开始]-->|多线程数据创建|B(获取数据线程池)B-->C[Data1]B-->D[Data2]B-->E[Data...]C-->F(DataSummary)D-->FE-->FF-->|velocity|G(生成html)G-->|Phantomjs,根据页面的复杂程度,设置渲染时间|H(生成pdf)H-->I(加水印)I-->J(上传文件系统)J-->K(删除系统上的文件,html\pdf等)K-->L(修改数据库状态和文件地址)L-->M(结束ofprocess)详细说明:由于数据划分为月维度和周维度,未来可能增加年维度,所以采用strategy模式获取数据,获取数据的渠道很多,都是io操作。为了提高性能,创建了一个单独的数据采集线程池,线程数为100(根据具体使用情况调整。),通过future获取不同的数据源,最后汇总到主报表数据中,需要一系列的处理(例如:小数点的统一处理,根据数据计算消耗率,每个段落要根据该段的数据填写建议文字),这里我抽象了两个接口fillTips和dataHandle,多个paragraphs通过实现fillTips来填充建议文本,其他数据处理通过实现dataHandle实现整体数据处理,这里的处理采用责任链模型。每个处理类只负责自己负责解耦的部分。如果后面需要扩展,直接添加实现类即可。完成后调用velocity获取填充的html文本,并将Text输出到一个文件中进行后续操作具体代码展示:线程池定义和任务提交publicstaticfinalStringRECORD_NAME_PREFIX="%s_%s_%s_%s.pdf";私有静态ThreadPoolExecutorGEN_THREAD_POOL=newThreadPoolExecutor(50,50,0L,TimeUnit.MILLISECONDS,newArrayBlockingQueue<>(50000),newThreadFactoryBuilder().setNameFormat("task-genPdf-%d").build(),newThreadPoolExecutor。CallerRunsPolicy());/***构建任务**@paramreq*@paramuser*/publicvoidsubmitGenPdfTask(PdfReportDTOreq,Useruser)throwsException{StringtraceId=UUID.randomUUID().toString().replace("-","");logger.info("生成pdf任务,traceId:[{}],param:[{}],user:[{}]",traceId,JSONObject.toJSONString(req),JSONObject.toJSONString(user));StringfileName=String.format(RECORD_NAME_PREFIX,req.getCustId(),req.getCustName(),WEEK_EXPORT_TYPE.equals(req.getExportType())?"Weekly":"Monthly",DateFormatUtils.format(newDate(),"yyyy-MM-ddHH:mm:ss"));长记录Id=reportsExportDao.saveExportRecord(Long.valueOf(req.getCustId()),fileName,(long)user.getEmpId());GEN_THREAD_POOL.submit(newGenWorker(traceId,recordId,req,0,fileName,user.getLoginName()));}Taskworker/***生成pdf的Worker*/classGenWorkerimplementsRunnable{/***traceId**/privateStringtraceId;/***存储id*/privateLongrecordId;/***请求实体*/privatePdfReportDTOreportDTO;/***执行次数*/privateIntegerfrequency;/***销售编号*/privateStringnewFileName;/***登录名**/privateStringloginName;publicGenWorker(StringtraceId,LongrecordId,PdfReportDTOreportDTO,Integerfrequency,StringnewFileName,StringloginName){this.traceId=traceId;this.recordId=recordId;这个.reportDTO=reportDTO;this.frequency=频率;this.newFileName=newFileName;this.loginName=登录名;}@Overridepublicvoidrun(){//默认执行3次后失败,更新执行结果if(frequency>=3){reportsExportDao.updateStatus(ExportStatus.DATA_ERROR,recordId);logger.error("生成pdf,多次尝试执行失败,traceId:[{}],recordId:[{}]",traceId,recordId);返回;}StringhtmlFileName=null,pdfFileName=null,afterMarkFile=null;试试{MDC.put("X-B3-TraceId",traceId);PdfReportVOreportVO=genPdfReportStrategy.genPdfReport(reportDTO);logger.info("生成pdf任务,reportVO:[{}]",JSONObject.toJSONString(reportVO));StringstaticHtml=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);PutFileDTOfileDTO=uploadUtil.putFile(afterMarkFile);reportsExportDao.updateStatusAndFileUrl(fileDTO.getFileUrl(),ExportStatus.DATA_CREATE,recordId);}catch(Exceptione){logger.error("生成pdf,执行失败",traceId,e);//执行失败重试GEN_THREAD_POOL.submit(this);}最后{FileUtil.delFile(htmlFileName);FileUtil.delFile(pdf文件名);FileUtil.delFile(afterMarkFile);MDC.clear();频率++;}}}构建提示(每段提示文案)实现@Component("fillTipsStrategy")publicclassFillTipsStrategy{@AutowiredprivateListfillTipsList=newArrayList<>(6);/***填充提示**@parampdfReportVO*/publicvoidfillTips(PdfReportVOpdfReportVO){if(Objects.isNull(pdfReportVO)){return;}fillTipsList.forEach(x->x.fillTips(pdfReportVO));}}各种数据处理实现@Component("reportHandlerStrategy")publicclassReportHandlerStrategy{@AutowiredprivateListreportHandlerServices=newArrayList<>(5);@PostConstructpublicvoidinit(){Collections.sort(reportHandlerServices,Comparator.comparingInt(Ordered::getOrder));}/***处理数据**@parampdfReportVO*/publicvoiddataHandle(PdfReportVOpdfReportVO){reportHandlerServices.forEach(x->x.dataHandle(pdfReportVO));}}最后总结:服务重启过程中未完成的任务,重启后不再继续。定时去数据库通过定时线程获取数据,因为有分布式锁,所以可能会导致多台服务器分配不均。极端情况下,有些服务器可能一直拿不到锁,所以一直没有任务重试策略。它应该放在一个单独的异常队列中进行5分钟重试。10分钟重试,30分钟重试……,放弃主任务处理的资源