后台生成EChart報表圖片並插入到Word文件中
前期准備
PhantomJS
https://phantomjs.org/download.html
官方介紹:
PhantomJS是一個基於 WebKit 的服務器端JavaScript API。它全面支持web而不需瀏覽器支持,支持各種Web標准:DOM處理,CSS選擇器, JSON,Canvas,和SVG。
PhantomJS常用於頁面自動化,網絡監測,網頁截屏,以及無界面測試等。
通常我們使用PhantomJS作為爬蟲工具。傳統的爬蟲只能單純地爬取html的代碼,對於js渲染的頁面,就無法爬取,如ECharts統計圖。而PhantomJS正可以解決此類問題。
echarts-convert
https://gitee.com/saintlee/echartsconvert
一個配合phantomjs,在服務端生成EChart圖片的工具包。
ECharts-2.2.7.jar
https://github.com/abel533/ECharts
一個供Java開發使用的ECharts的開發包,主要目的是方便在Java中構造ECharts中可能用到的全部數據結構,如完整的結構Option。
注:我的自用版本資源在這里
鏈接: https://pan.baidu.com/s/1Y4hQZXtKglWq7LnISQtiUw 密碼: 8alt
生成EChart圖片
新建一個測試類 EChartWordDemo,后續的方法都加在這個類中
/** * 后台生成EChart圖片並插入Word測試類 * * @Author FanZhen * @Date 2021/3/12 */ public class EChartWordDemo { // ============== 這里要改成自己電腦對應的文件位置,服務器部署時可以通過環境變量等方式來動態改變它們的值 ================= /** * echart-convert包的路徑 */ private String eChartJSPath = "/Users/helios_fz/IdeaProjects/websocket/src/main/resources/echart/echarts-convert/echarts-convert1.js"; /** * echart臨時文件存儲路徑 */ private String eChartTempPath = "/Users/helios_fz/Desktop/a/b/"; /** * phantomjs命令路徑 */ private String phantomjsPath = "/Users/helios_fz/IdeaProjects/rbac/rbac-admin/src/main/resources/phantomjs/phantomjs-2.1.1-macosx/bin/phantomjs"; }
在pom文件中引入 ECharts.jar 的依賴。因為這個包在中心庫沒有,阿里雲的資源我試了幾次又沒拉下來,所以我就配置靜態引入了(這里也是要根據自己放置jar包的位置來調整配置):
<!-- echart依賴 --> <dependency> <groupId>com.github.abel533</groupId> <artifactId>echarts</artifactId> <version>2.2.7</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/echart/ECharts-2.2.7.jar</systemPath> </dependency>
拼接EChart初始化json的方法:

/** * 生成EChart初始化json * * @param title 圖片標題 * @param xAxis x軸 * @param line1 柱狀圖1 * @param line2 柱狀圖2 * @return json字符串 */ private String getEChartOption(String title, String xAxis, String line1, String line2) { return "{\n" + " color: [\"#f8732c\", \"#0094c8\"],\n" + " title: {\n" + // 名字+流量 " text: " + "\"" + title + "\"" + ",\n" + " },\n" + " tooltip: {\n" + " trigger: \"axis\",\n" + " },\n" + " legend: {\n" + " data: [\"line1\", \"line2\"],\n" + " },\n" + " toolbox: {\n" + " show: true,\n" + " feature: {\n" + " saveAsImage: { show: true },\n" + " },\n" + " },\n" + " calculable: true,\n" + " xAxis: {\n" + " type: \"category\",\n" + " data: " + // X軸數據 xAxis + ",\n" + " axisLabel: { interval: 0 },\n" + " },\n" + " yAxis: [\n" + " {\n" + " type: \"value\",\n" + " axisLabel: {\n" + " formatter: function (value) {\n" + " return value.toFixed(1) + \"MB\";\n" + " },\n" + " },\n" + " },\n" + " ],\n" + " series: [\n" + " {\n" + " name: \"line1\",\n" + " type: \"bar\",\n" + " symbol: \"circle\",\n" + " barMaxWidth: 40,\n" + " data: " + // line1數據 line1 + ",\n" + " itemStyle: {\n" + " //上方顯示數值\n" + " normal: {\n" + " label: {\n" + " show: true, //開啟顯示\n" + " position: \"top\", //在上方顯示\n" + " textStyle: {\n" + " //數值樣式\n" + " color: \"black\",\n" + " // fontSize: 14\n" + " },\n" + " },\n" + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" + " { offset: 0, color: \"#f8732c\" },\n" + " { offset: 1, color: \"#FFCEBF\" },\n" + " { offset: 1, color: \"#FFCEBF\" },\n" + " ]),\n" + " },\n" + " emphasis: {\n" + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" + " { offset: 1, color: \"#f8732c\" },\n" + " { offset: 0, color: \"#FFCEBF\" },\n" + " { offset: 0, color: \"#FFCEBF\" },\n" + " ]),\n" + " },\n" + " },\n" + " },\n" + " {\n" + " name: \"line2\",\n" + " type: \"bar\",\n" + " symbol: \"circle\",\n" + " barMaxWidth: 40,\n" + " data: " + // line2數據 line2 + ",\n" + " itemStyle: {\n" + " //上方顯示數值\n" + " normal: {\n" + " label: {\n" + " show: true, //開啟顯示\n" + " position: \"top\", //在上方顯示\n" + " textStyle: {\n" + " //數值樣式\n" + " color: \"black\",\n" + " // fontSize: 14\n" + " },\n" + " },\n" + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" + " { offset: 0, color: \"#0094C8\" },\n" + " { offset: 1, color: \"#CEF2FF\" },\n" + " { offset: 1, color: \"#CEF2FF\" },\n" + " ]),\n" + " },\n" + " emphasis: {\n" + " color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" + " { offset: 1, color: \"#0094C8\" },\n" + " { offset: 0, color: \"#CEF2FF\" },\n" + " { offset: 0, color: \"#CEF2FF\" },\n" + " ]),\n" + " },\n" + " },\n" + " },\n" + " ],\n" + " }"; }
上邊的代碼折疊起來的原因有兩點:
- 這個json我是寫死的,因為我這邊的需求只是生成一個柱狀圖,其他類型也可以生成,只要把前端的json復制過來就行了
- 這里也可以寫一個動態的拼接方法,EChart.jar那個包就是做這部分工作的,可是我懶得寫 = =
生成EChart圖片的代碼:
/** * 生成EChart圖 * * @param options EChart初始化json * @param tmpPath 臨時文件存放處 * @param echartJsPath 第三方工具路徑 * @return */ private String generateEChart(String options, String tmpPath, String echartJsPath) { // 生成Echart的初始化json文件 String dataPath = writeFile(options, tmpPath); // 生成隨機文件名 String fileName = UUID.randomUUID().toString().substring(0, 8) + ".png"; String path = tmpPath + fileName; try { // 文件路徑(路徑+文件名) File file = new File(path); // 文件不存在則創建文件,先創建目錄 if (!file.exists()) { File dir = new File(file.getParent()); dir.mkdirs(); file.createNewFile(); } // 這里只能寫絕對路徑,因為要執行系統命令行 String cmd = phantomjsPath + " " + echartJsPath + " -infile " + dataPath + " -outfile " + path; Process process = Runtime.getRuntime().exec(cmd); BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream())); String line = ""; while ((line = input.readLine()) != null) { System.out.println(line); } input.close(); // 刪除生成的臨時json文件 File jsonFile = new File(dataPath); jsonFile.delete(); return path; } catch (IOException e) { e.printStackTrace(); return path; } } /** * 保存EChart臨時json * * @param options echart初始化js * @param tmpPath 臨時文件保存路徑 * @return 文件完整路徑 */ private String writeFile(String options, String tmpPath) { String dataPath = tmpPath + UUID.randomUUID().toString().substring(0, 8) + ".json"; try { /* 寫入Txt文件 */ // 相對路徑,如果沒有則要建立一個新的output.txt文件 File writeName = new File(dataPath); // 文件不存在則創建文件,先創建目錄 if (!writeName.exists()) { File dir = new File(writeName.getParent()); dir.mkdirs(); // 創建新文件 writeName.createNewFile(); } BufferedWriter out = new BufferedWriter(new FileWriter(writeName)); out.write(options); // 把緩存區內容壓入文件 out.flush(); // 最后記得關閉文件 out.close(); } catch (IOException e) { e.printStackTrace(); } return dataPath; }
生成Word文件並插入EChart圖片
在pom文件中引入生成word需要的依賴:
<!-- word依賴 --> <dependency> <groupId>com.lowagie</groupId> <artifactId>itext</artifactId> <version>2.1.5</version> </dependency> <dependency> <groupId>com.lowagie</groupId> <artifactId>itext-rtf</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency>
生成word並插入圖片:
/** * 生成word文件 * * @param title 標題 * @param content 正文 * @param imagePaths 待插入的圖片地址列表 * @param docPath word文件保存地址 * @throws IOException * @throws com.lowagie.text.DocumentException */ private void writeWord(String title, String content, List<String> imagePaths, String docPath) throws IOException, com.lowagie.text.DocumentException { // 創建文件 File file = new File(docPath); Document document = new Document(PageSize.A4); RtfWriter2.getInstance(document, new FileOutputStream(file)); document.open(); // 初始化字體 BaseFont bfChinese = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED); // 標題字體 Font titleFont = new Font(bfChinese, 30, Font.BOLD); // 小標題字體 // Font littleTitleFont = new Font(bfChinese, 20, Font.BOLD); // 正文字體 Font contextFont = new Font(bfChinese, 15, Font.NORMAL); // 設置大標題 Paragraph titlePar = new Paragraph(title); titlePar.setFont(titleFont); titlePar.setAlignment(Element.ALIGN_CENTER); document.add(titlePar); // 正文 Paragraph context = new Paragraph(content); context.setAlignment(Element.HEADER); context.setFont(contextFont); document.add(context); // 圖片 imagePaths.forEach(imagePath -> { Image img = null; try { img = Image.getInstance(imagePath); img.setAbsolutePosition(0, 0); img.scalePercent(50f); // 設置圖片顯示位置 img.setAlignment(Image.LEFT); document.add(img); } catch (IOException | DocumentException e) { e.printStackTrace(); } }); document.close(); }
調用方法生成圖片並插入word中
/** * 導出docx文件 * * @param params 參數列表 * @param response response 返回的文件流 */ public void getWordWithEchart(Map<String, Object> params, HttpServletResponse response) throws IOException { // 這一部分代碼和業務邏輯強相關了,需要自己根據現實情況去實現 List<String> imagePaths = new ArrayList<>(); 一個業務數組.forEach(id -> { // 這里的X軸其實有一個問題,就是用"MM-dd"形式傳入其中的話,生成工具會默認這是一個減法運算。 // 這個時候需要遍歷一下X軸數據,在每一個數據外面加上一對轉義的雙引號,like this:"\"" String eChartOption = getEChartOption( 標題, x軸, 柱狀圖1, 柱狀圖2); // 生成圖片 imagePaths.add(generateEChart(eChartOption, eChartTempPath, eChartJSPath)); }); // 根據EChart圖片生成Word文檔 String title = "Word文檔Title\n"; // doc文件命名 String docName = UUID.randomUUID().toString().substring(0, 8) + ".docx"; String docPath = eChartTempPath + docName; StringBuffer content = new StringBuffer(); ...加一大堆生成word內容的邏輯 content.append("\n"); try { // 生成word文件 writeWord(title, content.toString(), imagePaths, docPath); } catch (IOException | DocumentException e) { e.printStackTrace(); } //刪除臨時文件 imagePaths.forEach(imagePath -> { File file = new File(imagePath); file.delete(); }); // 返回文件流 File file = new File(docPath); // 八進制輸出流 response.setContentType("application/octet-stream"); response.setHeader("content-type", "application/octet-stream"); // 設置導出Word的名稱 response.setHeader("Content-disposition", "attachment;filename=" + "Word文檔.docx"); // 刷新緩沖 response.flushBuffer(); // 將doc文件流寫入到返回 // 根據路徑獲取要下載的文件輸入流 InputStream inputStream = new FileInputStream(file); OutputStream out = response.getOutputStream(); //創建數據緩沖區 byte[] b = new byte[1024]; int length; while ((length = inputStream.read(b)) > 0) { //把文件流寫到緩沖區里 out.write(b, 0, length); } out.flush(); out.close(); inputStream.close(); // 傳輸過后把doc文件刪除 file.delete(); }
注:這個demo在運行之后會把生成的文件都刪掉,如果想看看生成的臨時文件長成啥樣,把代碼中的文件刪除操作都注釋掉就可以了。