使用 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