Bootstrap

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

目录

1、简介

 2、PhantomJS 下载安装

3、PhantomJS 测试脚本

4、Echarts 环境配置

脚本使用

5、SpringBoot 调用 PhantomJS

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

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

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

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

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

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

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


1、简介

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

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

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

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

 2、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

3、PhantomJS 测试脚本

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

4、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 则关闭脚本程序。

Linux的脚本启动样例:

nohup phantomjs echarts-convert.js -s >> /opt/software/phantomjs/log/output.log 2>&1 &

5、SpringBoot 调用 PhantomJS

5.1、在 pom.xml 引入 freemarker,用于解析 ftl 模板文件。
<!-- 解析ftl模板文件 -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>
 5.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: '雪雨天气',
font: '14px sans-serif',
}
},{
type: 'text',
right: '70',
bottom: '80',
style: {
fill: '#333333',
text: '天气为王',
font: '48px sans-serif',
}
}]
}
 5.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;
    }

}
5.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.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;
    }

}
5.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;
    }

}
 5.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);
    }

}

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

;