使用 Java 携手 SpringBoot + PhantomJS + ECharts 在服务端生成图表并存为图片

前言

随着前端技术越来越成熟,许多公司的项目也转为了前后端分离框架,而最常用的图表组件(Echarts )也非常成熟的与 Vue 结合起来。

但是,最近接到了一个非常奇葩的需求,要求用 Java 在服务端生成图表,并转为图片,用于媒体分享和邮件传送!

作为一个 “资深” 的 Java 开发工程师,我能被这点小事难住吗?于是查阅大量资料,翻遍 GitHub 、Stack Overflow、简书、Gitee 等 著名网站,终于让我整出来了,总结出来分享出来,记得点赞收藏,以备不时之需!

使用技术

SpringBoot + PhantomJS + Echarts

1、SpringBoot 各位都熟悉,不用过多介绍。

2、PhantomJS 是一个不需要浏览器的富客户端。

官方介绍:PhantomJS是一个基于 WebKit 的服务器端JavaScript API。它全面支持web而不需浏览器支持,支持各种Web标准:DOM处理,CSS选择器, JSON,Canvas,和SVG。PhantomJS常用于页面自动化,网络监测,网页截屏,以及无界面测试等。

通常我们使用PhantomJS作为爬虫工具。传统的爬虫只能单纯地爬取html的代码,对于js渲染的页面,就无法爬取,如Echarts统计图。而PhantomJS正可以解决此类问题。

我们可以这么理解 PhantomJS,PhantomJS是一个无界面、可运行脚本的谷歌浏览器。

3、ECharts:一个基于 JavaScript 的开源可视化图表库。

PhantomJS 环境配置

PhantomJS 下载安装:

PhantomJS安装非常简单,直接在官网 http://phantomjs.org/download.html 下载最新的安装包, 安装包有Windows,Mac OS X, Linux 64/32 bit,选择对应的版本下载解压即可使用,在下载包里有个example文件夹,里面对应了许多示例供参考。

将下载后解压的文件夹放在 D:\\Program Files\\PhantomJS,为方便使用,我们将 PhantomJS 添加至环境变量中,并将下载到的安装包放在对应的目录下。

Windows:
右键我的电脑
->属性
->高级系统设置
->高级
->环境变量
->用户变量/系统变量
-> 在 Path 添加 D:\\Program Files\\PhantomJS\\bin\\

Linux:
vi /etc/profile
export PATH=$PATH:/usr/local/phantomjs/bin

PhantomJS 测试脚本

打开 CMD,进入 example 目录,运行命令 phantomjs hello.js, 输出 “Hello World” 则代表配置成功。

Echarts 环境配置

生成图片的核心脚本在于 echarts-convert.js ,同时结合 echarts.min.js、jquery.min.js、china.js 三个脚本来生成图片。

由于 js 源码内容过长,我已将 js 脚本及其项目源码放在 GitHub、Gitee、Coding 等代码开源平台,文末附带源码链接,有需要的可自行下载。

将脚本下载完后,放在 D:\\Program Files\\echartsconvert,以便于 PhantomJS 调用脚本生成图片。

脚本使用

在 `echarts-convert.js` 同级目录下,运行命令 ` phantomjs echarts-convert.js -s `,如果控制台出现"echarts-convert server start success. [pid]=xxxx"则表示启动成功,默认端口 9090,关闭 CMD 则关闭脚本程序。

为了方便在 Windows 开发的小伙伴使用,我写了一个 bat 脚本 PhantomJS.bat ,直接复制代码,粘贴在记事本中并保存为 .bat 文件,然后再桌面双击脚本即可一键启动 PhantomJS。

# PhantomJS.bat

D:
cd D:\\Program Files\\echartsconvert
phantomjs echarts-convert.js -s

至此,环境已经配置完毕,迫不及待的小伙伴们终于可以开始撸 Java 代码了。

SpringBoot 调用 PhantomJS

项目结构:

1、在 pom.xml 引入 freemarker,用于解析 ftl 模板文件。

<!-- 解析ftl模板文件 -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

2、在 templates 目录下 创建 echarts 目录,并放入 EChartsLineOption.ftl 模板文件,可通过 ftl 模板调整参数完成自定义图片。示例为折线图,有需要别的图表类型自行更换 Option 内容即可。

