项目描述:springboot项目,数据可视化,实现的功能是从集群当中获取Hbase Table 的名字,然后你可以选择获取所有数据还是条件查找,使用获得的数据封装到List<Map>当中,再传到前端地echarts中做数据可视化。
前端echarts的代码可以见我前面写的博客,当然这边也会稍微贴一点点出来,我们就先从ServiceImpl说起吧。
HbaseServiceImpl:
连接数据库
用注入的方式注入hbase相关的配置,这边需要用到三个配置的东西
1、application.yml
hbase:
config:
hbase.zookeeper.quorum: server1,server2,server3
hbase.zookeeper.property.clientPort: 2181
zookeeper.znode.parent: /hbase-unsecure
其中server1、2、3要替换成你自己的集群的地址
2、HbaseProperties.java
(注入yml到porperties)
@ConfigurationProperties(prefix = "hbase")
public class HBaseProperties {
private Map<String,String> config ;
public Map<String, String> getConfig() {
return config;
}
public void setConfig (Map<String , String> config){
this.config = config;
}
}
3、HbaseConfig.java
(使得配置生效)
@Configuration
@EnableConfigurationProperties(HBaseProperties.class)
public class HBaseConfig {
private final HBaseProperties properties;
public HBaseConfig(HBaseProperties properties) {
this.properties = properties;
}
public org.apache.hadoop.conf.Configuration configuration() {
org.apache.hadoop.conf.Configuration configuration = HBaseConfiguration.create();
Map<String, String> config = properties.getConfig();
Set<String> keySet = config.keySet();
for (String key : keySet) {
configuration.set(key, config.get(key));
}
return configuration;
}
}
4、然后在实现类当中连接hbase就可以了
HTable hTable = new HTable(con.configuration(), tableName );
当然这以上四步的代码可以替换成
configuration = new Configuration();
configuration.set( "hbase.zookeeper.quorum","server1:2181,server2:2181,server3:2181" );
configuration.set( "zookeeper.znode.parent", "/hbase-unsecure" );
connection = ConnectionFactory.createConnection( con.configuration() ); // hbase链接
table = connection.getTable( TableName.valueOf( tableName ) );
结果是一样的。
连上数据库了之后,可以使用查询所有数据来验证自己是否真的连上了,当然,连接过程如果没有抛出异常,就大概率说明连上了已经。
public List<Map> getValueAll(String tableName) throws IOException {
List<Map> steelList = new ArrayList<>();
Steel steel = new Steel();
HTable hTable = new HTable(con.configuration(), tableName ); //连接hbase,配置项已经在其他地方配好了
Scan scan = new Scan();
ResultScanner resultScanner = hTable.getScanner( scan );
int i =0;
for (Result result : resultScanner) {
Map<String, Object> columnMap = new HashMap<>();
String rowKey = null;
for (Cell cell : result.listCells()) {
if (rowKey == null) { //判断行键是否为空,不为空就加入不为空的所有数据
rowKey = Bytes.toString(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
}
columnMap.put(Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength()), Bytes.toString(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength()));
if(columnMap != null) {
steelList.add( columnMap );
}
}
}
return steelList;
}
此时return的List<Steel>中每个Object中都包括了你的该行簇的一整套的字段和它的值。
当然你也可以选择打印出来。到这里,连Hbase数据库就完成了。
遍历查找并过滤数据
上层的代码已经告诉了怎么去查找所有的值的代码,如果是空值,就剔除。我在获取数据的过程当中发现,某个属性的值也可能为空,于是又做了一层的判断
if(columnMap != null) {
//到这一步已经成功获取了某一个数据的三个属性
String prod_date = String.valueOf( columnMap.get( "PROD_DATE" ) ); //获取其中PROD_DATE属性作比较,先判断是否为空,再判断是否在时间范围内
if (prod_date .equals("null")){
continue;
}else{
try {
if((dateFormat.parse(prod_date).getTime()<dateFormat.parse(endTime).getTime()) && (dateFormat.parse(prod_date).getTime() > dateFormat.parse(startTime).getTime())){
steelList.add( columnMap ); //steelList存放符合条件的数据,形式为[{},{},{}..]
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
上面做了一个日期的判断,这就是我要说的第三层
根据条件,查找对应条件下的值存放到list当中去
我们做的是根据时间,将符合输入时间区间的值放到list当中。
代码也贴在前面那里了,hbase因为是NOSQL,所以它妹有条件查询的sql语句,它支持
1、按指定RowKey获取唯一一条记录,get方法(org.apache.hadoop.hbase.client.Get)
2、按指定的条件获取一批记录,scan方法(org.apache.hadoop.hbase.client.Scan)
上面的scan方法就是获取所有数据的。
那如果我想根据行键、表格、列簇、列名获取某个单独的数据怎么做呢,请看下文
public String getValue(String tableName,String rowKey,String family,String column){
String res = ""; //返回结果
Table table = null;
if (StringUtils.isBlank(tableName) || StringUtils.isBlank(family) //判断输入值是否为空
|| StringUtils.isBlank(rowKey) || StringUtils.isBlank(column)) {
return null;
}
try{
connection = ConnectionFactory.createConnection( con.configuration() ); // hbase链接
table = connection.getTable( TableName.valueOf( tableName ) );
Get g = new Get( rowKey.getBytes() );
g.addColumn( family.getBytes(),column.getBytes() );
Result result = table.get( g );
List<Cell> ceList = result.listCells();
if (ceList != null && ceList.size() > 0) {
for (Cell cell : ceList) {
res = Bytes.toString(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
}
}
}catch (Exception e){
e.printStackTrace();
}
return res;
}
以上就是底层的查询层的代码,控制层就是调用接口获取一下这个方法值,传入参数值(这里是startTime和endTime),然后得到相应list,传回前端。
前端用的是LayUI的laydate以及echarts的渲染
Layui
Echarts
Layui
layui.use(['layer', 'form','table','jquery','laydate'], function() { //用之前一定要先定义
var layer = layui.layer
, form = layui.form, table = layui.table, $ = layui.jquery, laydate = layui.laydate;
var applyTime = laydate.render({
elem: '#startTime',
format: 'yyyyMMdd' //可任意组合
});
var deadTime = laydate.render({
elem: '#endTime',
format: 'yyyyMMdd' //可任意组合
});
});
Echarts
function search(){
$.post({
url:"/visualization",
data:{
"startTime":$("#startTime").val(),
"endTime":$("#endTime").val(),
},
async: false,
error : function () {
alert("查询出错");
},
success: function(data) {
data1 = data;
generate_pricture();
}
})
}
/***
* 生成 工作图的函数
*/
function generate_pricture() {
var data2 = new Array(2); //data2是一个Array,用来存放我们将json数据转化之后的结果,底下是它的初始化和声明
data2[0] = new Array(data1.length);
data2[1] = new Array(data1.length);
for (i=0;i<data1.length;i++){
data2[0][i] = new Array(3);
data2[1][i] = new Array(3);
}
var dom = document.getElementById("container");
var myChart = echarts.init(dom);
var yesLength =0; //版批号有异常的 数据 有多少条
var noLength = 0; //版批号无异常的 数据 有多少条
for(i = 0; i <= data1.length; i++){
if(data1[i] == null){
continue;
}
if(data1[i].hasOwnProperty('SUMSLAB_NO')){
if(data1[i].SUMSLAB_NO == "yes"){
data2[0][yesLength][0] = data1[i].PROD_DATE;
data2[0][yesLength][1] = data1[i].WEIGHT_RSLT;
data2[0][yesLength][2] = data1[i].SUMSLAB_NO;
yesLength++; //data2[0][yeslength] 现在存放的是版批号有异常的 数组
}
else{
data2[1][noLength][0] = data1[i].PROD_DATE;
data2[1][noLength][1] = data1[i].WEIGHT_RSLT;
data2[1][noLength][2] = data1[i].SUMSLAB_NO;
noLength++; //data2[1][nolength] 现在存放的是版批号无异常的 数组
}
}else{
continue;
}
}
//从这里开始是echarts本身的API,基本思路是,有异常数据存一边,无异常数据存一边,通过属性去控制他们的颜色和大小就好了
var option = {
backgroundColor: new echarts.graphic.RadialGradient(0.1, 0.1, 0.8, [{
offset: 0,
color: '#f7f8fa'
}, {
offset: 1,
color: '#cdd0d5'
}]),
title: {
text: '板材异常状况图',
},
grid: {
left: '3%',
right: '7%',
bottom: '3%',
containLabel: true
},
tooltip: {
showDelay: 0,
enterable: true,
formatter: function (params) {
var table = '<table style="width:100%;text-align:center"><tbody><tr>'
+ '<tr> <td>' + params.seriesName + '<a href="test.html">详情</a></td></tr>'
+ '<td>' + params.value[0] + ',</td>'
+ '<td>' + params.value[1] + ' </td>'
+ '</tr>';
table += '</tbody></table>';
return table;
}
},
toolbox: {
show: true,
feature: {
mark: {show: true},
dataView: {show: true, readOnly: false},
restore: {show: true},
saveAsImage: {show: true}
}
},
brush: {},
legend: {
left: 'center',
data: ['异常', '正常']
},
xAxis: [
{
name: '日期',
type: 'category',
scale: true,
axisLabel: {
formatter: '{value}'
},
splitLine: {
show: false
}
}
],
yAxis: [
{
name: '权重实绩',
type: 'value',
scale: true,
axisLabel: {
formatter: '{value}'
},
splitLine: {
show: false
}
}
],
series: [
{
symbolSize: 10,
name: '正常',
type: 'scatter',
data: data2[0],
markArea: {
silent: true,
itemStyle: {
normal: {
color: 'transparent',
borderWidth: 1,
borderType: 'dashed'
}
},
data: [[{
name: '正常分布区间',
xAxis: 'min',
yAxis: 'min'
}, {
xAxis: 'max',
yAxis: 'max'
}]]
},
markPoint: {
data: [
{type: 'max', name: '最大值'},
{type: 'min', name: '最小值'}
]
},
type: 'scatter',
markLine: {
lineStyle: {
normal: {
type: 'solid'
}
},
data: [
{type: 'average', name: '平均值'},
]
}
},
{
symbolSize: 10,
name: '异常',
type: 'scatter',
data:data2[1],
markArea: {
silent: true,
itemStyle: {
normal: {
color: 'transparent',
borderWidth: 1,
borderType: 'dashed'
}
},
data: [[{
name: '异常分布区间',
xAxis: 'min',
yAxis: 'min'
}, {
xAxis: 'max',
yAxis: 'max'
}]]
},
markPoint: {
data: [
{type: 'max', name: '最大值'},
{type: 'min', name: '最小值'}
]
},
type: 'scatter',
markLine: {
lineStyle: {
normal: {
type: 'solid'
}
},
data: [
{type: 'average', name: '平均值'},
]
}
}]
};
myChart.setOption(option);
}
总结:
如果你还记得JDBC连接数据库的过程
1、加载数据库驱动
2、填入登录数据
3、建立连接
4、进行数据库增删改查
5、断开数据库连接,关闭流
再来理解一下hbase连接数据库的过程
1、加载驱动(使用hbase_client)
2、填入登录数据(集群地址,端口号,连接方式secure还是unsecure)
3、建立连接
4、增删改查 (这边没法使用sql语句的加载了,因为是非关系型数据库的关系,这部分的操作很不一样)
5、断开连接,关闭流
这是一个很共同的一个过程,不同的就是例如说条件查询,它木有办法搞一个where,它遍历所有的数据,再做挑选,这是我使用的方法,当然我知道少之又少,如果有指正的地方烦请纠正我。然后这一次的过程也让我更深入的立即了SQL和NOSQL的去区别,也再随便说两句。
SQL:使用的是类似二维表的一种样式,我表结构的字段固定,如果有需求,更多的是纵向的扩展,这也符合我们的逻辑思维,它可以支持事务和SQL语句(因为有丰富的数据优化方法来加快检索到那一步操作的过程),但是同时也意味着它不适合大数据量的存储和广泛的频繁发生的数据操作。
NOSQL:表结构就不是很固定,不同的列簇(假如说这边列簇说的是某几个原料加工厂),也许厂区一想要的字段是生产日期、负责人和权重,厂区二想要的只有生产日期和权重,于是它们就可以拥有不同的字段的设置。它对SQL语句和事务不是很适合,例如MongoDB的json样式的存储方法,例如HBase的列簇的做法,sql语句都难以派上很大用场。但是它可以存储大数据,因为它的数据是一块一块存的,不像你关系型数据库那么结构分明那么事多,而且它一般部署起来都比较简单,像Hbase,在Linux上新建一个hbase库,就很容易上手。
SQL数据库有sql server、mysql、oracle等等 ,NOSQL数据库有MongoDB、Hbase、redis等等。