接上上次的学习,这次学习的内容是如何动态的显示数据。
目录
一、学习动态显示数据前的准备工作
在正式说到如何显示前,我们需要知道,我们大概要如何来动态显示数据。
- 要动态的显示数据,那么我们后台一旦有数据变化,我们就需要响应给前端一个信号。
- 前端根据信号,更新数据,或者重新查询数据等。
涉及的知识点:WebSocket、SpringBoot、Mybatis
我使用的案例,是我自己课程设计作为一个例子,实时的显示一个光照数据。
项目中涉及到的环境:
编写代码平台:IntelliJ IDEA 2019.2.4
项目管理:Maven 3.6.0
使用框架:SpringBoot 2.4.0、Mybatis 2.1.4(整合SpringBoot)、MySQL 5.1.48、websocket 5.3.1(整合SpringBoot)、thymeleaf 3.0.11(整合SpringBoot)
辅助工具:gson 2.8.5(解析JSON数据)
<!-- 工程父项目 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- MySQL 数据库连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
<scope>runtime</scope>
</dependency>
<!-- websocket 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 处理 JSON 数据 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!-- MyBatis 框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
二、如何建立长连接,来实现前后端通信。
对于前面的一些创建数据库,将数据显示到一个折线图上,我在这里就不进行讲述了,不知道如何使用 Highcharts 来显示折线图的,可以去看看博主的第一篇文章。
Highcharts 学习笔记1-折线图(时间解析不对的坑点)
大致就是显示一张这样的光照折线图
我们需要解决的第一个问题,就是如何建立一种连接,使前后端能够通信。
解决办法就是:使用 WebSocket 建立长连接。
这里只是简单的讲述,详细的一些用法,请大家自行学习
WebSocket 使用的方法:
1. 编写一个 WebSocket 的配置
配置的一些具体的含义,这里不做解释。
下面的两个配置类,都需要写到项目中。
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import javax.websocket.server.ServerEndpointConfig;
public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {
private static volatile BeanFactory context;
@Override
public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
return context.getBean(clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
MyEndpointConfigure.context = applicationContext;
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
@Bean
public MyEndpointConfigure newConfigure() {
return new MyEndpointConfigure();
}
}
2. 编写一个 WebSocket 用来处理连接
通过访问 @ServerEndpoint
注解上的 value 值对应的 URL,建立一个长连接。
连接建立成功后,会调用 onOpen(Session session)
方法,参数是建立连接的会话。
建立成功后,会将成功建立的会话添加到 CopyOnWriteArraySet 集合中,方便我们进行群发消息。
我们需要给前端发送消息时,调用 sendMessage(String message)
方法,将我们需要发送的字符串发送给前端。
前端发送消息给后端,当前端发送消息后,后端会监听到前端发送的消息,会调用 onMessage(String message, Session session)
方法。
当长连接关闭时,该监听对象,也会调用 onClose()
方法,同时将当前 session 从集合中移除
package com.smxy.sensor;
import com.google.gson.JsonObject;
import com.smxy.sensor.bean.LightData;
import com.smxy.sensor.config.MyEndpointConfigure;
import com.smxy.sensor.dao.LightDataDao;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArraySet;
@Data
@Component
@Accessors(chain = true)
@ServerEndpoint(value = "/productWebSocket/{name}", configurator = MyEndpointConfigure.class)
public class ProductWebSocket {
// concurrent包的线程安全Set,用来存放每个客户端对应的ProductWebSocket对象
private static CopyOnWriteArraySet<ProductWebSocket> webSocketSet = new CopyOnWriteArraySet<ProductWebSocket>();
private Session session;
@Autowired
private LightDataDao lightDataDao;
/**
* 添加测试线程
*/
public void addTestThread() {
new Thread(() -> {
Random random = new Random();
while (true) {
try {
Thread.sleep(1000);
String lightData = String.format("%.2f", random.nextInt(300) + 100 + Math.random());
/* 光照数据 */
LightData lightDataClass = new LightData().setTime(new Date()).setData(lightData);
boolean res1 = lightDataDao.addData(lightDataClass);
System.out.println("lightDataDaoResult: " + lightData);
this.judgeData("Light", lightDataClass);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(@PathParam("name") String name, Session session) {
System.out.println(name + "-建立连接");
this.session = session;
webSocketSet.add(this);
/* 测试实时页面实时显示数据 */
addTestThread();
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
System.out.println("连接关闭");
webSocketSet.remove(this);
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("ID: " + session.getId() + " message: " + message);
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 发送消息
*/
public void sendMessage(String message) {
try {
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 群发消息
*/
public void sendGroupMessage(String message) {
for (ProductWebSocket webSocket : webSocketSet) {
webSocket.sendMessage(message);
}
}
/**
* 将更新的数据发送给前端
*/
public void judgeData(String type, LightData lightData) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("data", Double.parseDouble(lightData.getData()));
jsonObject.addProperty("time", lightData.getTime().getTime());
/* 发送更新消息 */
this.sendGroupMessage("update" + type + "|" + jsonObject.toString());
return;
}
}
代码中,我在建立连接的一开始,就加入了一个线程,用来模拟实时更新数据库,更新数据库的同时,我们将插入到数据库的这条数据,也通过 gson 辅助工具,将这条数据转为 Json 数据发送给前端。
处理 JSON 数据,并发送消息,就是上述类中的 judgeData(String message)
这个方法中实现了。
这里为什么需要将更新的数据,发送给前端,等到我们解决第二个问题时候,再来解释。
3. 前端建立连接
刚刚,我们在编写 WebSocket 类的时候,也说到,需要发送一个请求,连建立长连接。
接下来我们就需要前端编写 JS 来建立长连接。
通过下述代码,我们就成功的建立的长连接。
var websocket = null;
// 判断当前浏览器是否支持 WebSocket
if ('WebSocket' in window) {
// 连接 WebSocket 节点
websocket = new WebSocket("ws://localhost:11111/productWebSocket/graphDataPage");
} else {
alert("浏览器不支持 WebSocket,无法正常使用,请更换浏览器");
}
三、前端展示实时数据
当连接建立后,我们就可以和后端一样的,编写一个方法,来监听后端反馈给前端的更新信号
这里解释下,这里做了一个字符串的分割,使用的是 | 来进行分割,主要是因为用来表示一种格式:信号 | 数据。
通过前面的信号来判断当前是什么操作,随后使用前端的 JSON.parse()
来将 JSON 对象转换为对象。
/* 接收到消息后的回调函数 */
websocket.onmessage = function (event) {
console.log(event.data);
var str = event.data.split("|");
if (str[0] == 'updateLight') {
var data = JSON.parse(str[1]);
chartLight.series[0].addPoint([data.time, data.data], true);
}
}
在官方中,给了一个实时显示的代码例子,通过这个例子
在这里,可以看到我使用了一个方法 addPoint
,该方法的详细介绍在:https://api.hcharts.cn/highcharts#Series.addPoint
这个方法的,主要的作用是添加一个数据点。改方法实际上有4个参数。
第一个参数:点的坐标
第二个参数:是否在添加点后,重绘图表(默认:true)
第三个参数:是否在添加点后,删除第一个点(默认:false)
第四个参数:新增点时包含默认动画效果(默认:true)
这里我只使用到了三个参数,点的坐标、重绘图表,不删除第一点。
看到这里就知道,更新后为什么需要将更新的数据发送给前端,原因就是前端展示,需要将新的数据添加到图表中。
如果你不这样做,而是重新发送最开始创建图表的请求,则效果就是图表会头到尾重新绘制一遍。这不是我们要的实时显示的效果。
来看看,我们实时更新的效果
完整的 JS 代码:
var chartLight = null;
/* 关闭世界标准时间 */
/* 不关闭会出现解析时间错误 */
Highcharts.setOptions({global: {useUTC: false}});
/* JSON 数据格式 */
/* 参数:[[1370131200000, 0.7695]] */
/* 时间(long)、数据 */
var httpUrl = "http://localhost:11111/getGraphData"
/* 初始化折线图数据 */
getLightGraphData();
/* 获取光照折线图数据 */
function getLightGraphData() {
$.getJSON(httpUrl + "/Light", function (data) {
chartLight = Highcharts.chart('containerLight', {
/* 图标配置 */
chart: {
zoomType: 'x',
type: 'spline'
},
/* 标题 */
title: {
text: '光照折线图'
},
/* 副标题 */
subtitle: {
text: document.ontouchstart === undefined ?
'鼠标拖动可以进行缩放' : '手势操作进行缩放'
},
/* x轴 */
xAxis: {
/* 坐标轴类型 */
type: 'datetime',
/* 时间轴标签格式化字符串 */
dateTimeLabelFormats: {
millisecond: '%H:%M:%S.%L',
second: '%H:%M:%S',
minute: '%H:%M',
hour: '%H:%M',
day: '%Y-%m-%d',
week: '%m-%d',
month: '%Y-%m',
year: '%Y'
}
},
/* 数据提示框 */
tooltip: {
/* 时间轴标签格式化字符串 */
dateTimeLabelFormats: {
second: '%Y-%m-%d %H:%M:%S',
minute: '%Y-%m-%d %H:%M',
hour: '%Y-%m-%d %H',
day: '%Y-%m-%d',
week: '%Y-%m-%d',
month: '%Y-%m',
year: '%Y'
}
},
/* y轴 */
yAxis: {
/* 标题 */
title: {
text: '光照'
},
/* 轴标签 */
labels: {
formatter: function () {
return this.value + " xl"
}
}
},
/* 图例 */
legend: {
/* 图例开关 */
enabled: false
},
/* 数据列 */
series: [{
/* 名字 */
name: '光照',
/* 显示数据 */
data: data.extend.list,
/* 设置折线颜色 */
color: '#f7a35c',
}]
});
});
}
var websocket = null;
// 判断当前浏览器是否支持 WebSocket
if ('WebSocket' in window) {
// 连接 WebSocket 节点
websocket = new WebSocket("ws://localhost:11111/productWebSocket/graphDataPage");
} else {
alert("浏览器不支持 WebSocket,无法正常使用,请更换浏览器");
}
/* 接收到消息后的回调函数 */
websocket.onmessage = function (event) {
console.log(event.data);
var str = event.data.split("|");
if (str[0] == 'updateLight') {
var data = JSON.parse(str[1]);
chartLight.series[0].addPoint([data.time, data.data], true);
}
}
获取图表数据的请求代码:
中间的 lightDataDao 是 Dao 对象,用来执行数据库操作的类,属于 Mybatis 的知识点。
/**
* 获取折线图数据
*/
@GetMapping("/getGraphData/{type}")
@ResponseBody
public ResultData getGraphData(@PathVariable("type") String type) {
Object dataObject[][] = null;
List<LightData> lightData = lightDataDao.getAllData();
dataObject = new Object[lightData.size()][2];
for (int i = 0; i < lightData.size(); i++) {
dataObject[i][0] = lightData.get(i).getTime().getTime();
dataObject[i][1] = Double.parseDouble(lightData.get(i).getData());
}
return ResultData.success().add("list", dataObject);
}
结果封装类:
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.HashMap;
import java.util.Map;
@Data
@Accessors(chain = true)
public class ResultData {
/* 状态码:200正常;400错误 */
private Integer code;
/* 提示信息 */
private String msg;
/* 返回给前端的数据 */
private Map<String, Object> extend = new HashMap<>();
public static ResultData success() {
return new ResultData().setCode(200).setMsg("成功");
}
public static ResultData failure() {
return new ResultData().setCode(400).setMsg("失败");
}
public ResultData add(String key, Object value) {
this.extend.put(key, value);
return this;
}
}