需求描述:
公司有一些数据(查数据库、查接口),汇总后要体现到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));
}
}
最后总结:
- 服务重启过程中未完成的任务没有在重启后继续执行
- 通过定时线程定时去数据库拿数据,因为有分布式锁,所以可能导致多个server之间分配不均,极端情况下可能有些server就一直拿不到锁,所以一直也没有进行任务
- 重试策略应该放到单独的一个异常队列进行5分钟重试、10分钟重试、30分钟重试...,把主任务处理的资源让出来。