最近在自学Springboot,由于其轻量级的web开发特性,让我萌生了重新开始构建ActiveMQ Web Console的想法。记得去年尝试用nodejs+vue构建一个获取ActiveMQ监控数据的页面,但是中途搁置了,原因有很多,一方面是水平能力的不足,另一方面是nodejs用js让我一直觉得不舒服,毕竟我的主要语言还是java。
springboot用起来顺手多了,随着java水平的提升,这次也摆脱了上次用jolokia接口的约束,还是通过JMX接口来获取数据,大致架构就是通过一个webConsole来连接各个AMQ,获取监控数据,并支持集群能力计算。
大致架构是这样的这样做的好处主要有几点:
- 摆脱需要登录多个ActivmeMQ页面的繁琐操作,一般如果需要通过页面操作集群所有的服务器,需要在web中同时开启N个标签,而如果要清理一个队列,还需要一个个页面进行操作。
- ActiveMQ不能支持集群的计算,在完全图的架构下,同一个队列的数据需要将所有ActiveMQ的数据进行一次相加统计并去除集群间转发的部分,无法从单个ActiveMQ上获取这个数据。
- 最主要的,我想找个东西练练手……
架构及技术要点
由于自己也是新手,所以有不少问题需要解决,对于一个网站来说,无非是前后端的设计和互动,总结起来就是以下的问题
- 前端要展示什么
前端分成两个模块,一个模块展示单个broker状态,另一个计算展示集群状态。
- 后端如何快速获取数据
后端与要展示状态的AMQ保持JMX端口的长连接,快速获取数据。连接设置超时机制,超时主动关闭。同时只能与单个集群的所有Broker建立连接,如果跳转到另一个集群则主动断开与上一个集群的所有连接。
- 如何保存数据
数据保存在内存中,每次前端调用接口时重新获取数据。所以这个网站只是对当前状态的展示,不涉及对历史数据的趋势视图展示。
- 前后端如何交互
前后端通过get,post等方式向后端请求数据,数据为json格式。
技术栈选择
后端毋庸置疑是springboot,前端原来想使用bootstrap,但是前几日看到了layui感觉弹窗很方便,所以这次就选择用layui来做吧。前端使用thymeleaf,主要是用来做router,js使用vue,主要是用vue-resource来替代jquery的ajax调用,通过vue-resource可以不用不停刷页面,只需要更新数据即可。数据的计算通过java写算法实现,图表使用echarts。
搭个框架
少说废话上代码吧。
Controller
package com.springdemo.controller;
import com.springdemo.entity.RespCode;
import com.springdemo.entity.RespEntity;
import com.springdemo.mqinfo.Broker;
import com.springdemo.mqinfo.BrokerInfo;
import org.springframework.boot.SpringApplication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.management.*;
import java.io.IOException;
import java.util.Date;
/**
* @author Cadmean
*/
@Controller
public class AppController {
Broker broker=null;
@RequestMapping("/")
public String indexMain(){
return "index";
}
@GetMapping("/connectBroker.action")
public String connectBroker(@RequestParam(value = "addr", required = false) String addr){
try {
if (this.broker!=null) {
if (!this.broker.getAddr().equalsIgnoreCase(addr)) {
this.broker.closeConnection();
this.broker = new Broker(addr);
}
} else {
this.broker=new Broker(addr);
}
} catch (IOException e) {
e.printStackTrace();
}
return "broker";
}
@RequestMapping(value = "/brokerInfo.json",method=RequestMethod.GET)
@ResponseBody
public RespEntity<BrokerInfo> getBrokerInfo() throws MalformedObjectNameException, InstanceNotFoundException, IOException, ReflectionException, AttributeNotFoundException, MBeanException {
return new RespEntity<BrokerInfo>(RespCode.SUCCESS,this.broker.getBrokerInfo());
}
}
设计点相关:
- 有一个index.html主页,用来输入一个IP,跳转到broker.html页面,展示单个broker状态
- connectBroker.action用于创建一个broker的JMX连接
- brokerInfo.json用于获取broker的基本信息,json用一个RespEntity来封装,主要是封装上请求是否成功的信息。
数据
package com.springdemo.mqinfo;
/**
* @author Cadmean
*/
public class BrokerInfo {
private String brokerName;
private String addr;
private String brokerId;
private String uptime;
private int connection;
private long memoryLimit;
private long storeLimit;
@Override
public String toString() {
return "BrokerInfo{" +
"brokerName='" + brokerName + '\'' +
", addr='" + addr + '\'' +
", brokerId='" + brokerId + '\'' +
", uptime='" + uptime + '\'' +
", connection=" + connection +
", memoryLimit=" + memoryLimit +
", storeLimit=" + storeLimit +
'}';
}
public String getBrokerName() {
return brokerName;
}
public void setBrokerName(String brokerName) {
this.brokerName = brokerName;
}
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
public String getBrokerId() {
return brokerId;
}
public void setBrokerId(String brokerId) {
this.brokerId = brokerId;
}
public int getConnection() {
return connection;
}
public void setConnection(int connection) {
this.connection = connection;
}
public long getMemoryLimit() {
return memoryLimit;
}
public void setMemoryLimit(long memoryLimit) {
this.memoryLimit = memoryLimit;
}
public String getUptime() {
return uptime;
}
public void setUptime(String uptime) {
this.uptime = uptime;
}
public BrokerInfo(String addr) {
this.addr=addr;
}
public long getStoreLimit() {
return storeLimit;
}
public void setStoreLimit(long storeLimit) {
this.storeLimit = storeLimit;
}
}
package com.springdemo.mqinfo;
import javax.management.*;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
/**
* @author Cadmean
*/
public class Broker {
private String url;
private JMXConnector connector;
private MBeanServerConnection mbsc;
private BrokerInfo brokerInfo;
private String addr;
public Broker(String addr) throws IOException {
this.addr=addr;
this.url="service:jmx:rmi:///jndi/rmi://"+addr+":1099/jmxrmi";
this.brokerInfo=new BrokerInfo(this.addr);
createConnector();
}
public String getAddr() {
return addr;
}
private void createConnector() throws IOException {
JMXServiceURL url = new JMXServiceURL(this.url);
connector = JMXConnectorFactory.connect(url, null);
connector.connect();
mbsc = connector.getMBeanServerConnection();
}
public void closeConnection() throws IOException {
this.connector.close();
}
public BrokerInfo getBrokerInfo() throws MalformedObjectNameException, AttributeNotFoundException, MBeanException, ReflectionException, InstanceNotFoundException, IOException {
String objectName="org.apache.activemq:brokerName="+this.addr+",type=Broker";
ObjectName name = new ObjectName(objectName);
this.brokerInfo.setBrokerName((String) mbsc.getAttribute(name,"BrokerName"));
this.brokerInfo.setBrokerId((String) mbsc.getAttribute(name,"BrokerId"));
this.brokerInfo.setConnection((Integer) mbsc.getAttribute(name,"CurrentConnectionsCount"));
this.brokerInfo.setMemoryLimit((Long) mbsc.getAttribute(name,"MemoryLimit"));
this.brokerInfo.setStoreLimit((Long) mbsc.getAttribute(name,"StoreLimit"));
this.brokerInfo.setUptime((String) mbsc.getAttribute(name,"Uptime"));
return brokerInfo;
}
}
Broker类用于存放Info信息和一些数据获取的操作,BrokerInfo用于保存Broker基本数据,相对应的后续还可以扩展出Queue、Topic、Connection等等数据信息类。
封装请求
package com.springdemo.entity;
/**
* @author Cadmean
*/
public enum RespCode {
SUCCESS(0, "ok"),
WARN(-1, "fail");
private int code;
private String msg;
RespCode(int code, String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
package com.springdemo.entity;
/**
* @author Cadmean
*/
public class RespEntity<R> {
private int code;
private String msg;
private R data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public R getData() {
return data;
}
public void setData(R data) {
this.data = data;
}
public RespEntity(RespCode respCode) {
this.code = respCode.getCode();
this.msg = respCode.getMsg();
}
public RespEntity(RespCode respCode, R data) {
this(respCode);
this.data = data;
}
@Override
public String toString() {
return "RespEntity{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
使用RespEntity,并且加了个泛型来作为数据往外传输的通道,RespCode是用来存放返回码和具体返回说明的。在RespEntity中封装上这个就可以在前端得到返回的数据是否正常了。
前端
<!DOCTYPE HTML>
<html >
<head>
<meta content="text/html;charset=UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" th:href="@{/layui/css/layui.css}">
<script src="/webjars/vue/2.5.16/vue.min.js"></script>
<script
</head>
<body>
<div id="app" class="layui-container" style="margin-top:1%">
<ul class="layui-nav" lay-filter="">
<li class="layui-nav-item"><a href="">ActiveMQ Console</a></li>
</ul>
<table class="layui-table layui-anim layui-anim-scale" lay-skin="nob">
<tr>
<td>UpTime</td>
<td id="times" v-on:click="get()">{{ msg.uptime }}</td>
</tr>
<tr>
<td>BrokerName</td>
<td>{{ msg.brokerName }}</td>
</tr>
<tr>
<td>BrokerId</td>
<td>{{ msg.brokerId }}</td>
</tr>
<tr>
<td>Connections</td>
<td>{{ msg.connection }}</td>
</tr>
<tr>
<td>MemoryLimit</td>
<td>{{ Math.floor(msg.memoryLimit/1024/1024) }} MB</td>
</tr>
<tr>
<td>StoreLimit</td>
<td>{{ Math.floor(msg.storeLimit/1024/1024/1024) }} GB</td>
</tr>
</table>
</div>
<script th:inline="javascript">
var vm = new Vue({
el:'#app',
data:{
msg:[],
},
methods:{
get:function(){
//发送get请求
this.$http.get('/brokerInfo').then(function(res){
console.log(res.body.data);
this.$set(this,'msg',res.body.data);
},function(){
console.log('请求失败处理');
});
}
}
});
window.onload=function(){
vm.get();
}
</script>
</body>
</html>
用到了layui的table,vue和vue-resource来进行数据渲染。
公司内部网络限制,主要还是在公司内部开发,初步成果差不多是这样。