{
    backgroundColor: '#000000',
    color: ['#FEE108', '#9e9e9e'],
    title: {
        text: '${title}',
        left: 25,
        top: 10,
        textStyle: {
            color: '#FFFFFF',
            fontSize: 14,
            fontWeight: 'normal'
        }
    },
    grid: {
        top: '60px',
        left: '60px',
        right: '80px',
        bottom: '80px'
    },
    xAxis: {
        type: 'category',
        axisLine: {
            onZero: false,
            lineStyle: {
                color: '#FFFFFF'
            }
        },
        splitLine: {
            show: false
        },
        axisTick: {
            inside: true
        },
        axisLabel: {
            color: '#FFFFFF'
        },
        data: ${categories}
    },
    yAxis: {
        type: 'value',
        position: 'right',
        splitLine: {
            show: false
        },
        axisLine: {
            lineStyle: {
                color: '#FFFFFF'
            }
        },
        axisLabel: {
            color: '#FFFFFF',
            formatter:function (value, index) {
                return value.toFixed(0);
            }
        },
        min: function (value, index) {
            return value.min - 1;
        },
        max: function (value, index) {
            return value.max + 1;
        }
    },
    series: [
        {
            type: 'line',
            symbol: 'none',
            data: ${values},
        },{
            type: 'line',
            markLine: {
            symbol: ['none', 'none'],
            label: {
                show: false,
                fontSize: 0
            },
            data: [{
                yAxis: 0,
                lineStyle: {
                    color: '#9e9e9e'
                }
            }]
        }
    }],
    graphic: [{
        type: 'text',
        right: '48',
        top: '10',
        style: {
            fill: '#FFFFFF',
            text: 'https://github.com/LouisLiu00',
            font: '14px sans-serif',
        }
    },{
        type: 'text',
        right: '70',
        bottom: '80',
        style: {
            fill: '#333333',
            text: 'Louis',
            font: '48px sans-serif',
        }
    }]
}

3、创建 EchartsUtil 工具类,编写 generateEChartsBase64() 方法,用于生成 base64 编码图片。

package louis.echarts.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

/**
* @Description ECharts 工具类
* @Author Louis
* @Date 2022/07/10 16:36
*/
@Slf4j
public final class EChartsUtil {

    private static final String SUCCESS_CODE = "1";

    /**
    * @Description 生成ECharts图片的Base64编码
    * @Param [option]
    * @Return java.lang.String
    * @Author Louis
    * @Date 2022/07/10 16:40
    */
    public static String generateEChartsBase64(String phantomjsUrl, String option) {
        // 手动拼接option示例
        // String option = "{title:{text:'ECharts 示例'},tooltip:{},legend:{data:['销量']},xAxis:{data:['衬衫','羊毛衫','雪纺衫','裤子','高跟鞋','袜子']},yAxis:{},series:[{name:'销量',type:'bar',data:[5,20,36,10,10,20]}]}";
        if (!StringUtils.hasText(option)) {
            return null;
        }
        // 替换掉换行符,将双引号替换为单引号
        option = option.replaceAll("\\\\r\\\\n", "").replaceAll("\\"", "'");
        // 将option字符串作为参数发送给echartsConvert服务器
        String result = RESTUtil.sendPostRequest(phantomjsUrl, "opt=" + option);
        // 解析echartsConvert响应
        JSONObject response = JSON.parseObject(result);
        // 如果echartsConvert正常返回
        if (SUCCESS_CODE.equals(response.getString("code"))) {
            return response.getString("data");
        } else {
            // 未正常返回
            log.error("ECharts Convert 服务器异常:{}", response);
        }
        return null;
    }

}

4、创建 RESTUtil 工具类,用于发送 Http 请求。

package louis.echarts.util;

import cn.hutool.http.HttpUtil;

/**
 * @ClassName RESTUtil
 * @Description 发送REST请求工具类
 * @Author Louis
 * @Date 2022/7/10 16:14
 */
public final class RESTUtil {

    public static String sendPostRequest(String url, String params) {
        return HttpUtil.createPost(url).body(params).execute().body();
    }

}

5、创建 FreemarkerUtil 工具类,用于读取解析 ftl 模板文件。

package bots.util;

import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.Map;

/**
* @Description Freemarker 工具类
* @Author Louis
* @Date 2022/07/10 17:16
*/
@Slf4j
public final class FreemarkerUtil {

    // 类加载器,用于获取项目目录
    private static final ClassLoader CLASS_LOADER = FreemarkerUtil.class.getClassLoader();
    // 模板存放的目录
    private static final String BASE_PATH = "templates/echarts";

    /**
    * @Description 加载模板并生成ECharts的option数据字符串
    * @Param [templateFileName, data]
    * @Return java.lang.String
    * @Author Louis
    * @Date 2022/07/10 17:16
    */
    public static String generate(String templateFileName, Map<String, Object> data) {
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_31);
        // 设置默认编码
        configuration.setDefaultEncoding("UTF-8");
        // 将 data 写入模板并返回
        try {
            StringWriter writer = new StringWriter();
            // 设置模板所在目录,设置目录打成jar包后无法读取,所以使用类加载器
            // configuration.setDirectoryForTemplateLoading(new File(BASE_PATH));
            configuration.setClassLoaderForTemplateLoading(CLASS_LOADER, BASE_PATH);
            // 生成模板对象
            Template template = configuration.getTemplate(templateFileName);
            template.process(data, writer);
            writer.flush();
            return writer.getBuffer().toString();
        } catch (Exception e) {
            log.error("解析模板异常:{}", e);
        }
        return null;
    }

}

