需求背景根据实际数据填充文档模板文档中的统计图也由后端生成后嵌入word中再转为PDF导出。效果如下。这是生成的word效果这是转PDF的效果表格会有一点点的错位在接受范围内技术栈评估研究了一些成熟的工具综合成本、学习曲线、项目情况以及实现效果最终选了一套方案用poi-tl生成word文档JFreeChart后端生成图表 XDocReport转PDF。参考过的一些生成word和转PDF的、处理PDF的工具Aspose.Words可生成复杂word可导出PDF(商用的不开源)也能转PDF但商用(不商用有破解的license)Docx4j支持动态创建word直接导出为PDF。(开源尝试了一下中文支持的问题比较多)帆软成熟的报表软件工具作为单独的服务(体量比较大商用成本高小项目不合适)Apache POI直接操作ooxml底层结构灵活性高手动创建文档结构和图表。开源代码量大且复杂需要转PDFXDocReport模板驱动支持docx和doc格式可直接导出pdf/html。开源用过根据ftp模板生成文档的功能调格式比较难搞LibreOffice转PDF的工具需要在服务器上安装本体的软件。代码实现1.先把文档模板放到resource目录下模板以{{totalEle}}的格式留好占位符图片用{{electricityChart}}为占位符再准备好实体类。word模板实体类/** * 单位基本情况dto */ public class BasicInfoDTO { /** * 建筑面积 */ private String buildingArea; /** * 用能总人数 */ private String energyUsers; /** *用能区总个数 */ private String energyRegionCount; /** * 总耗电量 */ private String electricityInfo; /** * 总耗水量 */ private String waterInfo; /** * 天然气 */ private String naturalGasInfo; /** * 汽油 */ private String gasolineInfo 0; /** * 柴油 */ private String dieselInfo 0; /** * 液化石油气 */ private String LPGInfo 0; //getter setter }2.引入依赖!-- poi-tl: Word模板引擎-- dependency groupIdcom.deepoove/groupId artifactIdpoi-tl/artifactId version1.12.2/version /dependency !-- JFreeChart: 图表生成 -- dependency groupIdorg.jfree/groupId artifactIdjfreechart/artifactId version1.5.3/version /dependency !-- XDocReport转PDF-- dependency groupIdfr.opensagres.xdocreport/groupId artifactIdfr.opensagres.xdocreport.document/artifactId version2.0.4/version /dependency dependency groupIdfr.opensagres.xdocreport/groupId artifactIdfr.opensagres.xdocreport.converter/artifactId version2.0.4/version /dependency dependency groupIdfr.opensagres.xdocreport/groupId artifactIdfr.opensagres.xdocreport.converter.odt.odfdom/artifactId version2.0.4/version /dependency dependency groupIdfr.opensagres.xdocreport/groupId artifactIdfr.opensagres.xdocreport.converter.docx.xwpf/artifactId version2.0.4/version /dependency3.从业务数据中提取信息结合 Word 模板生成包含文本、样式和图表的最终报告并以字节数组形式返回便于下载、存储或传输。EnergyReportService用来加载模板文件,并生成报表,调用ChartService的图表生成方法分别创建用电趋势图、能耗对比图、天气情况图将图表转换为字节数组后通过Pictures.ofBytes()指定尺寸如 500x300生成图片对象存入模板数据实现 Word 中图表的动态嵌入。/** * 模拟获取报表数据实际应该从数据库获取 */ public EnergyReportData getReportData() { EnergyReportData data new EnergyReportData(); // 基础信息 data.setUnitName(XX省XX市XX单位); data.setTimePeriod(2024年第一季度); data.setCreateTime(LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd))); // 单位情况 data.setBuildingArea(5.2); data.setEnergyUsers(1200); data.setTimeStr(2024年1月1日至2024年3月31日); data.setEleValue(156.78); data.setWaterValue(8.92); data.setNatureValue(3.45); data.setGasolineValue(41.7389); data.setDieselValue(9.7815); data.setLpgValue(9.9530); // 天气情况 data.setRainDay(12); data.setSunDay(15); // 用电量分析 data.setTotalEle(1567800.0); // 转换为kW·h data.setMaxEle(580000.0); data.setMaxEleTime(2024-03-15 14:30:00); // 月度数据用于生成图表 data.setMonthlyEnergies(Arrays.asList( createMonthlyEnergy(1月, 480000.0, 2.8, 1.1), createMonthlyEnergy(2月, 508000.0, 3.0, 1.2), createMonthlyEnergy(3月, 580000.0, 3.12, 1.15) )); return data; } private EnergyReportData.MonthlyEnergy createMonthlyEnergy(String month, Double ele, Double water, Double gas) { EnergyReportData.MonthlyEnergy energy new EnergyReportData.MonthlyEnergy(); energy.setMonth(month); energy.setElectricity(ele); energy.setWater(water); energy.setGas(gas); return energy; } /** * 生成Word报表 */ public byte[] generateWordReport() throws IOException { // 1. 获取报表数据 EnergyReportData reportData getReportData(); // 2. 准备模板数据 MapString, Object templateData new HashMap(); // 2.1 填充文本数据 templateData.put(unitName, reportData.getUnitName()); templateData.put(timePeriod, reportData.getTimePeriod()); templateData.put(createTime, reportData.getCreateTime()); templateData.put(buildingArea, reportData.getBuildingArea()); templateData.put(energyUsers, reportData.getEnergyUsers()); templateData.put(timeStr, reportData.getTimeStr()); templateData.put(eleValue, reportData.getEleValue()); templateData.put(waterValue, reportData.getWaterValue()); templateData.put(natureValue, reportData.getNatureValue()); templateData.put(totalEle, reportData.getTotalEle()); templateData.put(maxEle, reportData.getMaxEle()); templateData.put(maxEleTime, reportData.getMaxEleTime()); templateData.put(rainDay, reportData.getRainDay()); templateData.put(sunDay, reportData.getSunDay()); // 2.2 特殊文本处理加粗、颜色等 templateData.put(unitName, Texts.of(reportData.getUnitName()) .color(000000) .bold() .create()); // 2.3 生成图表并添加到数据模型 // 用电趋势图折线图 byte[] electricityChartBytes chartService.generateElectricityTrendChart( reportData.getMonthlyEnergies() ); templateData.put(electricityChart, Pictures.ofBytes(electricityChartBytes) .size(500, 300) .create()); // 能耗对比图柱状图 byte[] comparisonChartBytes chartService.generateEnergyComparisonChart(reportData); templateData.put(energyComparisonChart, Pictures.ofBytes(comparisonChartBytes) .size(500, 300) .create()); // 天气情况图饼图 byte[] weatherChartBytes chartService.generateWeatherChart( reportData.getRainDay(), reportData.getSunDay() ); templateData.put(weatherChart, Pictures.ofBytes(weatherChartBytes) .size(400, 300) .create()); // 3. 加载模板文件 ClassPathResource templateResource new ClassPathResource(templates/能耗报告模板2.0.docx); // 4. 渲染模板 try (XWPFTemplate template XWPFTemplate.compile(templateResource.getInputStream())) { template.render(templateData); // 5. 输出到字节数组 try (ByteArrayOutputStream out new ByteArrayOutputStream()) { template.write(out); return out.toByteArray(); } } }4.基于JFreeChart库实现折线图、柱状图、饼图的动态生成按不同的图表类型拆分功能将生成的图表统一转为 PNG 字节数组 嵌入Word 模板。通过指定系统已安装的「微软雅黑」字体替代默认的无中文字体解决生成的图片会中文乱码的问题。所有生成的图表共用一套字体、配色规则。生成折线图根据X轴Y轴构建数据集创建折线图美化样式输出为字节数组。ChartFactory可以创建折线图条形图面积图散点图饼图甘特图和各种专用图。通过倾斜标签30 度避免X轴名称重叠柱状图可以显示具体数值ChartService详细代码贴上下次直接用‍public class ChartService { static { try { // 直接使用系统已安装的字体 Font titleFont new Font(微软雅黑, Font.BOLD, 16); Font labelFont new Font(微软雅黑, Font.PLAIN, 12); StandardChartTheme theme new StandardChartTheme(CN, true); theme.setExtraLargeFont(titleFont); theme.setLargeFont(labelFont); theme.setRegularFont(labelFont); theme.setSmallFont(labelFont.deriveFont(10f)); // 设置其他样式 theme.setTitlePaint(Color.BLACK); theme.setSubtitlePaint(Color.DARK_GRAY); theme.setLegendBackgroundPaint(Color.WHITE); theme.setPlotBackgroundPaint(Color.WHITE); ChartFactory.setChartTheme(theme); System.out.println(✅ 使用系统字体微软雅黑); } catch (Exception e) { System.err.println(⚠️ 使用默认字体图表中文可能乱码); } } /** * 生成用电量趋势图折线图 */ public byte[] generateElectricityTrendChart(ListEnergyReportData.MonthlyEnergy monthlyData) throws IOException { DefaultCategoryDataset dataset new DefaultCategoryDataset(); // 添加月度用电数据 for (EnergyReportData.MonthlyEnergy data : monthlyData) { dataset.addValue(data.getElectricity(), 用电量(kW·h), data.getMonth()); } // 创建折线图 JFreeChart chart ChartFactory.createLineChart( 月度用电量趋势, // 图表标题 月份, // X轴标签 用电量(kW·h), // Y轴标签 dataset, // 数据集 PlotOrientation.VERTICAL, true, // 显示图例 true, // 显示提示 false // 不生成URL ); // 美化样式 chart.setBackgroundPaint(Color.white); CategoryPlot plot chart.getCategoryPlot(); plot.setBackgroundPaint(Color.white); plot.setRangeGridlinePaint(Color.gray); LineAndShapeRenderer renderer (LineAndShapeRenderer) plot.getRenderer(); renderer.setSeriesPaint(0, new Color(79, 129, 189)); // 设置线条颜色 renderer.setSeriesStroke(0, new BasicStroke(2.0f)); // 设置线条粗细 // 转换为字节数组 return chartToBytes(chart, 600, 400); } /** * 生成能耗对比图柱状图 */ public byte[] generateEnergyComparisonChart(EnergyReportData data) throws IOException { DefaultCategoryDataset dataset new DefaultCategoryDataset(); // 添加各项能耗数据 dataset.addValue(data.getEleValue(), 能耗值, 电力(万千瓦时)); dataset.addValue(data.getWaterValue(), 能耗值, 水(万吨)); dataset.addValue(data.getNatureValue(), 能耗值, 天然气(万m³)); dataset.addValue(data.getGasolineValue(), 能耗值, 汽油(万升)); dataset.addValue(data.getDieselValue(), 能耗值, 柴油(万升)); dataset.addValue(data.getLpgValue(), 能耗值, 液化石油气(吨)); // 创建柱状图 JFreeChart chart ChartFactory.createBarChart( 各类能源消耗对比, 能源类型, 消耗量, dataset, PlotOrientation.VERTICAL, true, true, false ); // 美化样式 chart.setBackgroundPaint(Color.white); CategoryPlot plot chart.getCategoryPlot(); plot.setBackgroundPaint(Color.white); plot.setRangeGridlinePaint(Color.gray); CategoryAxis domainAxis plot.getDomainAxis(); // 设置标签角度倾斜显示解决X轴标签省略问题 domainAxis.setCategoryLabelPositions( CategoryLabelPositions.createUpRotationLabelPositions(Math.PI / 6.0) // 30度倾斜 ); domainAxis.setTickLabelPaint(Color.BLACK); // 设置柱子颜色 BarRenderer renderer (BarRenderer) plot.getRenderer(); Color[] colors { new Color(79, 129, 189), // 蓝色 - 电力 new Color(155, 187, 89), // 绿色 - 水 new Color(255, 153, 0), // 橙色 - 天然气 new Color(192, 80, 77), // 红色 - 汽油 new Color(128, 100, 162), // 紫色 - 柴油 new Color(75, 172, 198) // 青色 - 液化石油气 }; for (int i 0; i colors.length; i) { renderer.setSeriesPaint(i, colors[i]); } // 让柱子显示数值 renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator()); renderer.setDefaultItemLabelsVisible(true); // 3. 设置数值标签的位置柱子上方 renderer.setDefaultPositiveItemLabelPosition( new ItemLabelPosition( ItemLabelAnchor.OUTSIDE12, // 柱子顶部外侧 TextAnchor.BOTTOM_CENTER // 底部居中 ) ); return chartToBytes(chart, 600, 500); } /** * 生成天气情况饼图 */ public byte[] generateWeatherChart(int rainDays, int sunDays) throws IOException { DefaultPieDataset dataset new DefaultPieDataset(); dataset.setValue(晴天, sunDays); dataset.setValue(雨天, rainDays); dataset.setValue(多云, 30 - rainDays - sunDays); // 假设一个月30天 JFreeChart chart ChartFactory.createPieChart( 天气情况分布, dataset, true, // 显示图例 true, // 显示提示 false ); chart.setBackgroundPaint(Color.white); PiePlot plot (PiePlot)chart.getPlot(); // 设置颜色 Color green new Color(117, 189, 66); Color orange new Color(242, 186, 2); Color blue new Color(72, 116, 203); plot.setSectionPaint(晴天, orange); plot.setSectionPaint(雨天, green); plot.setSectionPaint(多云, blue); // 设置白色边框 plot.setSectionOutlinePaint(晴天, Color.WHITE); plot.setSectionOutlinePaint(雨天, Color.WHITE); plot.setSectionOutlinePaint(多云, Color.WHITE); // 设置白色边框 for (Object key : dataset.getKeys()) { plot.setSectionOutlineStroke((Comparable) key, new BasicStroke(2.0f)); } plot.setSectionOutlinesVisible(true); // 设置标签背景色 plot.setLabelBackgroundPaint(null); plot.setLabelOutlinePaint(null); // 设置数值显示 plot.setLabelGenerator(new StandardPieSectionLabelGenerator( {0}: {1}天 ({2}), NumberFormat.getNumberInstance(), NumberFormat.getPercentInstance())); return chartToBytes(chart, 400, 300); } private byte[] chartToBytes(JFreeChart chart, int width, int height) throws IOException { try (ByteArrayOutputStream baos new ByteArrayOutputStream()) { ChartUtils.writeChartAsPNG(baos, chart, width, height); return baos.toByteArray(); } }5.使用XDocReport将生成的word转换成PDF虽然XDocReport生成文档不太好操作用来转换PDF效果还可以。public class XDocReport2Pdf { protected static final Logger log LoggerFactory.getLogger(XDocReport2Pdf.class); /** * 将Word文档转换为PDF * param wordBytes Word文档字节数组 * return PDF字节数组 */ public static byte[] convertToPdf(byte[] wordBytes) throws Exception { if (wordBytes null || wordBytes.length 0) { throw new IllegalArgumentException(Word文档不能为空); } File tempWordFile null; File tempPdfFile null; try { // 1. 创建临时Word文件 tempWordFile File.createTempFile(temp_word_, .docx); Files.write(tempWordFile.toPath(), wordBytes); // 2. 创建临时PDF文件 tempPdfFile File.createTempFile(temp_pdf_, .pdf); // 3. 执行转换 convertWordToPdf(tempWordFile, tempPdfFile); // 4. 读取PDF文件 return Files.readAllBytes(tempPdfFile.toPath()); } finally { // 5. 清理临时文件 if (tempWordFile ! null tempWordFile.exists()) { tempWordFile.delete(); } if (tempPdfFile ! null tempPdfFile.exists()) { tempPdfFile.delete(); } } } /** * 转换pdf */ private static void convertWordToPdf(File inputWord, File outputPdf) throws Exception { try (InputStream in Files.newInputStream(inputWord.toPath()); OutputStream out Files.newOutputStream(outputPdf.toPath())) { // 创建转换选项 Options options Options.getFrom(DocumentKind.DOCX) .to(ConverterTypeTo.PDF) .via(ConverterTypeVia.XWPF); // 使用XWPF转换器处理.docx // 获取转换器 IConverter converter ConverterRegistry.getRegistry().getConverter(options); if (converter null) { throw new RuntimeException(未找到合适的PDF转换器请检查依赖); } // 执行转换 converter.convert(in, out, options); } catch (XDocConverterException e) { throw new RuntimeException(Word转PDF失败: e.getMessage(), e); } } }6.差不多就是这样了在controller层调用就可以了GetMapping(/energy/preview) public void xDoc(HttpServletResponse response) { try { // 生成Word文档字节 byte[] wordBytes energyReportService.generateWordReport(); // word转PDF byte[] out XDocReport2Pdf.convertToPdf(wordBytes); // 设置响应头 String fileName URLEncoder.encode(能耗报告_ System.currentTimeMillis() .pdf, StandardCharsets.UTF_8.toString()); response.setContentType(application/pdf); response.setHeader(Content-Disposition, inline; filename\ fileName \); // inline表示在线预览 response.setHeader(Content-Length, String.valueOf(out.length)); response.getOutputStream().write(out); response.getOutputStream().flush(); } catch (Exception e) { throw new RuntimeException(e); } }在Windows上效果还可以也没有出现别的中文乱码情况服务器上需要测试一下。以上