Echarts是百度一款开源可视化图表库,基于html5 Canvas的。能够快速让你看到漂亮的效果。也是百度开源产品中的良心之作。
有时候在Java程序中也需要导出好看的图表,比如我经常会基于JMH做各种微基准测试,想将测试结果可视化导出为图表形式。
试用了一下JFreeChart,跟Echarts导出的图比起来还是弱了不少。但是Echarts是基于js的,只能在浏览器中解析和导出图片,怎么办呢?
后来我想到一个方法,就是基于WebSocket技术,服务器将图表数据推送到页面,然后页面再触发导出动作。
本篇将介绍如何通过SpringBoot、SocketIO、Echarts技术来实现这个图表导出功能。
大致步骤是这样的:
- 编写一个RESTful API接口,用来接收生成图表需要的数据,然后向页面推送
导出图片
的消息请求。
- 编写一个WebSocket接口,接收Base64格式的图片编码,然后将其转换为图片保存到磁盘上
- 启动应用后打开websocket服务端,并监听页面发送的
Echarts图片导出
消息
- 打开页面,自动连接上websocket服务器,并监听
导出图片
的消息请求,收到消息后发送Echarts图片导出
消息
- 后面就可以调用这个RESTful API接口来讲数据可视化为Echarts图片并保存了。
我曾经尝试过,Java程序通过phantomjs启动浏览器打开页面,最后导出来的图片有1.5M,而直接用浏览器打开后导出大小只有50K,
所以放弃了phantomjs方案。
maven依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <netty.version>4.1.19.Final</netty.version> <thymeleaf.version>3.0.7.RELEASE</thymeleaf.version> <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version> <jmh.version>1.20</jmh.version> </properties>
<dependencies>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-all</artifactId> <version>1.3</version> <scope>test</scope> </dependency>
<dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.13</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-buffer</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec-http</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-common</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-handler</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-resolver</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-epoll</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-unix-common</artifactId> <version>${netty.version}</version> </dependency> <dependency> <groupId>io.socket</groupId> <artifactId>socket.io-client</artifactId> <version>1.0.0</version> </dependency>
</dependencies>
|
配置文件
修改application.yml,配置web端口、socket服务端口、图片保存路径等:
1 2 3 4 5 6 7 8 9 10 11 12
| xncoding: socket-port: 9076 ping-interval: 60000 ping-timeout: 180000 image-dir: E:/pics/
server: port: 9075 jetty: max-http-post-size: 20000000
|
WebSocket服务
基于Socket.IO来实现WebSocket服务,关于这个我专门写了一篇博客:集成SocketIO实时通信
这里我不再多讲怎么集成,我只贴一下监听方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@OnEvent(value = "savePic") public void onSavePic(SocketIOClient client, AckRequest ackRequest, String imgData) { logger.info("保存客户端传来的图片数据 start, sessionId=" + client.getSessionId().toString()); String r = apiService.saveBase64Pic(imgData); logger.info("保存客户端传来的图片 = {}", r); ackRequest.sendAckData("图片保存结果=" + r); }
|
这里WebSocket服务器会监听消息savePic
,参数为Base64的图形数据,然后将其保存到磁盘上面。
Echarts配置类
Echarts可以到处非常多类型的图表,每个图表初始化需要一个特定json对象,里面的配置不一样。我使用了其中的一种用来显示性能测试对比的纵向柱状图。
然后需要编写这个配置对象,包括各个内嵌对象,具体我就不贴出来了,参考最后面的github源码。
图片导出接口
接下来编写对外开放的图片导出RESTful API接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@RestController @RequestMapping(value = "/api/v1") public class PublicController {
@Resource private ApiService apiService;
private static final Logger _logger = LoggerFactory.getLogger(PublicController.class); @RequestMapping(value = "/data", method = RequestMethod.POST) public ResponseEntity<BaseResponse> doJoin(HttpServletRequest request) throws Exception { _logger.info("数据上传消息push接口 start...."); String jsonBody = IOUtils.toString(request.getInputStream(), Charset.forName("UTF-8")); EchartsData echartsData = new EchartsData("", jsonBody); String jsonString = new ObjectMapper().writeValueAsString(echartsData); apiService.pushMsg("notify", jsonString); BaseResponse result = new BaseResponse<>(true, "数据上传消息push成功", null); return new ResponseEntity<>(result, HttpStatus.OK); } }
|
上面的pushMsg方法定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public void pushMsg(String msgType, String jsonData) { SocketIOClient targetClient = this.server.getClient(UUID.fromString(sessionId)); if (targetClient == null) { logger.error("sessionId=" + sessionId + "在server中获取不到client"); } else { targetClient.sendEvent(msgType, jsonData); } }
|
服务器将数据转换成echarts的配置json字符串后,就给浏览器推送一个notify
的消息。
页面客户端
接下来编写js客户端来连接WebSocket服务,并监听notify
的消息,在页面上生成Echarts图表后,
再给服务器发送一个savePic
的消息,并把图表的Base64编码数据传过去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="renderer" content="webkit"> <title>Echarts Demo</title> <link rel="shortcut icon" th:href="@{/favicon.ico}"/> <link th:href="@{/static/css/bootstrap.css}" rel="stylesheet"/> <style> body { padding: 20px; }
#console { height: 100px; overflow: auto; }
.connect-msg { color: green; }
.disconnect-msg { color: red; }
.send-msg { color: #888 } </style> </head> <body> <h1>Netty-socketio Demo Chat</h1> <br/> <div id="console" class="well"></div>
<div id="main" style="width:860px; height:470px;"></div>
<form class="well form-inline" onsubmit="return false;"> <input id="msg" class="input-xlarge" type="text" placeholder="Type something..."/> <button type="button" onClick="sendDisconnect()" class="btn">Disconnect</button> <button type="button" onClick="sendSavePic()" class="btn">Save Picture</button> </form>
<script th:src="@{/static/js/jquery-1.10.1.min.js}"></script> <script th:src="@{/static/js/socket.io.js}"></script> <script th:src="@{/static/js/moment.min.js}"></script> <script th:src="@{/static/js/echarts.common.min.js}" charset="utf-8"></script>
<script> var socket; var myChart;
function sendDisconnect() { socket.disconnect(); }
function output(message) { var currentTime = "<span class='time'>" + moment().format('HH:mm:ss.SSS') + "</span>"; var element = $("<div>" + currentTime + " " + message + "</div>"); $('#console').prepend(element); }
function sendSavePic() { socket.emit('savePic', myChart.getDataURL(), function (result) { output('<span class="connect-msg">' + result + '</span>'); }); }
function initPage() { console.log('this is index.html log...');
var userName = 'user' + Math.floor((Math.random() * 1000) + 1); socket = io.connect('http://localhost:9076');
console.log('socket = ' + socket);
socket.on('connect', function () { console.log('connect successful'); output('<span class="connect-msg">Client has connected to the server!</span>'); });
socket.on('disconnect', function () { console.log('disconnect successful'); output('<span class="disconnect-msg">The client has disconnected!</span>'); });
socket.on('notify', function (jsonBody) { console.log('get notify message from server...'); var msg = JSON.parse(jsonBody); output('<span class="connect-msg">接收到notify消息, 去给我保存图片</span>'); var option = msg.option; myChart.setOption(JSON.parse(option)); sendSavePic(); });
myChart = echarts.init(document.getElementById('main')); console.log('index initPage finished!') }
$(function () { initPage(); });
</script>
</body>
</html>
|
测试
编写测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class ApplicationTests { @Test public void testOption() throws Exception { String titleStr = "对象序列化为JSON字符串"; List<String> objects = Arrays.asList("FastJson", "Jackson", "Gson", "Json-lib"); List<String> dimensions = Arrays.asList("10000次", "100000次", "1000000次"); List<List<Double>> allData = new ArrayList<List<Double>>(){ { add(Arrays.asList(2.17, 9.10, 21.70)); add(Arrays.asList(1.94, 8.94, 19.43)); add(Arrays.asList(4.88, 22.88, 48.89)); add(Arrays.asList(9.11, 58.14, 108.44)); } }; String optionStr = generateOption(titleStr, objects, dimensions, allData, "秒"); postOption(optionStr, "http://localhost:9075/api/v1/data"); } }
|
一切准备就绪后,就可以开始测试了,步骤如下:
- 启动应用
- 打开首页 http://localhost:9075/
- 运行测试类
ApplicationTests.java
结果首页显示如下:
同时去E:/pics/
目录去看看,图片也成功保存。
GitHub源码
springboot-echarts