6、创建 Base64Util 工具类,将生成的 bae64 转为 java.io.File 文件。

package louis.echarts.util;

import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Base64;

/**
* @Description Base64 工具类
* @Author Louis
* @Date 2022/07/10 17:24
*/
@Slf4j
public final class Base64Util {

    /**
    * @Description 将Base64字符串转为文件对象
    * @Param [base64]
    * @Return java.io.File
    * @Author Louis
    * @Date 2022/07/10 17:25
    */
    public static File base64ToFile(String base64) {
        try {
            // Base64解码
            byte[] b = Base64.getDecoder().decode(base64);
            for(int i = 0; i < b.length; ++i ){
                if(b[i] < 0){
                    //调整异常数据
                    b[i] += 256;
                }
            }
            // 对文件重命名,设定为当前系统时间的毫秒数加UUID
            String newFileName = System.currentTimeMillis() + "-" + CommonUtil.randomUUID() + ".png";
            // 放在本地临时文件目录
            String localFilePath = String.format("%stemp%s%s%s%s%s%s", File.separator, File.separator, DateUtil.currentYear(), File.separator, DateUtil.currentMonth(), File.separator, DateUtil.currentDay());
            File filePath = new File(localFilePath);
            if (!filePath.exists()) {
                // mkdirs(): 创建多层目录
                filePath.mkdirs();
            }
            // 文件全限定名
            String path = localFilePath + File.separator + newFileName;
            // 将数据通过流写入文件
            OutputStream out = new FileOutputStream(path);
            out.write(b);
            out.flush();
            out.close();
            return new File(path);
        } catch (Exception e) {
            log.error(e.toString());
        }
        return null;
    }

}

7、编写 EChartsService 服务层业务代码,调用工具类生成图片。

package louis.echarts.service;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import louis.echarts.util.Base64Util;
import louis.echarts.util.EChartsUtil;
import louis.echarts.util.FreemarkerUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * @Description ECharts 图表服务层
 * @Author Louis
 * @Date 2022/07/10 17:14
 */
@Slf4j
@Service
public class EChartsService {

    // PhontomJS 服务网址
    @Value("${phantomjs.url}")
    private String phantomjsUrl;

    /**
    * @Description 生成图表
    * @Return java.io.File
    * @Author Louis
    * @Date 2022/07/10 17:30:19
    */
    public File generateEcharts(){
        // 数据参数,可以自己通过API查询json数据
        String title = "上海天气折线图";
        List<String> categories = Arrays.asList("2022-07-10", "2022-07-11", "2022-07-12", "2022-07-13", "2022-07-14", "2022-07-15", "2022-07-16", "2022-07-17", "2022-07-18", "2022-07-19", "2022-07-20", "2022-07-21", "2022-07-22");
        List<String> values = Arrays.asList("38", "33", "33", "31", "30", "32", "34", "37", "38", "37", "36", "38", "37");
        // 模板参数
        HashMap<String, Object> data = new HashMap<>();
        data.put("title", title);
        data.put("categories", JSON.toJSONString(categories));
        data.put("values", JSON.toJSONString(values));
        // 调用模板加载数据
        String option = FreemarkerUtil.generate("EChartsLineOption.ftl", data);
        // 生成图片的base64编码
        String base64 = EChartsUtil.generateEChartsBase64(phantomjsUrl, option);
        // 将base64转为文件
        return Base64Util.base64ToFile(base64);
    }

}

8、运行 SpringBoot 核心启动类,注入 EChartsService, 调用 EChartsService 服务层的 generateEcharts() 方法。运行完毕后,打开系统文件资源管理器,发现在 D:\\Temp\\2022\\7\\10 目录下已经生成一张 .png 图片,可通过 ftl 模板调整参数完成自定义图片。generateEcharts() 方法返回的 java.io.File 对象可直接用于业务文件流操作使用。

至此,使用 Java 携手 SpringBoot + PhantomJS + ECharts 在服务端生成图片已经大功告成。

源码资源

GitHub:

https://github.com/LouisLiu00/springboot-echarts-demo

Gitee:

https://gitee.com/louis_liu_oneself/springboot-echarts-demo

Coding:

https://hupiao-coder.coding.net/public/springboot-echarts-demo/springboot-echarts-demo/git/files

版权声明:
作者:一个正经的程序员
链接:https://jkboy.com/archives/14218.html
来源:随风的博客
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
使用 Java 携手 SpringBoot + PhantomJS + ECharts 在服务端生成图表并存为图片
随着前端技术越来越成熟,许多公司的项目也转为了前后端分离框架,而最常用的图表组件(Echarts )也非常成熟的与 Vue 结合起来。
<<上一篇
下一篇>>